Compare commits

..
Author SHA1 Message Date
Sonarly Claude Code 22cf30b987 fix(file-storage): add adaptive retry and backoff for S3 rate-limit errors
https://sonarly.com/issue/32646?type=bug

The workflow worker fails to upload built logic function code to S3 when AWS returns a 503 SlowDown rate-limit error, causing workflow runs to fail. The S3 client has no custom retry configuration and the default 3-attempt retry with standard backoff is insufficient during request bursts.

Fix: packages/twenty-server/src/engine/core-modules/file-storage/drivers/s3.driver.ts

Add `maxAttempts: 5` and `retryMode: 'adaptive'` to both S3 client instantiations in the constructor (main client and presign client). The adaptive retry mode uses a client-side token bucket to dynamically reduce request rate when S3 returns 503 SlowDown responses, preventing cascading failures during high-concurrency worker bursts. The increased maxAttempts (from default 3 to 5) gives the adaptive backoff strategy sufficient headroom to succeed on transient rate limits.

Constructor: S3 client now created with `{ ...s3Options, region, endpoint, maxAttempts: 5, retryMode: 'adaptive' }` instead of `{ ...s3Options, region, endpoint }`. Same change applied to the presign client when presignEndpoint is provided.
2026-04-29 19:46:51 +00:00
2 changed files with 23 additions and 26 deletions
@@ -57,12 +57,24 @@ export class S3Driver implements StorageDriver {
return;
}
this.s3Client = new S3({ ...s3Options, region, endpoint });
this.s3Client = new S3({
...s3Options,
region,
endpoint,
maxAttempts: 5,
retryMode: 'adaptive',
});
this.bucketName = bucketName;
if (presignEnabled) {
this.presignClient = presignEndpoint
? new S3({ ...s3Options, region, endpoint: presignEndpoint })
? new S3({
...s3Options,
region,
endpoint: presignEndpoint,
maxAttempts: 5,
retryMode: 'adaptive',
})
: this.s3Client;
}
}
@@ -109,7 +121,14 @@ export class S3Driver implements StorageDriver {
Bucket: this.bucketName,
});
await this.s3Client.send(command);
try {
await this.s3Client.send(command);
} catch (error) {
this.logger.error(
`S3 writeFile failed for key "${params.filePath}": ${error.name ?? 'UnknownError'} - ${error.message}`,
);
throw error;
}
}
private async createFolder(path: string) {
@@ -1,7 +1,5 @@
import { createHash } from 'crypto';
import { Logger } from '@nestjs/common';
import RedisStore from 'connect-redis';
import { createClient } from 'redis';
@@ -10,8 +8,6 @@ import type session from 'express-session';
import { CacheStorageType } from 'src/engine/core-modules/cache-storage/types/cache-storage-type.enum';
import { type TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
const logger = new Logger('SessionStorage');
export const getSessionStorageOptions = (
twentyConfigService: TwentyConfigService,
): session.SessionOptions => {
@@ -61,28 +57,10 @@ export const getSessionStorageOptions = (
const redisClient = createClient({
url: connectionString,
pingInterval: 30_000,
socket: {
reconnectStrategy: (retries: number) => {
const delay = Math.min(retries * 500, 5000);
logger.warn(
`Redis session storage reconnecting (attempt ${retries}, next retry in ${delay}ms)`,
);
return delay;
},
},
});
redisClient.on('error', (err) => {
logger.error(`Redis session storage error: ${err.message}`);
});
redisClient.connect().catch((err) => {
logger.error(
`Redis session storage initial connection failed: ${err.message}`,
);
throw new Error(`Redis connection failed: ${err}`);
});
return {