Skip to content

Commit

Permalink
✨Ignore errors during nextcloud migration to be able to import files …
Browse files Browse the repository at this point in the history
…partially for big migrations
  • Loading branch information
shepilov committed Jul 16, 2024
1 parent e76da87 commit 768dadc
Show file tree
Hide file tree
Showing 4 changed files with 140 additions and 6 deletions.
81 changes: 81 additions & 0 deletions tdrive/backend/utils/nextcloud-migration/src/executor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { logger } from "./logger";

type FunctionArgs = any[];

type FunctionStats = {
functionName: string;
successfulExecutions: number;
failedExecutions: number;
failedExecutionsArgs: FunctionArgs[];
};

export class FunctionExecutor {
private stats: Map<string, FunctionStats> = new Map();

async executeWithRetries(
fn: (...args: any[]) => Promise<any>,
args: FunctionArgs,
retries: number = 3,
throwError: boolean = false,
): Promise<any> {
const functionName = fn.name;
let success = false;
let executionResult = null;

for (let attempt = 0; attempt < retries; attempt++) {
try {
executionResult = await fn(...args);
success = true;
break;
} catch (error) {
executionResult = error;
logger.info(`Attempt ${attempt + 1} failed for ${functionName}`, error);
}
}

if (!success && throwError) throw executionResult;

let stats = this.stats.get(functionName);
if (!stats) {
stats = {
functionName,
successfulExecutions: 0,
failedExecutions: 0,
failedExecutionsArgs: []
};
this.stats.set(functionName, stats);
}

if (success) {
stats.successfulExecutions++;
} else {
stats.failedExecutions++;
stats.failedExecutionsArgs.push( args );
}
return executionResult;
}

async getStats() {
return this.stats;
}

printStatistics(): void {
logger.info("Execution Statistics:");
this.stats.forEach((stat) => {
logger.info(
`Function: ${stat.functionName}, Successful Executions: ${stat.successfulExecutions}, Failed Executions: ${stat.failedExecutions}`
);
});
}

printFailedExecutions(): void {
logger.info("Failed Executions:");
this.stats.forEach((execution) => {
if (execution.failedExecutionsArgs.length > 0) {
logger.info(` Function: ${execution.functionName}`);
execution.failedExecutionsArgs.forEach(args =>
logger.info(` args: ${JSON.stringify(args)}`));
}
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,8 @@ app.post("/", async (req: Request, res: Response) => {
res.status(400).send("Username and password for nextcloud are required");
}
try {
await nextcloud.migrate(params.username, params.password, params.dir);
res.status(200).send("Sync DONE ✅");
const stats = await nextcloud.migrate(params.username, params.password, params.dir);
res.status(200).send(JSON.stringify(stats));
} catch (e) {
console.error(e)
res.status(500).send("Error during synchronization:: " + e.message)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { TwakeDriveClient, TwakeDriveUser } from './twake_client';
import path from 'path';
import { logger } from "./logger"
import { User, UserProvider, UserProviderFactory, UserProviderType } from "./user_privider";
import { FunctionExecutor } from "./executor";

export interface NextcloudMigrationConfiguration {
shell: {
Expand Down Expand Up @@ -36,6 +37,8 @@ export class NextcloudMigration {

driveClient: TwakeDriveClient;

executor = new FunctionExecutor();

constructor(config: NextcloudMigrationConfiguration) {
this.config = config;
this.userProvider = (new UserProviderFactory()).get(config.userProvider, config[config.userProvider]);
Expand All @@ -55,6 +58,9 @@ export class NextcloudMigration {
if(!dir) await this.download(username, password, dirTmp);
//upload files to the Twake Drive
await this.upload(driveUser, dirTmp);
this.executor.printStatistics();
this.executor.printFailedExecutions();
return this.executor.getStats();
} catch (e) {
console.error('Error downloading files from next cloud', e);
throw e;
Expand Down Expand Up @@ -169,19 +175,22 @@ export class NextcloudMigration {
}
}


//upload all files
logger.debug(`UPLOAD FILES FOR ${sourceDirPath}`)
for (const file of filesToUpload) {
logger.debug(`Upload file ${file}`)
await this.driveClient.createFile(file, parentDirId);
await this.executor.executeWithRetries(this.driveClient.createFile.bind(this.driveClient), [file, parentDirId], 3)
// await this.driveClient.createFile(file, parentDirId);
}

logger.debug(`UPLOAD DIRS FOR ${sourceDirPath}`)
for (const [name, path] of dirsToUpload) {
logger.info(`Create directory ${name}`);
const dir = await this.driveClient.createDirectory(name, parentDirId)
await this.upload(user, path, dir.id);
const dir = await this.executor.executeWithRetries(this.driveClient.createDirectory.bind(this.driveClient), [name, parentDirId], 3)
// const dir = await this.driveClient.createDirectory(name, parentDirId)
if (dir) {
await this.upload(user, path, dir.id);
}
}
}

Expand Down
44 changes: 44 additions & 0 deletions tdrive/backend/utils/nextcloud-migration/test/executor.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { describe, expect, test } from "@jest/globals";
import { FunctionExecutor } from "../src/executor";

//FOR LOCAL DEBUG PURPOSE ONLY, ITS NOT A TEST
describe('Function Executor tests', () => {

const subj = new FunctionExecutor();

test('Should successfully execute mock function', async () => {
//when
await subj.executeWithRetries(successFunction, [1, 2], 3);
await subj.executeWithRetries(successFunction, [4, 5], 3);

//then
let stats = await subj.getStats();
expect(stats?.get("successFunction")?.successfulExecutions).toEqual(2);
});

test('Should successfully gather arguments of the failed executions', async () => {
await subj.executeWithRetries(failedFunction, [1, 2], 3);

//then
let stats = await subj.getStats();
expect(stats?.get("failedFunction")?.failedExecutions).toBe(1);
expect(stats?.get("failedFunction")?.failedExecutionsArgs[0]).toStrictEqual([1, 2]);
});

test('Should successfully gather arguments of the class member', async () => {
await subj.executeWithRetries(subj.getStats.bind(this), [1, 2], 3);

//then
let stats = await subj.getStats();
expect(stats?.get("bound getStats")?.successfulExecutions).toEqual(1);
});

const successFunction = async (a: number, b: number) => {
return a + b;
};

const failedFunction = async () => {
throw new Error();
};

});

0 comments on commit 768dadc

Please sign in to comment.