refactor(core): Implement soft-deletions for executions (#7092)

Based on #7065 | Story: https://linear.app/n8n/issue/PAY-771

n8n on filesystem mode marks binary data to delete on manual execution
deletion, on unsaved execution completion, and on every execution
pruning cycle. We later prune binary data in a separate cycle via these
marker files, based on the configured TTL. In the context of introducing
an S3 client to manage binary data, the filesystem mode's mark-and-prune
setup is too tightly coupled to the general binary data management
client interface.

This PR...
- Ensures the deletion of an execution causes the deletion of any binary
data associated to it. This does away with the need for binary data TTL
and simplifies the filesystem mode's mark-and-prune setup.
- Refactors all execution deletions (including pruning) to cause soft
deletions, hard-deletes soft-deleted executions based on the existing
pruning config, and adjusts execution endpoints to filter out
soft-deleted executions. This reduces DB load, and keeps binary data
around long enough for users to access it when building workflows with
unsaved executions.
- Moves all execution pruning work from an execution lifecycle hook to
`execution.repository.ts`. This keeps related logic in a single place.
- Removes all marking logic from the binary data manager. This
simplifies the interface that the S3 client will meet.
- Adds basic sanity-check tests to pruning logic and execution deletion.

Out of scope:

- Improving existing pruning logic.
- Improving existing execution repository logic.
- Adjusting dir structure for filesystem mode.

---------

Co-authored-by: कारतोफ्फेलस्क्रिप्ट™ <aditya@netroy.in>
This commit is contained in:
Iván Ovejero
2023-09-20 15:21:42 +02:00
committed by GitHub
parent 09a7cf0980
commit cd08c8e4c6
36 changed files with 411 additions and 253 deletions

View File

@@ -1,4 +1,3 @@
import glob from 'fast-glob';
import { createReadStream } from 'fs';
import fs from 'fs/promises';
import path from 'path';
@@ -10,32 +9,18 @@ import { jsonParse } from 'n8n-workflow';
import type { IBinaryDataConfig, IBinaryDataManager } from '../Interfaces';
import { FileNotFoundError } from '../errors';
const PREFIX_METAFILE = 'binarymeta';
const executionExtractionRegexp =
/^(\w+)(?:[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12})$/;
export class BinaryDataFileSystem implements IBinaryDataManager {
private storagePath: string;
private binaryDataTTL: number;
constructor(config: IBinaryDataConfig) {
this.storagePath = config.localStoragePath;
this.binaryDataTTL = config.binaryDataTTL;
}
async init(startPurger = false): Promise<void> {
if (startPurger) {
setInterval(async () => {
await this.deleteMarkedFiles();
}, this.binaryDataTTL * 30000);
}
async init() {
await this.assertFolder(this.storagePath);
await this.assertFolder(this.getBinaryDataMetaPath());
await this.deleteMarkedFiles();
}
async getFileSize(identifier: string): Promise<number> {
@@ -81,44 +66,6 @@ export class BinaryDataFileSystem implements IBinaryDataManager {
return this.resolveStoragePath(`${identifier}.metadata`);
}
async markDataForDeletionByExecutionId(executionId: string): Promise<void> {
const tt = new Date(new Date().getTime() + this.binaryDataTTL * 60000);
return fs.writeFile(
this.resolveStoragePath('meta', `${PREFIX_METAFILE}_${executionId}_${tt.valueOf()}`),
'',
);
}
async deleteMarkedFiles(): Promise<void> {
return this.deleteMarkedFilesByMeta(this.getBinaryDataMetaPath(), PREFIX_METAFILE);
}
private async deleteMarkedFilesByMeta(metaPath: string, filePrefix: string): Promise<void> {
const currentTimeValue = new Date().valueOf();
const metaFileNames = await glob(`${filePrefix}_*`, { cwd: metaPath });
const executionIds = metaFileNames
.map((f) => f.split('_') as [string, string, string])
.filter(([prefix, , ts]) => {
if (prefix !== filePrefix) return false;
const execTimestamp = parseInt(ts, 10);
return execTimestamp < currentTimeValue;
})
.map((e) => e[1]);
const filesToDelete = [];
const deletedIds = await this.deleteBinaryDataByExecutionIds(executionIds);
for (const executionId of deletedIds) {
filesToDelete.push(
...(await glob(`${filePrefix}_${executionId}_`, {
absolute: true,
cwd: metaPath,
})),
);
}
await Promise.all(filesToDelete.map(async (file) => fs.rm(file)));
}
async duplicateBinaryDataByIdentifier(binaryDataId: string, prefix: string): Promise<string> {
const newBinaryDataId = this.generateFileName(prefix);
@@ -160,10 +107,6 @@ export class BinaryDataFileSystem implements IBinaryDataManager {
return [prefix, uuid()].join('');
}
private getBinaryDataMetaPath() {
return path.join(this.storagePath, 'meta');
}
private async deleteFromLocalStorage(identifier: string) {
return fs.rm(this.getBinaryPath(identifier));
}

View File

@@ -156,22 +156,6 @@ export class BinaryDataManager {
throw new Error('Storage mode used to store binary data not available');
}
async markDataForDeletionByExecutionId(executionId: string): Promise<void> {
if (this.managers[this.binaryDataMode]) {
await this.managers[this.binaryDataMode].markDataForDeletionByExecutionId(executionId);
}
}
async markDataForDeletionByExecutionIds(executionIds: string[]): Promise<void> {
if (this.managers[this.binaryDataMode]) {
await Promise.all(
executionIds.map(async (id) =>
this.managers[this.binaryDataMode].markDataForDeletionByExecutionId(id),
),
);
}
}
async deleteBinaryDataByExecutionIds(executionIds: string[]): Promise<void> {
if (this.managers[this.binaryDataMode]) {
await this.managers[this.binaryDataMode].deleteBinaryDataByExecutionIds(executionIds);

View File

@@ -38,7 +38,6 @@ export interface IBinaryDataConfig {
mode: 'default' | 'filesystem';
availableModes: string;
localStoragePath: string;
binaryDataTTL: number;
}
export interface IBinaryDataManager {
@@ -51,8 +50,6 @@ export interface IBinaryDataManager {
retrieveBinaryDataByIdentifier(identifier: string): Promise<Buffer>;
getBinaryPath(identifier: string): string;
getBinaryStream(identifier: string, chunkSize?: number): Readable;
markDataForDeletionByExecutionId(executionId: string): Promise<void>;
deleteMarkedFiles(): Promise<unknown>;
deleteBinaryDataByIdentifier(identifier: string): Promise<void>;
duplicateBinaryDataByIdentifier(binaryDataId: string, prefix: string): Promise<string>;
deleteBinaryDataByExecutionIds(executionIds: string[]): Promise<string[]>;