Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export async function mainProcessSharedInfraBuilder(): Promise<ContainerBuilder>

builder.register(UploadProgressTracker).use(MainProcessUploadProgressTracker).private();

builder.register(RemoteItemsGenerator).use(SQLiteRemoteItemsGenerator).private();
builder.register(RemoteItemsGenerator).use(SQLiteRemoteItemsGenerator);

return builder;
}
3 changes: 3 additions & 0 deletions src/backend/common/rate-limit/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const INITIAL_RATE_LIMIT_DELAY_MS = 30_000;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You moved this from a module we created to a legacy structure, why?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It has already moved

export const INITIAL_SERVER_ERROR_DELAY_MS = 1_000;
export const MAX_BACKOFF_MS = 480_000;
Original file line number Diff line number Diff line change
@@ -1,27 +1,27 @@
import { DriveDesktopError } from '../../../../context/shared/domain/errors/DriveDesktopError';
import { createBackupUploadErrorHandler } from './backup-upload-error-handler';
import { INITIAL_RATE_LIMIT_DELAY_MS, MAX_BACKOFF_MS, RETRY_DELAYS_MS } from './constants';
import { DriveDesktopError } from '../../../context/shared/domain/errors/DriveDesktopError';
import { createTransientErrorHandler } from './transient-error-handler';
import { INITIAL_RATE_LIMIT_DELAY_MS, INITIAL_SERVER_ERROR_DELAY_MS, MAX_BACKOFF_MS } from './constants';

describe('createBackupUploadErrorHandler', () => {
describe('createTransientErrorHandler', () => {
it('should return null for non-retryable errors', () => {
const handler = createBackupUploadErrorHandler('/file.txt');
const handler = createTransientErrorHandler({ tag: 'BACKUPS', context: 'TEST', path: '/file.txt' });

expect(handler(new DriveDesktopError('UNKNOWN'))).toBeNull();
expect(handler(new DriveDesktopError('NOT_ENOUGH_SPACE'))).toBeNull();
expect(handler(new DriveDesktopError('FILE_ALREADY_EXISTS'))).toBeNull();
});

it('should return exponential backoff delay for INTERNAL_SERVER_ERROR', () => {
const handler = createBackupUploadErrorHandler('/file.txt');
const handler = createTransientErrorHandler({ tag: 'BACKUPS', context: 'TEST', path: '/file.txt' });
const error = new DriveDesktopError('INTERNAL_SERVER_ERROR');

expect(handler(error)).toBe(RETRY_DELAYS_MS[0] * Math.pow(2, 0)); // attempt 1: 1000ms
expect(handler(error)).toBe(RETRY_DELAYS_MS[0] * Math.pow(2, 1)); // attempt 2: 2000ms
expect(handler(error)).toBe(RETRY_DELAYS_MS[0] * Math.pow(2, 2)); // attempt 3: 4000ms
expect(handler(error)).toBe(INITIAL_SERVER_ERROR_DELAY_MS * Math.pow(2, 0)); // attempt 1: 1000ms
expect(handler(error)).toBe(INITIAL_SERVER_ERROR_DELAY_MS * Math.pow(2, 1)); // attempt 2: 2000ms
expect(handler(error)).toBe(INITIAL_SERVER_ERROR_DELAY_MS * Math.pow(2, 2)); // attempt 3: 4000ms
});

it('should cap INTERNAL_SERVER_ERROR delay at MAX_BACKOFF_MS', () => {
const handler = createBackupUploadErrorHandler('/file.txt');
const handler = createTransientErrorHandler({ tag: 'SYNC-ENGINE', context: 'TEST', path: '/file.txt' });
const error = new DriveDesktopError('INTERNAL_SERVER_ERROR');

// base=1000, cap=480000 → attempt 9: 256000ms, attempt 10: 512000ms → capped
Expand All @@ -32,30 +32,30 @@ describe('createBackupUploadErrorHandler', () => {

it('should use retry_after from RATE_LIMITED message as base delay', () => {
const retryAfterMs = 60_000;
const handler = createBackupUploadErrorHandler('/file.txt');
const handler = createTransientErrorHandler({ tag: 'BACKUPS', context: 'TEST', path: '/file.txt' });
const error = new DriveDesktopError('RATE_LIMITED', String(retryAfterMs));

expect(handler(error)).toBe(retryAfterMs * Math.pow(2, 0)); // attempt 1: 60000ms
});

it('should fall back to INITIAL_RATE_LIMIT_DELAY_MS when RATE_LIMITED message is not a number', () => {
const handler = createBackupUploadErrorHandler('/file.txt');
const handler = createTransientErrorHandler({ tag: 'SYNC-ENGINE', context: 'TEST', path: '/file.txt' });
const error = new DriveDesktopError('RATE_LIMITED', 'not-a-number');

expect(handler(error)).toBe(INITIAL_RATE_LIMIT_DELAY_MS);
});

it('should apply exponential backoff across multiple RATE_LIMITED retries', () => {
const retryAfterMs = 10_000;
const handler = createBackupUploadErrorHandler('/file.txt');
const handler = createTransientErrorHandler({ tag: 'BACKUPS', context: 'TEST', path: '/file.txt' });
const error = new DriveDesktopError('RATE_LIMITED', String(retryAfterMs));

expect(handler(error)).toBe(retryAfterMs * Math.pow(2, 0)); // attempt 1: 10000ms
expect(handler(error)).toBe(retryAfterMs * Math.pow(2, 1)); // attempt 2: 20000ms
});

it('should share attempt counter between RATE_LIMITED and INTERNAL_SERVER_ERROR', () => {
const handler = createBackupUploadErrorHandler('/file.txt');
const handler = createTransientErrorHandler({ tag: 'SYNC-ENGINE', context: 'TEST', path: '/file.txt' });

handler(new DriveDesktopError('INTERNAL_SERVER_ERROR')); // attempt 1, base=1000 → 1000ms
const delay = handler(new DriveDesktopError('RATE_LIMITED', String(INITIAL_RATE_LIMIT_DELAY_MS))); // attempt 2, base=30000 → 60000ms
Expand All @@ -64,14 +64,14 @@ describe('createBackupUploadErrorHandler', () => {
});

it('should create independent state per handler instance', () => {
const handler1 = createBackupUploadErrorHandler('/file1.txt');
const handler2 = createBackupUploadErrorHandler('/file2.txt');
const handler1 = createTransientErrorHandler({ tag: 'BACKUPS', context: 'TEST', path: '/file1.txt' });
const handler2 = createTransientErrorHandler({ tag: 'SYNC-ENGINE', context: 'TEST', path: '/file2.txt' });
const error = new DriveDesktopError('INTERNAL_SERVER_ERROR');

handler1(error); // advance handler1 to attempt 1
handler1(error); // advance handler1 to attempt 2

// handler2 should start fresh at attempt 1
expect(handler2(error)).toBe(RETRY_DELAYS_MS[0]);
expect(handler2(error)).toBe(INITIAL_SERVER_ERROR_DELAY_MS);
});
});
67 changes: 67 additions & 0 deletions src/backend/common/rate-limit/transient-error-handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { logger } from '@internxt/drive-desktop-core/build/backend';
import { DriveDesktopError } from '../../../context/shared/domain/errors/DriveDesktopError';
import { extractPropertyFromStringyfiedJson } from '../../../shared/extract-property-from-json';
import { INITIAL_RATE_LIMIT_DELAY_MS, INITIAL_SERVER_ERROR_DELAY_MS, MAX_BACKOFF_MS } from './constants';

export function parseRetryAfterMs(message?: string) {
const retryAfterSeconds = extractPropertyFromStringyfiedJson(message ?? '', 'retry_after');
return typeof retryAfterSeconds === 'number' ? retryAfterSeconds * 1000 : INITIAL_RATE_LIMIT_DELAY_MS;
}

export function mapEnvironmentUploadError(err: Error & { code?: unknown; status?: unknown }): DriveDesktopError {
if (err.code === 'EACCES' || err.code === 'EPERM') {
return new DriveDesktopError('ACTION_NOT_PERMITTED', err.message);
}
if (err.message === 'Max space used') {
return new DriveDesktopError('NOT_ENOUGH_SPACE');
}
if (typeof err.status === 'number') {
if (err.status === 429) {
return new DriveDesktopError('RATE_LIMITED', String(parseRetryAfterMs(err.message)));
}
if (err.status >= 500) {
return new DriveDesktopError('INTERNAL_SERVER_ERROR');
}
}
return new DriveDesktopError('UNKNOWN', err.message);
}

function exponentialBackoff(attempts: number, baseMs: number) {
return Math.min(baseMs * Math.pow(2, attempts - 1), MAX_BACKOFF_MS);
}

type Props = {
tag: 'BACKUPS' | 'SYNC-ENGINE';
context: string;
path: string;
};

export function createTransientErrorHandler({ tag, context, path }: Props) {
let transientAttempts = 0;

return (error: DriveDesktopError): number | null => {
if (error.cause === 'RATE_LIMITED' || error.cause === 'INTERNAL_SERVER_ERROR') {
transientAttempts++;

const baseDelayMs =
error.cause === 'RATE_LIMITED'
? Number(error.message) || INITIAL_RATE_LIMIT_DELAY_MS
: INITIAL_SERVER_ERROR_DELAY_MS;

const delayMs = exponentialBackoff(transientAttempts, baseDelayMs);

logger.debug({
tag,
msg: `[${context}]`,
cause: error.cause,
attempt: transientAttempts,
delayMs,
path,
});

return delayMs;
}

return null;
};
}
28 changes: 0 additions & 28 deletions src/backend/features/backup/upload/backup-upload-error-handler.ts

This file was deleted.

4 changes: 0 additions & 4 deletions src/backend/features/backup/upload/constants.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1 @@
export const DEFAULT_CONCURRENCY = 6;
export const MAX_RETRIES = 3;
export const RETRY_DELAYS_MS = [1000, 2000, 4000];
export const INITIAL_RATE_LIMIT_DELAY_MS = 30_000;
export const MAX_BACKOFF_MS = 480_000;
6 changes: 3 additions & 3 deletions src/backend/features/backup/upload/update-file-to-backup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { DriveDesktopError } from '../../../../context/shared/domain/errors/Driv
import { Result } from '../../../../context/shared/domain/Result';
import { uploadContentToEnvironment } from './upload-content-to-environment';
import { retryWithBackoff } from '../../../../shared/retry-with-backoff';
import { createBackupUploadErrorHandler } from './backup-upload-error-handler';
import { createTransientErrorHandler } from '../../../../backend/common/rate-limit/transient-error-handler';
import { overrideFileToBackend } from './override-file-to-backend';

export type UpdateFileParams = {
Expand All @@ -25,7 +25,7 @@ async function updateFile(file: UpdateFileParams): Promise<Result<void, DriveDes
environment: file.environment,
signal: file.signal,
}),
createBackupUploadErrorHandler(file.path),
createTransientErrorHandler({ tag: 'BACKUPS', context: 'BACKUP UPLOAD RETRY', path: file.path }),
file.signal,
);

Expand All @@ -44,7 +44,7 @@ async function updateFile(file: UpdateFileParams): Promise<Result<void, DriveDes
fileContentsId: contentsId,
fileSize: file.size,
}),
createBackupUploadErrorHandler(file.path),
createTransientErrorHandler({ tag: 'BACKUPS', context: 'BACKUP UPLOAD RETRY', path: file.path }),
file.signal,
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import { createReadStream } from 'node:fs';
import { UploadStrategyFunction } from '@internxt/inxt-js/build/lib/core';
import { Result } from '../../../../context/shared/domain/Result';
import { logger } from '@internxt/drive-desktop-core/build/backend';
import { extractPropertyFromStringyfiedJson } from '../../../../shared/extract-property-from-json';
import { isError } from '../../../../shared/errors/is-error';
import { safeAccess } from '../../../../infra/local-file-system/safe-access';
import { mapEnvironmentUploadError } from '../../../../backend/common/rate-limit/transient-error-handler';

export type ContentUploadParams = {
path: string;
Expand All @@ -16,25 +16,6 @@ export type ContentUploadParams = {
environment: Environment;
signal: AbortSignal;
};
function mapUploadError(err: Error & { code?: unknown; status?: unknown }): DriveDesktopError {
if (err.code === 'EACCES' || err.code === 'EPERM') {
return new DriveDesktopError('ACTION_NOT_PERMITTED', err.message);
}
if (err.message === 'Max space used') {
return new DriveDesktopError('NOT_ENOUGH_SPACE');
}
if (typeof err.status === 'number') {
if (err.status === 429) {
const retryAfter = extractPropertyFromStringyfiedJson(err.message, 'retry_after');
const retryAfterMs = typeof retryAfter === 'number' ? retryAfter * 1000 : 30_000;
return new DriveDesktopError('RATE_LIMITED', String(retryAfterMs));
}
if (err.status >= 500) {
return new DriveDesktopError('INTERNAL_SERVER_ERROR');
}
}
return new DriveDesktopError('UNKNOWN');
}

export async function uploadContentToEnvironment({
path,
Expand Down Expand Up @@ -66,7 +47,7 @@ export async function uploadContentToEnvironment({

readable.on('error', (err: Error & { code?: unknown; status?: unknown }) => {
logger.error({ tag: 'BACKUPS', msg: '[ENVLFU READ STREAM ERROR]', err, path });
resolveOnce({ error: mapUploadError(err) });
resolveOnce({ error: mapEnvironmentUploadError(err) });
});

const state = uploadFn(bucket, {
Expand All @@ -77,7 +58,7 @@ export async function uploadContentToEnvironment({

if (err) {
logger.error({ tag: 'BACKUPS', msg: '[ENVLFU UPLOAD ERROR]', err });
return resolveOnce({ error: mapUploadError(err) });
return resolveOnce({ error: mapEnvironmentUploadError(err) });
}

if (!contentsId) {
Expand All @@ -103,7 +84,7 @@ export async function uploadContentToEnvironment({
});
} catch (err) {
if (isError(err)) {
return { error: mapUploadError(err) };
return { error: mapEnvironmentUploadError(err) };
}
return { error: new DriveDesktopError('UNKNOWN') };
}
Expand Down
6 changes: 3 additions & 3 deletions src/backend/features/backup/upload/upload-file-to-backup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { uploadContentToEnvironment } from './upload-content-to-environment';
import { Result } from '../../../../context/shared/domain/Result';
import { deleteFileFromStorageByFileId } from '../../../../infra/drive-server/services/files/services/delete-file-content-from-bucket';
import { retryWithBackoff } from '../../../../shared/retry-with-backoff';
import { createBackupUploadErrorHandler } from './backup-upload-error-handler';
import { createTransientErrorHandler } from '../../../../backend/common/rate-limit/transient-error-handler';

export type UploadFileParams = {
path: string;
Expand All @@ -29,7 +29,7 @@ async function uploadFile(file: UploadFileParams): Promise<Result<File | null, D
environment: file.environment,
signal: file.signal,
}),
createBackupUploadErrorHandler(file.path),
createTransientErrorHandler({ tag: 'BACKUPS', context: 'BACKUP UPLOAD RETRY', path: file.path }),
file.signal,
);

Expand All @@ -51,7 +51,7 @@ async function uploadFile(file: UploadFileParams): Promise<Result<File | null, D
folderUuid: file.folderUuid,
bucket: file.bucket,
}),
createBackupUploadErrorHandler(file.path),
createTransientErrorHandler({ tag: 'BACKUPS', context: 'BACKUP UPLOAD RETRY', path: file.path }),
file.signal,
);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,28 @@
import { RemoteTreeBuilder } from '../../../../context/virtual-drive/remoteTree/application/RemoteTreeBuilder';
import { RemoteItemsGenerator } from '../../../../context/virtual-drive/remoteTree/domain/RemoteItemsGenerator';
import { FolderRepositorySynchronizer } from '../../../../context/virtual-drive/folders/application/FolderRepositorySynchronizer/FolderRepositorySynchronizer';
import { FileRepositorySynchronizer } from '../../../../context/virtual-drive/files/application/FileRepositorySynchronizer';
import { StorageRemoteChangesSyncher } from '../../../../context/storage/StorageFiles/application/sync/StorageRemoteChangesSyncher';
import { ServerFolderStatus } from '../../../../context/shared/domain/ServerFolder';
import { logger } from '@internxt/drive-desktop-core/build/backend';
import { User } from '../../../../apps/main/types';
import { Container } from 'diod';

// This is the old src/apps/drive/fuse/FuseApp.update
export async function updateVirtualDriveContainer({ container, user }: { container: Container; user: User }) {
try {
const tree = await container.get(RemoteTreeBuilder).run(user.root_folder_id, user.rootFolderId);
const [tree, allRemoteItems] = await Promise.all([
container.get(RemoteTreeBuilder).run(user.root_folder_id, user.rootFolderId),
container.get(RemoteItemsGenerator).getAll(),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why a promise all and not wait for the treeBuilder to finish?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't have to wait for the tree to be ready; both tasks can be done at the same time without interfering with each other.

]);

const deletedFolderIds = new Set(
allRemoteItems.folders.filter((f) => f.status !== ServerFolderStatus.EXISTS).map((f) => f.id),
);

await Promise.all([
container.get(FileRepositorySynchronizer).run(tree.files),
container.get(FolderRepositorySynchronizer).run(tree.folders),
container.get(FolderRepositorySynchronizer).run(tree.folders, deletedFolderIds),
container.get(StorageRemoteChangesSyncher).run(),
]);
logger.debug({ msg: '[VIRTUAL DRIVE] Tree updated successfully' });
Expand Down
Loading
Loading