App uninstall lambda, layers cleanup (#21749)
# Introduction On a logic function deletion also remove the driver entry On a app uninstall also remove the sdk layer ( keeps the dep one as it can be shared across several lambda ) | Resource | Scope | Before this PR | After | |---|---|---|---| | DB metadata (functions, objects, fields…) | per-app | deleted | deleted | | Source folder (`FileFolder.Source`) | per-function | deleted | deleted | | Built handler file (`FileFolder.BuiltLogicFunction`) | per-function | deleted | deleted | | **Lambda function** | per-function | **leaked** | **deleted** (driver `delete`) | | **SDK layer** `sdk-<wsId>-<appId>` (all versions) | per-app | **leaked** | **deleted** (driver `deleteApplicationResources` → `deleteSdkLayer`) | | Deps layer `deps-<checksum>` | shared across apps/workspaces | not deleted | **intentionally not deleted** (content-addressed, GC'd) | ## What I don't like about all that Right now there's some non reversible side effect inside the workspace migration transaction - If the transaction fails we're facing data loss - It also slows down everything I'm about to create a new PR that allow population post transaction commit side effect / cleanup to be run later <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21749?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. -->
This commit is contained in:
+4
@@ -14,6 +14,10 @@ export class DisabledDriver implements LogicFunctionDriver {
|
||||
// No-op when disabled
|
||||
}
|
||||
|
||||
async deleteApplicationResources(): Promise<void> {
|
||||
// No-op when disabled
|
||||
}
|
||||
|
||||
async execute(): Promise<LogicFunctionExecuteResult> {
|
||||
throw new LogicFunctionException(
|
||||
'Logic function execution is disabled. Set LOGIC_FUNCTION_TYPE to LOCAL or LAMBDA to enable.',
|
||||
|
||||
+13
@@ -95,6 +95,19 @@ export class LambdaDriver implements LogicFunctionDriver {
|
||||
await this.executorManager.delete(flatLogicFunction);
|
||||
}
|
||||
|
||||
async deleteApplicationResources({
|
||||
workspaceId,
|
||||
applicationUniversalIdentifier,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
applicationUniversalIdentifier: string;
|
||||
}): Promise<void> {
|
||||
await this.layerManager.deleteSdkLayer({
|
||||
workspaceId,
|
||||
applicationUniversalIdentifier,
|
||||
});
|
||||
}
|
||||
|
||||
async installPrebuiltBundle(
|
||||
params: LogicFunctionInstallPrebuiltBundleParams,
|
||||
): Promise<void> {
|
||||
|
||||
+55
-16
@@ -5,14 +5,16 @@ import {
|
||||
type GetFunctionCommandOutput,
|
||||
ListLayerVersionsCommand,
|
||||
PublishLayerVersionCommand,
|
||||
ResourceNotFoundException,
|
||||
} from '@aws-sdk/client-lambda';
|
||||
import { Logger } from '@nestjs/common';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
|
||||
import { SDK_LAYER_PREFIX_IN_ZIP } from 'src/engine/core-modules/logic-function/logic-function-drivers/drivers/lambda/constants/lambda-driver.constant';
|
||||
import { type LambdaDriverOptions } from 'src/engine/core-modules/logic-function/logic-function-drivers/drivers/lambda/types/lambda-driver.type';
|
||||
import { type LambdaAwsClientService } from 'src/engine/core-modules/logic-function/logic-function-drivers/drivers/lambda/services/lambda-aws-client.service';
|
||||
import { type LambdaToolFunctionsService } from 'src/engine/core-modules/logic-function/logic-function-drivers/drivers/lambda/services/lambda-tool-functions.service';
|
||||
import { type LambdaDriverOptions } from 'src/engine/core-modules/logic-function/logic-function-drivers/drivers/lambda/types/lambda-driver.type';
|
||||
import { getLambdaDepsLayerName } from 'src/engine/core-modules/logic-function/logic-function-drivers/drivers/lambda/utils/get-lambda-deps-layer-name.util';
|
||||
import { getLambdaSdkLayerName } from 'src/engine/core-modules/logic-function/logic-function-drivers/drivers/lambda/utils/get-lambda-sdk-layer-name.util';
|
||||
import { reprefixLambdaZipEntries } from 'src/engine/core-modules/logic-function/logic-function-drivers/drivers/lambda/utils/reprefix-lambda-zip-entries.util';
|
||||
@@ -27,6 +29,8 @@ type LayerAppContext = {
|
||||
};
|
||||
|
||||
export class LambdaLayerManagerService {
|
||||
private readonly logger = new Logger(LambdaLayerManagerService.name);
|
||||
|
||||
constructor(
|
||||
private readonly options: Pick<LambdaDriverOptions, 'layerBucket'>,
|
||||
private readonly awsClient: LambdaAwsClientService,
|
||||
@@ -96,6 +100,21 @@ export class LambdaLayerManagerService {
|
||||
return arn;
|
||||
}
|
||||
|
||||
async deleteSdkLayer({
|
||||
workspaceId,
|
||||
applicationUniversalIdentifier,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
applicationUniversalIdentifier: string;
|
||||
}): Promise<void> {
|
||||
const layerName = getLambdaSdkLayerName({
|
||||
workspaceId,
|
||||
applicationUniversalIdentifier,
|
||||
});
|
||||
|
||||
await this.deleteAllLayerVersions(layerName);
|
||||
}
|
||||
|
||||
hasExpectedLayers({
|
||||
lambdaExecutor,
|
||||
flatApplication,
|
||||
@@ -226,25 +245,45 @@ export class LambdaLayerManagerService {
|
||||
let marker: string | undefined;
|
||||
|
||||
do {
|
||||
const listResult = await lambdaClient.send(
|
||||
new ListLayerVersionsCommand({
|
||||
LayerName: layerName,
|
||||
MaxItems: 50,
|
||||
Marker: marker,
|
||||
}),
|
||||
);
|
||||
let listResult;
|
||||
|
||||
try {
|
||||
listResult = await lambdaClient.send(
|
||||
new ListLayerVersionsCommand({
|
||||
LayerName: layerName,
|
||||
MaxItems: 50,
|
||||
Marker: marker,
|
||||
}),
|
||||
);
|
||||
} catch (error) {
|
||||
// Layer never existed or already fully removed. Idempotent.
|
||||
if (error instanceof ResourceNotFoundException) {
|
||||
return;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
const versions = listResult.LayerVersions ?? [];
|
||||
|
||||
await Promise.all(
|
||||
versions.map((version) =>
|
||||
lambdaClient.send(
|
||||
new DeleteLayerVersionCommand({
|
||||
LayerName: layerName,
|
||||
VersionNumber: version.Version,
|
||||
}),
|
||||
),
|
||||
),
|
||||
versions.map(async (version) => {
|
||||
try {
|
||||
await lambdaClient.send(
|
||||
new DeleteLayerVersionCommand({
|
||||
LayerName: layerName,
|
||||
VersionNumber: version.Version,
|
||||
}),
|
||||
);
|
||||
} catch (error) {
|
||||
// Already gone: another concurrent cleanup removed it. Idempotent.
|
||||
if (error instanceof ResourceNotFoundException) {
|
||||
return;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
marker = listResult.NextMarker;
|
||||
|
||||
+2
@@ -90,6 +90,8 @@ export class LocalDriver implements LogicFunctionDriver {
|
||||
|
||||
async delete(): Promise<void> {}
|
||||
|
||||
async deleteApplicationResources(): Promise<void> {}
|
||||
|
||||
async installPrebuiltBundle(
|
||||
params: LogicFunctionInstallPrebuiltBundleParams,
|
||||
): Promise<void> {
|
||||
|
||||
+8
@@ -43,8 +43,16 @@ export type LogicFunctionTranspileResult = {
|
||||
builtCode: string;
|
||||
};
|
||||
|
||||
export type LogicFunctionDeleteApplicationResourcesParams = {
|
||||
workspaceId: string;
|
||||
applicationUniversalIdentifier: string;
|
||||
};
|
||||
|
||||
export interface LogicFunctionDriver {
|
||||
delete(flatLogicFunction: FlatLogicFunction): Promise<void>;
|
||||
deleteApplicationResources(
|
||||
params: LogicFunctionDeleteApplicationResourcesParams,
|
||||
): Promise<void>;
|
||||
execute(
|
||||
params: LogicFunctionExecuteParams,
|
||||
): Promise<LogicFunctionExecuteResult>;
|
||||
|
||||
Reference in New Issue
Block a user