Improve cleaning job (#17208)
# Introduction Refactored the workspace deletion to dynamically iterate over all known v2 syncable entities repos and delete all of them from child to parent Exception for field metadata that we chunk delete in order to avoid locking the core schema too long, it does not have an impact on perfs at all ( neither plus or less ) Chunking by constraint within a transaction is not necessary both does not cost more ## From 30s for a workspace complete deletion ```ts [Nest] 93244 - 01/16/2026, 10:24:52 PM LOG [WorkspaceService] workspace WS_ID cache flushed [Runner] Total execution: 26.290s // ( deleteAllObjectMetadatas v2 ) [Nest] 93244 - 01/16/2026, 10:25:22 PM LOG [WorkspaceService] workspace WS_ID hard deleted ``` ## To 3s ! ```ts [Nest] 65112 - 01/18/2026, 4:37:38 PM LOG [DatabaseConfigDriver] [INIT] Config variables loaded: 0 values found in DB, 69 falling to env vars/defaults [Nest] 65112 - 01/18/2026, 4:37:38 PM LOG [CleanSuspendedWorkspacesCommand] IGNORING GRACE PERIOD - Cleaning 1 suspended workspaces [Nest] 65112 - 01/18/2026, 4:37:38 PM LOG [CleanerWorkspaceService] batchWarnOrCleanSuspendedWorkspaces running... [Nest] 65112 - 01/18/2026, 4:37:38 PM LOG [CleanerWorkspaceService] Processing workspace - 1/1 [Nest] 65112 - 01/18/2026, 4:37:38 PM LOG [CleanerWorkspaceService] Destroying workspace Twenty Eng [Nest] 65112 - 01/18/2026, 4:37:38 PM LOG [WorkspaceService] workspace user workspaces deleted [Nest] 65112 - 01/18/2026, 4:37:38 PM LOG [WorkspaceService] workspace cache flushed [Nest] 65112 - 01/18/2026, 4:37:38 PM LOG [WorkspaceService] workspace: deleted 80 viewFilter record(s) [Nest] 65112 - 01/18/2026, 4:37:38 PM LOG [WorkspaceService] workspace: deleted 21 pageLayoutWidget record(s) [Nest] 65112 - 01/18/2026, 4:37:38 PM LOG [WorkspaceService] workspace: deleted 1515 viewField record(s) [Nest] 65112 - 01/18/2026, 4:37:38 PM LOG [WorkspaceService] workspace: deleted 91 index record(s) [Nest] 65112 - 01/18/2026, 4:37:38 PM LOG [WorkspaceService] workspace: deleted 66 roleTarget record(s) [Nest] 65112 - 01/18/2026, 4:37:38 PM LOG [WorkspaceService] workspace: deleted 174 viewGroup record(s) [Nest] 65112 - 01/18/2026, 4:37:38 PM LOG [WorkspaceService] workspace: deleted 1 agent record(s) [Nest] 65112 - 01/18/2026, 4:37:38 PM LOG [WorkspaceService] workspace: deleted 7 pageLayout record(s) [Nest] 65112 - 01/18/2026, 4:37:38 PM LOG [WorkspaceService] workspace: deleted 111 view record(s) [Nest] 65112 - 01/18/2026, 4:37:38 PM LOG [WorkspaceService] workspace: fieldMetadata chunk 1/15 - deleted 50 record(s) [Nest] 65112 - 01/18/2026, 4:37:38 PM LOG [WorkspaceService] workspace: fieldMetadata chunk 2/15 - deleted 50 record(s) [Nest] 65112 - 01/18/2026, 4:37:38 PM LOG [WorkspaceService] workspace: fieldMetadata chunk 3/15 - deleted 50 record(s) [Nest] 65112 - 01/18/2026, 4:37:38 PM LOG [WorkspaceService] workspace: fieldMetadata chunk 4/15 - deleted 50 record(s) [Nest] 65112 - 01/18/2026, 4:37:38 PM LOG [WorkspaceService] workspace: fieldMetadata chunk 5/15 - deleted 50 record(s) [Nest] 65112 - 01/18/2026, 4:37:38 PM LOG [WorkspaceService] workspace: fieldMetadata chunk 6/15 - deleted 50 record(s) [Nest] 65112 - 01/18/2026, 4:37:38 PM LOG [WorkspaceService] workspace: fieldMetadata chunk 7/15 - deleted 50 record(s) [Nest] 65112 - 01/18/2026, 4:37:38 PM LOG [WorkspaceService] workspace: fieldMetadata chunk 8/15 - deleted 50 record(s) [Nest] 65112 - 01/18/2026, 4:37:38 PM LOG [WorkspaceService] workspace: fieldMetadata chunk 9/15 - deleted 50 record(s) [Nest] 65112 - 01/18/2026, 4:37:38 PM LOG [WorkspaceService] workspace: fieldMetadata chunk 10/15 - deleted 50 record(s) [Nest] 65112 - 01/18/2026, 4:37:38 PM LOG [WorkspaceService] workspace: fieldMetadata chunk 11/15 - deleted 50 record(s) [Nest] 65112 - 01/18/2026, 4:37:38 PM LOG [WorkspaceService] workspace: fieldMetadata chunk 12/15 - deleted 51 record(s) [Nest] 65112 - 01/18/2026, 4:37:38 PM LOG [WorkspaceService] workspace: fieldMetadata chunk 13/15 - deleted 50 record(s) [Nest] 65112 - 01/18/2026, 4:37:38 PM LOG [WorkspaceService] workspace: fieldMetadata chunk 14/15 - deleted 50 record(s) [Nest] 65112 - 01/18/2026, 4:37:38 PM LOG [WorkspaceService] workspace: fieldMetadata chunk 15/15 - deleted 36 record(s) [Nest] 65112 - 01/18/2026, 4:37:38 PM LOG [WorkspaceService] workspace: deleted 737 fieldMetadata record(s) [Nest] 65112 - 01/18/2026, 4:37:38 PM LOG [WorkspaceService] workspace: deleted 6 role record(s) [Nest] 65112 - 01/18/2026, 4:37:38 PM LOG [WorkspaceService] workspace: deleted 78 serverlessFunction record(s) [Nest] 65112 - 01/18/2026, 4:37:39 PM LOG [WorkspaceService] workspace: deleted 43 objectMetadata record(s) [Nest] 65112 - 01/18/2026, 4:37:41 PM LOG [WorkspaceService] workspace hard deleted [Nest] 65112 - 01/18/2026, 4:37:41 PM LOG [CleanerWorkspaceService] Destroyed 1 workspaces on 5 limit durings this execution [Nest] 65112 - 01/18/2026, 4:37:41 PM LOG [CleanerWorkspaceService] batchWarnOrCleanSuspendedWorkspaces done! [Nest] 65112 - 01/18/2026, 4:37:41 PM LOG [CleanSuspendedWorkspacesCommand] Command completed! ``` ## Update Discussed with @charlesBochet ended debugging and analyzing sql query operations He discovered that we were not indexing foreignKey effectively We've ended up fixing all the FK indeces coverage leading to ## Cleaning Removed the ```sh npx nx run twenty-server:command workspace:clean-soft-deleted-suspended-workspaces --ignore-grace-period ``` In favor of ```sh npx nx run twenty-server:command workspace:clean --only-operation destroy --ignore-destroy-grace-period ``` ## Conclusion Not that crazy but still worth it and could demultiply in production
This commit is contained in:
-106
@@ -1,106 +0,0 @@
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Command, Option } from 'nest-commander';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
|
||||
import { In, IsNull, Not, Repository } from 'typeorm';
|
||||
|
||||
import {
|
||||
type MigrationCommandOptions,
|
||||
MigrationCommandRunner,
|
||||
} from 'src/database/commands/command-runners/migration.command-runner';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { CleanerWorkspaceService } from 'src/engine/workspace-manager/workspace-cleaner/services/cleaner.workspace-service';
|
||||
|
||||
type HardDeleteSoftDeletedSuspendedWorkspacesCommandOptions =
|
||||
MigrationCommandOptions & {
|
||||
ignoreGracePeriod?: boolean;
|
||||
};
|
||||
|
||||
@Command({
|
||||
name: 'workspace:hard-delete-soft-deleted',
|
||||
description: 'Hard delete soft-deleted suspended workspaces',
|
||||
})
|
||||
export class HardDeleteSoftDeletedSuspendedWorkspacesCommand extends MigrationCommandRunner {
|
||||
private workspaceIds: string[] = [];
|
||||
|
||||
constructor(
|
||||
private readonly cleanerWorkspaceService: CleanerWorkspaceService,
|
||||
@InjectRepository(WorkspaceEntity)
|
||||
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
@Option({
|
||||
flags: '-w, --workspace-id [workspace_id]',
|
||||
description:
|
||||
'workspace id. Command runs on all suspended workspaces if not provided',
|
||||
required: false,
|
||||
})
|
||||
parseWorkspaceId(val: string): string[] {
|
||||
this.workspaceIds.push(val);
|
||||
|
||||
return this.workspaceIds;
|
||||
}
|
||||
|
||||
@Option({
|
||||
flags: '--ignore-grace-period',
|
||||
description:
|
||||
'Ignore the grace period and hard delete soft-deleted workspaces immediately',
|
||||
required: false,
|
||||
})
|
||||
parseIgnoreGracePeriod(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
async fetchSuspendedSoftDeletedWorkspaces(): Promise<WorkspaceEntity[]> {
|
||||
return await this.workspaceRepository.find({
|
||||
where: {
|
||||
activationStatus: In([WorkspaceActivationStatus.SUSPENDED]),
|
||||
deletedAt: Not(IsNull()),
|
||||
...(this.workspaceIds.length > 0 ? { id: In(this.workspaceIds) } : {}),
|
||||
},
|
||||
withDeleted: true,
|
||||
});
|
||||
}
|
||||
|
||||
override async runMigrationCommand(
|
||||
_passedParams: string[],
|
||||
options: HardDeleteSoftDeletedSuspendedWorkspacesCommandOptions,
|
||||
): Promise<void> {
|
||||
const { dryRun, ignoreGracePeriod } = options;
|
||||
|
||||
const softDeletedSuspendedWorkspaces =
|
||||
await this.fetchSuspendedSoftDeletedWorkspaces();
|
||||
let deletedWorkspaceCounter = 0;
|
||||
|
||||
this.logger.log(
|
||||
`${dryRun ? 'DRY RUN - ' : ''}${ignoreGracePeriod ? 'IGNORING GRACE PERIOD - ' : ''}Iterating over ${softDeletedSuspendedWorkspaces.length} soft deleted suspended workspaces`,
|
||||
);
|
||||
|
||||
for (const workspace of softDeletedSuspendedWorkspaces) {
|
||||
try {
|
||||
const result =
|
||||
await this.cleanerWorkspaceService.hardDeleteSoftDeletedWorkspace({
|
||||
workspace,
|
||||
dryRun,
|
||||
ignoreGracePeriod,
|
||||
});
|
||||
|
||||
if (isDefined(result)) {
|
||||
deletedWorkspaceCounter++;
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to destroy soft deleted workspace ${workspace.id}`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Destroyed ${deletedWorkspaceCounter}/${softDeletedSuspendedWorkspaces.length}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
+43
-9
@@ -9,7 +9,17 @@ import {
|
||||
MigrationCommandRunner,
|
||||
} from 'src/database/commands/command-runners/migration.command-runner';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { CleanerWorkspaceService } from 'src/engine/workspace-manager/workspace-cleaner/services/cleaner.workspace-service';
|
||||
import {
|
||||
CLEAN_SUSPENDED_WORKSPACES_OPERATIONS,
|
||||
CleanerWorkspaceService,
|
||||
CleanSuspendedWorkspacesOperation,
|
||||
type CleanSuspendedWorkspacesOptions,
|
||||
} from 'src/engine/workspace-manager/workspace-cleaner/services/cleaner.workspace-service';
|
||||
|
||||
type CleanSuspendedWorkspacesCommandOptions = MigrationCommandOptions &
|
||||
Pick<CleanSuspendedWorkspacesOptions, 'ignoreDestroyGracePeriod'> & {
|
||||
onlyOperation?: CleanSuspendedWorkspacesOperation;
|
||||
};
|
||||
|
||||
@Command({
|
||||
name: 'workspace:clean',
|
||||
@@ -38,11 +48,32 @@ export class CleanSuspendedWorkspacesCommand extends MigrationCommandRunner {
|
||||
return this.workspaceIds;
|
||||
}
|
||||
|
||||
@Option({
|
||||
flags: '--ignore-destroy-grace-period',
|
||||
description:
|
||||
'Ignore the grace period and hard delete soft-deleted workspaces immediately',
|
||||
required: false,
|
||||
})
|
||||
parseIgnoreDestroyGracePeriod(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Option({
|
||||
flags: '--only-operation <operation>',
|
||||
description:
|
||||
'Run only a specific operation: warn, destroy (hard delete), or soft-delete',
|
||||
required: false,
|
||||
choices: [...CLEAN_SUSPENDED_WORKSPACES_OPERATIONS],
|
||||
})
|
||||
parseOnlyOperation(val: string): CleanSuspendedWorkspacesOperation {
|
||||
return val as CleanSuspendedWorkspacesOperation;
|
||||
}
|
||||
|
||||
async fetchSuspendedWorkspaceIds(): Promise<string[]> {
|
||||
const suspendedWorkspaces = await this.workspaceRepository.find({
|
||||
select: ['id'],
|
||||
where: {
|
||||
activationStatus: In([WorkspaceActivationStatus.SUSPENDED]),
|
||||
...(this.workspaceIds.length > 0 ? { id: In(this.workspaceIds) } : {}),
|
||||
},
|
||||
withDeleted: true,
|
||||
});
|
||||
@@ -52,22 +83,25 @@ export class CleanSuspendedWorkspacesCommand extends MigrationCommandRunner {
|
||||
|
||||
override async runMigrationCommand(
|
||||
_passedParams: string[],
|
||||
options: MigrationCommandOptions,
|
||||
options: CleanSuspendedWorkspacesCommandOptions,
|
||||
): Promise<void> {
|
||||
const { dryRun } = options;
|
||||
const { dryRun, ignoreDestroyGracePeriod, onlyOperation } = options;
|
||||
|
||||
const suspendedWorkspaceIds =
|
||||
this.workspaceIds.length > 0
|
||||
? this.workspaceIds
|
||||
: await this.fetchSuspendedWorkspaceIds();
|
||||
const suspendedWorkspaceIds = await this.fetchSuspendedWorkspaceIds();
|
||||
|
||||
const operationLabel = onlyOperation
|
||||
? `ONLY ${onlyOperation.toUpperCase()} - `
|
||||
: '';
|
||||
|
||||
this.logger.log(
|
||||
`${dryRun ? 'DRY RUN - ' : ''}Cleaning ${suspendedWorkspaceIds.length} suspended workspaces`,
|
||||
`${dryRun ? 'DRY RUN - ' : ''}${ignoreDestroyGracePeriod ? 'IGNORING GRACE PERIOD - ' : ''}${operationLabel}Cleaning ${suspendedWorkspaceIds.length} suspended workspaces`,
|
||||
);
|
||||
|
||||
await this.cleanerWorkspaceService.batchWarnOrCleanSuspendedWorkspaces({
|
||||
workspaceIds: suspendedWorkspaceIds,
|
||||
dryRun,
|
||||
ignoreDestroyGracePeriod,
|
||||
onlyOperation,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
-91
@@ -1,91 +0,0 @@
|
||||
import { Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Command, CommandRunner, Option } from 'nest-commander';
|
||||
import { In, Repository } from 'typeorm';
|
||||
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { getDryRunLogHeader } from 'src/utils/get-dry-run-log-header';
|
||||
|
||||
type DeleteWorkspacesCommandOptions = {
|
||||
dryRun?: boolean;
|
||||
workspaceIds: string[];
|
||||
};
|
||||
|
||||
@Command({
|
||||
name: 'workspace:delete',
|
||||
description: 'Delete workspace',
|
||||
})
|
||||
export class DeleteWorkspacesCommand extends CommandRunner {
|
||||
private readonly logger = new Logger(DeleteWorkspacesCommand.name);
|
||||
|
||||
constructor(
|
||||
@InjectRepository(WorkspaceEntity)
|
||||
private readonly workspaceRepository: Repository<WorkspaceEntity>,
|
||||
private readonly dataSourceService: DataSourceService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
@Option({
|
||||
flags: '-d, --dry-run [dry run]',
|
||||
description: 'Dry run: Log delete actions without executing them.',
|
||||
required: false,
|
||||
})
|
||||
dryRun(value: string): boolean {
|
||||
return Boolean(value);
|
||||
}
|
||||
|
||||
@Option({
|
||||
flags: '-w, --workspace-ids [workspace_ids]',
|
||||
description: 'comma separated workspace ids',
|
||||
required: true,
|
||||
})
|
||||
parseWorkspaceIds(value: string): string[] {
|
||||
return value.split(',');
|
||||
}
|
||||
|
||||
async run(
|
||||
_passedParam: string[],
|
||||
options: DeleteWorkspacesCommandOptions,
|
||||
): Promise<void> {
|
||||
const workspaces = await this.workspaceRepository.find({
|
||||
where: { id: In(options.workspaceIds) },
|
||||
});
|
||||
|
||||
const dataSources =
|
||||
await this.dataSourceService.getManyDataSourceMetadata();
|
||||
|
||||
const workspaceIdsWithSchema = dataSources.map(
|
||||
(dataSource) => dataSource.workspaceId,
|
||||
);
|
||||
|
||||
const workspacesToDelete = workspaces.filter((WorkspaceEntity) =>
|
||||
workspaceIdsWithSchema.includes(WorkspaceEntity.id),
|
||||
);
|
||||
|
||||
if (workspacesToDelete.length) {
|
||||
this.logger.log(
|
||||
`Running Deleting workspaces on ${workspacesToDelete.length} workspaces`,
|
||||
);
|
||||
}
|
||||
|
||||
for (const workspace of workspacesToDelete) {
|
||||
this.logger.log(
|
||||
`${getDryRunLogHeader(options.dryRun)}Deleting workspace ${
|
||||
workspace.id
|
||||
} name: '${workspace.displayName}'`,
|
||||
);
|
||||
// const workspaceServiceInstance =
|
||||
// await this.loadServiceWithWorkspaceContext.load(
|
||||
// this.workspaceService,
|
||||
// workspace.id,
|
||||
// );
|
||||
|
||||
// if (!options.dryRun) {
|
||||
// await workspaceServiceInstance.softDeleteWorkspace(workspace.id);
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
+61
-44
@@ -25,12 +25,23 @@ import { UserVarsService } from 'src/engine/core-modules/user/user-vars/services
|
||||
import { WorkspaceService } from 'src/engine/core-modules/workspace/services/workspace.service';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { USER_WORKSPACE_DELETION_WARNING_SENT_KEY } from 'src/engine/workspace-manager/workspace-cleaner/constants/user-workspace-deletion-warning-sent-key.constant';
|
||||
import {
|
||||
WorkspaceCleanerException,
|
||||
WorkspaceCleanerExceptionCode,
|
||||
} from 'src/engine/workspace-manager/workspace-cleaner/exceptions/workspace-cleaner.exception';
|
||||
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||
|
||||
export const CLEAN_SUSPENDED_WORKSPACES_OPERATIONS = [
|
||||
'warn',
|
||||
'destroy',
|
||||
'soft-delete',
|
||||
] as const;
|
||||
export type CleanSuspendedWorkspacesOperation =
|
||||
(typeof CLEAN_SUSPENDED_WORKSPACES_OPERATIONS)[number];
|
||||
|
||||
export type CleanSuspendedWorkspacesOptions = {
|
||||
workspaceIds: string[];
|
||||
dryRun?: boolean;
|
||||
ignoreDestroyGracePeriod?: boolean;
|
||||
onlyOperation?: CleanSuspendedWorkspacesOperation;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class CleanerWorkspaceService {
|
||||
private readonly logger = new Logger(CleanerWorkspaceService.name);
|
||||
@@ -68,39 +79,30 @@ export class CleanerWorkspaceService {
|
||||
);
|
||||
}
|
||||
|
||||
async computeDaysSinceSubscriptionUnpaidOrThrow(
|
||||
async computeDaysSinceSubscriptionUnpaid(
|
||||
workspace: WorkspaceEntity,
|
||||
): Promise<number> {
|
||||
try {
|
||||
const lastSubscription =
|
||||
await this.billingSubscriptionRepository.findOneOrFail({
|
||||
where: {
|
||||
workspaceId: workspace.id,
|
||||
},
|
||||
order: { updatedAt: 'DESC' },
|
||||
});
|
||||
): Promise<number | null> {
|
||||
const lastSubscription =
|
||||
await this.billingSubscriptionRepository.findOneOrFail({
|
||||
where: {
|
||||
workspaceId: workspace.id,
|
||||
},
|
||||
order: { updatedAt: 'DESC' },
|
||||
});
|
||||
|
||||
if (
|
||||
lastSubscription.status !== SubscriptionStatus.Unpaid &&
|
||||
lastSubscription.status !== SubscriptionStatus.Canceled
|
||||
) {
|
||||
throw new Error(
|
||||
'No cancelled or unpaid billing subscription found for workspace',
|
||||
);
|
||||
}
|
||||
|
||||
const daysSinceSubscriptionUnpaid = differenceInDays(
|
||||
new Date(),
|
||||
lastSubscription.currentPeriodStart,
|
||||
);
|
||||
|
||||
return daysSinceSubscriptionUnpaid;
|
||||
} catch {
|
||||
throw new WorkspaceCleanerException(
|
||||
`No cancelled or unpaid billing subscription found for workspace ${workspace.id} ${workspace.displayName}`,
|
||||
WorkspaceCleanerExceptionCode.BILLING_SUBSCRIPTION_NOT_FOUND,
|
||||
);
|
||||
if (
|
||||
lastSubscription.status !== SubscriptionStatus.Unpaid &&
|
||||
lastSubscription.status !== SubscriptionStatus.Canceled
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const daysSinceSubscriptionUnpaid = differenceInDays(
|
||||
new Date(),
|
||||
lastSubscription.currentPeriodStart,
|
||||
);
|
||||
|
||||
return daysSinceSubscriptionUnpaid;
|
||||
}
|
||||
|
||||
async checkIfAtLeastOneWorkspaceMemberWarned(
|
||||
@@ -329,7 +331,7 @@ export class CleanerWorkspaceService {
|
||||
}
|
||||
}
|
||||
|
||||
async hardDeleteSoftDeletedWorkspace({
|
||||
async destroySoftDeletedWorkspace({
|
||||
workspace,
|
||||
ignoreGracePeriod = false,
|
||||
dryRun = false,
|
||||
@@ -375,10 +377,9 @@ export class CleanerWorkspaceService {
|
||||
async batchWarnOrCleanSuspendedWorkspaces({
|
||||
workspaceIds,
|
||||
dryRun = false,
|
||||
}: {
|
||||
workspaceIds: string[];
|
||||
dryRun?: boolean;
|
||||
}): Promise<void> {
|
||||
ignoreDestroyGracePeriod = false,
|
||||
onlyOperation,
|
||||
}: CleanSuspendedWorkspacesOptions): Promise<void> {
|
||||
this.logger.log(
|
||||
`${dryRun ? 'DRY RUN - ' : ''}batchWarnOrCleanSuspendedWorkspaces running...`,
|
||||
);
|
||||
@@ -404,23 +405,37 @@ export class CleanerWorkspaceService {
|
||||
deletedWorkspacesCount <
|
||||
this.maxNumberOfWorkspacesDeletedPerExecution;
|
||||
|
||||
if (isSoftDeletedWorkspace && isWithinDeletionLimit) {
|
||||
const result = await this.hardDeleteSoftDeletedWorkspace({
|
||||
if (
|
||||
(!isDefined(onlyOperation) || onlyOperation === 'destroy') &&
|
||||
isSoftDeletedWorkspace &&
|
||||
isWithinDeletionLimit
|
||||
) {
|
||||
const result = await this.destroySoftDeletedWorkspace({
|
||||
workspace,
|
||||
dryRun,
|
||||
ignoreGracePeriod: false,
|
||||
ignoreGracePeriod: ignoreDestroyGracePeriod,
|
||||
});
|
||||
|
||||
if (isDefined(result)) {
|
||||
deletedWorkspacesCount++;
|
||||
this.logger.log(
|
||||
`Destroyed ${deletedWorkspacesCount} workspaces on ${this.maxNumberOfWorkspacesDeletedPerExecution} limit durings this execution`,
|
||||
);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const workspaceInactivity =
|
||||
await this.computeDaysSinceSubscriptionUnpaidOrThrow(workspace);
|
||||
await this.computeDaysSinceSubscriptionUnpaid(workspace);
|
||||
|
||||
if (workspaceInactivity > this.inactiveDaysBeforeSoftDelete) {
|
||||
if (workspaceInactivity === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
(!isDefined(onlyOperation) || onlyOperation === 'soft-delete') &&
|
||||
workspaceInactivity > this.inactiveDaysBeforeSoftDelete
|
||||
) {
|
||||
await this.informWorkspaceMembersAndSoftDeleteWorkspace(
|
||||
workspace,
|
||||
workspaceInactivity,
|
||||
@@ -429,7 +444,9 @@ export class CleanerWorkspaceService {
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
(!isDefined(onlyOperation) || onlyOperation === 'warn') &&
|
||||
workspaceInactivity > this.inactiveDaysBeforeWarn &&
|
||||
workspaceInactivity <= this.inactiveDaysBeforeSoftDelete
|
||||
) {
|
||||
|
||||
-4
@@ -13,10 +13,8 @@ import { WorkspaceModule } from 'src/engine/core-modules/workspace/workspace.mod
|
||||
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
|
||||
import { CleanOnboardingWorkspacesCommand } from 'src/engine/workspace-manager/workspace-cleaner/commands/clean-onboarding-workspaces.command';
|
||||
import { CleanOnboardingWorkspacesCronCommand } from 'src/engine/workspace-manager/workspace-cleaner/commands/clean-onboarding-workspaces.cron.command';
|
||||
import { HardDeleteSoftDeletedSuspendedWorkspacesCommand } from 'src/engine/workspace-manager/workspace-cleaner/commands/clean-soft-deleted-suspended-workspaces.command';
|
||||
import { CleanSuspendedWorkspacesCommand } from 'src/engine/workspace-manager/workspace-cleaner/commands/clean-suspended-workspaces.command';
|
||||
import { CleanSuspendedWorkspacesCronCommand } from 'src/engine/workspace-manager/workspace-cleaner/commands/clean-suspended-workspaces.cron.command';
|
||||
import { DeleteWorkspacesCommand } from 'src/engine/workspace-manager/workspace-cleaner/commands/delete-workspaces.command';
|
||||
import { DestroyWorkspaceCommand } from 'src/engine/workspace-manager/workspace-cleaner/commands/destroy-workspace.command';
|
||||
import { CleanerWorkspaceService } from 'src/engine/workspace-manager/workspace-cleaner/services/cleaner.workspace-service';
|
||||
|
||||
@@ -36,11 +34,9 @@ import { CleanerWorkspaceService } from 'src/engine/workspace-manager/workspace-
|
||||
MetricsModule,
|
||||
],
|
||||
providers: [
|
||||
DeleteWorkspacesCommand,
|
||||
DestroyWorkspaceCommand,
|
||||
CleanSuspendedWorkspacesCronCommand,
|
||||
CleanSuspendedWorkspacesCommand,
|
||||
HardDeleteSoftDeletedSuspendedWorkspacesCommand,
|
||||
CleanOnboardingWorkspacesCommand,
|
||||
CleanOnboardingWorkspacesCronCommand,
|
||||
CleanerWorkspaceService,
|
||||
|
||||
Reference in New Issue
Block a user