feat(logic-function): add LIVE / PREBUILT execution modes (#20873)

## Summary

### Why

1. Sending the code to the lambda (~1Mb usually) is heavy on network and
results to a constant traffic of ~30Mb/s on AWS which results into TB of
network data every month
2. eval(1MB of code) is not that fast, it's heavy on memory and CPU on
lambda side

### High level

Adds two execution modes for logic functions, gated behind the new
`IS_LOGIC_FUNCTION_PREBUILT_MODE_ENABLED` workspace feature flag (off
everywhere by default):

- **LIVE** (current behavior, preserved bit-for-bit): the compiled
bundle is read from object storage and shipped in every Lambda invoke
payload. Used for fast iteration in the workflow editor / Settings test
runs.
- **PREBUILT** (new): the bundle is installed onto the per-function
Lambda alongside the unified executor, and invocations carry only `{
params, env, handlerName }` — saving JSON payload egress and warm-start
`import()` cost on every call.

### Key design choices

- **Unified Lambda handler** (`constants/executor/index.mjs`) dispatches
at runtime: `event.code` present ? LIVE (write to `/tmp`, dynamic
import) : `import('./prebuilt-logic-function.mjs')`. Both code paths
always coexist on the deployment package, so the same Lambda can serve
either mode without redeploying.
- **Install runs inside the `validateBuildAndRun` migration pipeline**,
not at execute time. `Create/UpdateLogicFunctionActionHandlerService`
calls `driver.installPrebuiltBundle` when `executionMode` flips
LIVE?PREBUILT or `checksum` changes while PREBUILT, gated on
`isBuildUpToDate=true` and a fresh checksum.
- **Strict execute, no reconciliation**:
`LogicFunctionExecutorService.execute` resolves `effectiveExecutionMode`
(caller override > feature flag > entity column). For PREBUILT it asks
the driver `getInstalledBundleChecksum` (Lambda `twenty:bundle-checksum`
tag for AWS, sidecar file locally) and throws
`LOGIC_FUNCTION_PREBUILT_BUNDLE_NOT_INSTALLED` on mismatch.
- **Feature flag gates every side effect**: with the flag off the
executor forces LIVE, the action-handler install hooks bail before AWS,
and workflow activation does not flip the mode. Rollback is just turning
the flag off.

### Lifecycle

- New workflow CODE step ? `LIVE`, no install.
- Workflow activated ? build + activation flips `executionMode=PREBUILT`
? action-handler installs the bundle + sets the Lambda tag.
- Draft from active version ? duplicated logic function reset to `LIVE`.
- App install ? manifest converter sets `PREBUILT`, create-action
handler installs.
- Test runs (`executeOneFromSource`, workflow editor) pass
`executionMode=LIVE` explicitly.

### Observability

`[lambda-timing]` log lines now include `effectiveExecutionMode` and
`payloadBytes`; the action handler logs `install_duration_ms` for each
install.

## Test plan

- [x] `npx nx typecheck twenty-server` ? passes
- [x] `npx oxlint --type-aware` on all changed files ? 0 warnings, 0
errors
- [x] `npx nx test twenty-server` ? 588 suites / 5009 tests pass (no
regressions vs main)
- [x] New unit suite `flat-logic-function-validator.service.spec.ts` ?
9/9
- [x] Existing
`workflow-version-step-operations.workspace-service.spec.ts` ? 8/8
(verified the new token-based DI avoids a circular-import regression)
- [x] Snapshot for
`ALL_UNIVERSAL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY` updated
to include `executionMode`
- [x] Integration suite `logic-function-execution.integration-spec.ts`
extended to assert `executionMode=LIVE` on newly-created functions and
continues to exercise the LIVE happy path
- [ ] Manual staging rollout: flip
`IS_LOGIC_FUNCTION_PREBUILT_MODE_ENABLED` per workspace, observe
`[lambda-timing]` `payloadBytes` drop + `install_duration_ms`, then ramp
in prod.
This commit is contained in:
Charles Bochet
2026-06-02 13:14:39 +02:00
committed by GitHub
parent 1ae00d6753
commit 58907b733c
44 changed files with 2212 additions and 951 deletions
@@ -4,6 +4,29 @@ import { createHash } from 'crypto';
export const handler = async (event) => {
const { code, params, env, handlerName } = event;
// oxlint-disable-next-line no-undef
const oldProcessEnv = { ...process.env };
try {
// oxlint-disable-next-line no-undef
process.env = { ...process.env, ...(env ?? {}) };
const mainFile = code
? await loadFromPayload(code)
: await import('./prebuilt-logic-function.mjs');
const handlerFn = handlerName
.split('.')
.reduce((obj, key) => obj[key], mainFile);
return await handlerFn(params);
} finally {
// oxlint-disable-next-line no-undef
process.env = oldProcessEnv;
}
};
const loadFromPayload = async (code) => {
// Use a content-hash filename so identical code is written once per
// warm container and Node's ESM module cache can short-circuit subsequent
// `import(mainPath)` calls. With random names, every warm invocation
@@ -23,31 +46,13 @@ export const handler = async (event) => {
const codeHash = createHash('sha256').update(code).digest('hex');
const mainPath = `/tmp/${codeHash}.mjs`;
// oxlint-disable-next-line no-undef
const oldProcessEnv = { ...process.env };
try {
try {
await fs.access(mainPath);
} catch {
await fs.writeFile(mainPath, code, 'utf8');
}
// oxlint-disable-next-line no-undef
process.env = { ...process.env, ...(env ?? {}) };
const mainFile = await import(mainPath);
const handlerFn = handlerName
.split('.')
.reduce((obj, key) => obj[key], mainFile);
return await handlerFn(params);
} finally {
// Intentionally NOT removing `mainPath` here so subsequent warm
// invocations hit Node's ESM cache (see comment above).
// oxlint-disable-next-line no-undef
process.env = oldProcessEnv;
await fs.access(mainPath);
} catch {
await fs.writeFile(mainPath, code, 'utf8');
}
// Intentionally NOT removing `mainPath` after import so subsequent warm
// invocations hit Node's ESM cache (see comment above).
return import(mainPath);
};
@@ -0,0 +1,2 @@
export const LOGIC_FUNCTION_DRIVER_FACTORY_TOKEN =
'LOGIC_FUNCTION_DRIVER_FACTORY';
@@ -27,4 +27,15 @@ export class DisabledDriver implements LogicFunctionDriver {
LogicFunctionExceptionCode.LOGIC_FUNCTION_DISABLED,
);
}
async installPrebuiltBundle(): Promise<void> {
throw new LogicFunctionException(
'Logic function prebuilt install is disabled. Set LOGIC_FUNCTION_TYPE to LOCAL or LAMBDA to enable.',
LogicFunctionExceptionCode.LOGIC_FUNCTION_DISABLED,
);
}
async getInstalledBundleChecksum(): Promise<string | null> {
return null;
}
}
@@ -18,6 +18,8 @@ import {
LogType,
PublishLayerVersionCommand,
ResourceNotFoundException,
TagResourceCommand,
UpdateFunctionCodeCommand,
UpdateFunctionConfigurationCommand,
waitUntilFunctionActiveV2,
waitUntilFunctionUpdatedV2,
@@ -32,6 +34,7 @@ import {
type LogicFunctionDriver,
type LogicFunctionExecuteParams,
type LogicFunctionExecuteResult,
type LogicFunctionInstallPrebuiltBundleParams,
type LogicFunctionTranspileParams,
type LogicFunctionTranspileResult,
} from 'src/engine/core-modules/logic-function/logic-function-drivers/interfaces/logic-function-driver.interface';
@@ -50,12 +53,16 @@ import { TemporaryDirManager } from 'src/engine/core-modules/logic-function/logi
import { type LogicFunctionResourceService } from 'src/engine/core-modules/logic-function/logic-function-resource/logic-function-resource.service';
import { type SdkClientArchiveService } from 'src/engine/core-modules/sdk-client/sdk-client-archive.service';
import { LogicFunctionExecutionStatus } from 'src/engine/metadata-modules/logic-function/dtos/logic-function-execution-result.dto';
import { LogicFunctionRuntime } from 'src/engine/metadata-modules/logic-function/logic-function.entity';
import {
LogicFunctionExecutionMode,
LogicFunctionRuntime,
} from 'src/engine/metadata-modules/logic-function/logic-function.entity';
import {
LogicFunctionException,
LogicFunctionExceptionCode,
} from 'src/engine/metadata-modules/logic-function/logic-function.exception';
import { type FlatLogicFunction } from 'src/engine/metadata-modules/logic-function/types/flat-logic-function.type';
import { isLogicFunctionReadyForPrebuiltInstall } from 'src/engine/metadata-modules/logic-function/utils/is-logic-function-ready-for-prebuilt-install.util';
enum LambdaExecutionPhase {
BUILD = 'build',
@@ -88,8 +95,16 @@ const BUILDER_HANDLER_PATH = resolve(
),
);
const LAMBDA_PREBUILT_BUNDLE_CHECKSUM_TAG = 'twenty:bundle-checksum';
const PREBUILT_BUNDLE_FILE_NAME = 'prebuilt-logic-function.mjs';
const PREBUILT_INSTALL_LOCK_TTL_MS = 180_000;
const PREBUILT_INSTALL_LOCK_RETRY_MS = 1_000;
const PREBUILT_INSTALL_LOCK_MAX_RETRIES = 180;
type LambdaDriverExecutorPayload = {
code: string;
code?: string;
params: object;
env: Record<string, string>;
handlerName: string;
@@ -1164,7 +1179,20 @@ export class LambdaDriver implements LogicFunctionDriver {
payload,
env,
timeoutMs = 900_000,
forceExecutionMode,
}: LogicFunctionExecuteParams): Promise<LogicFunctionExecuteResult> {
const executionMode = forceExecutionMode ?? flatLogicFunction.executionMode;
if (
executionMode === LogicFunctionExecutionMode.PREBUILT &&
!isLogicFunctionReadyForPrebuiltInstall(flatLogicFunction)
) {
throw new LogicFunctionException(
`Cannot run logic function '${flatLogicFunction.id}' in PREBUILT mode: bundle is not installed`,
LogicFunctionExceptionCode.LOGIC_FUNCTION_PREBUILT_BUNDLE_NOT_INSTALLED,
);
}
let currentPhase: LambdaExecutionPhase = LambdaExecutionPhase.BUILD;
let buildExecutorMs = 0;
let getBuiltCodeMs = 0;
@@ -1180,26 +1208,28 @@ export class LambdaDriver implements LogicFunctionDriver {
buildExecutorMs = Date.now() - buildStart;
currentPhase = LambdaExecutionPhase.FETCH_CODE;
const fetchStart = Date.now();
const compiledCode = await this.logicFunctionResourceService.getBuiltCode(
{
workspaceId: flatLogicFunction.workspaceId,
applicationUniversalIdentifier,
builtHandlerPath: flatLogicFunction.builtHandlerPath,
},
);
getBuiltCodeMs = Date.now() - fetchStart;
currentPhase = LambdaExecutionPhase.INVOKE;
const invokeFlowStart = Date.now();
const executorPayload: LambdaDriverExecutorPayload = {
params: payload,
code: compiledCode,
env: env ?? {},
handlerName: flatLogicFunction.handlerName,
};
if (executionMode === LogicFunctionExecutionMode.LIVE) {
const fetchStart = Date.now();
executorPayload.code =
await this.logicFunctionResourceService.getBuiltCode({
workspaceId: flatLogicFunction.workspaceId,
applicationUniversalIdentifier,
builtHandlerPath: flatLogicFunction.builtHandlerPath,
});
getBuiltCodeMs = Date.now() - fetchStart;
}
currentPhase = LambdaExecutionPhase.INVOKE;
const payloadString = JSON.stringify(executorPayload);
const params: InvokeCommandInput = {
FunctionName: flatLogicFunction.id,
@@ -1228,10 +1258,10 @@ export class LambdaDriver implements LogicFunctionDriver {
coldStart,
} = this.parseLambdaLogResult(result.LogResult);
const duration = Date.now() - fetchStart;
const duration = Date.now() - invokeFlowStart;
this.logger.log(
`[lambda-timing] fnId=${flatLogicFunction.id} totalMs=${Date.now() - buildStart} buildExecutorMs=${buildExecutorMs} getBuiltCodeMs=${getBuiltCodeMs} payloadBytes=${Buffer.byteLength(payloadString, 'utf8')} invokeSendMs=${invokeSendMs} reportDurationMs=${reportDurationMs ?? 'n/a'} billedMs=${billedDurationMs ?? 'n/a'} initDurationMs=${initDurationMs ?? 'n/a'} coldStart=${coldStart}`,
`[lambda-timing] fnId=${flatLogicFunction.id} executionMode=${executionMode} totalMs=${Date.now() - buildStart} buildExecutorMs=${buildExecutorMs} getBuiltCodeMs=${getBuiltCodeMs} payloadBytes=${Buffer.byteLength(payloadString, 'utf8')} invokeSendMs=${invokeSendMs} reportDurationMs=${reportDurationMs ?? 'n/a'} billedMs=${billedDurationMs ?? 'n/a'} initDurationMs=${initDurationMs ?? 'n/a'} coldStart=${coldStart}`,
);
if (result.FunctionError) {
@@ -1287,4 +1317,109 @@ export class LambdaDriver implements LogicFunctionDriver {
);
}
}
private getPrebuiltInstallLockKey(flatLogicFunction: FlatLogicFunction) {
return `lambda-install:${flatLogicFunction.id}`;
}
async installPrebuiltBundle({
flatLogicFunction,
flatApplication,
applicationUniversalIdentifier,
}: LogicFunctionInstallPrebuiltBundleParams): Promise<void> {
if (!isNonEmptyString(flatLogicFunction.checksum)) {
throw new LogicFunctionException(
`Cannot install prebuilt bundle for function '${flatLogicFunction.id}' without a checksum`,
LogicFunctionExceptionCode.LOGIC_FUNCTION_PREBUILT_BUNDLE_NOT_INSTALLED,
);
}
const checksum = flatLogicFunction.checksum;
await this.buildLambdaExecutor({
flatLogicFunction,
flatApplication,
applicationUniversalIdentifier,
});
await this.cacheLockService.withLock(
async () => {
const compiledCode =
await this.logicFunctionResourceService.getBuiltCode({
workspaceId: flatLogicFunction.workspaceId,
applicationUniversalIdentifier,
builtHandlerPath: flatLogicFunction.builtHandlerPath,
});
const temporaryDirManager = new TemporaryDirManager();
const { sourceTemporaryDir, lambdaZipPath } =
await temporaryDirManager.init();
try {
await copyExecutor(sourceTemporaryDir);
await fs.writeFile(
join(sourceTemporaryDir, PREBUILT_BUNDLE_FILE_NAME),
compiledCode,
'utf8',
);
await createZipFile(sourceTemporaryDir, lambdaZipPath);
const lambdaClient = await this.getLambdaClient();
const updateResult = await lambdaClient.send(
new UpdateFunctionCodeCommand({
FunctionName: flatLogicFunction.id,
ZipFile: await fs.readFile(lambdaZipPath),
}),
);
await this.waitFunctionUpdated(flatLogicFunction.id);
const functionArn = updateResult.FunctionArn;
if (!isNonEmptyString(functionArn)) {
throw new LogicFunctionException(
`UpdateFunctionCode did not return a FunctionArn for '${flatLogicFunction.id}'`,
LogicFunctionExceptionCode.LOGIC_FUNCTION_PREBUILT_BUNDLE_NOT_INSTALLED,
);
}
await lambdaClient.send(
new TagResourceCommand({
Resource: functionArn,
Tags: {
[LAMBDA_PREBUILT_BUNDLE_CHECKSUM_TAG]: checksum,
},
}),
);
} catch (error) {
this.logger.error(
`Failed to install prebuilt bundle for function ${flatLogicFunction.id}: ${error instanceof Error ? error.message : String(error)}`,
error instanceof Error ? error.stack : undefined,
);
throw error;
} finally {
await temporaryDirManager.clean();
}
},
this.getPrebuiltInstallLockKey(flatLogicFunction),
{
ttl: PREBUILT_INSTALL_LOCK_TTL_MS,
ms: PREBUILT_INSTALL_LOCK_RETRY_MS,
maxRetries: PREBUILT_INSTALL_LOCK_MAX_RETRIES,
},
);
}
async getInstalledBundleChecksum(
flatLogicFunction: FlatLogicFunction,
): Promise<string | null> {
const lambdaExecutor = await this.getLambdaExecutor(flatLogicFunction);
if (!isDefined(lambdaExecutor)) {
return null;
}
return lambdaExecutor.Tags?.[LAMBDA_PREBUILT_BUNDLE_CHECKSUM_TAG] ?? null;
}
}
@@ -9,10 +9,12 @@ import {
type LogicFunctionExecuteParams,
type LogicFunctionExecuteResult,
type LogicFunctionDriver,
type LogicFunctionInstallPrebuiltBundleParams,
type LogicFunctionTranspileParams,
type LogicFunctionTranspileResult,
} from 'src/engine/core-modules/logic-function/logic-function-drivers/interfaces/logic-function-driver.interface';
import { isNonEmptyString } from '@sniptt/guards';
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
import { type CacheLockService } from 'src/engine/core-modules/cache-lock/cache-lock.service';
import { LOGIC_FUNCTION_EXECUTOR_TMPDIR_FOLDER } from 'src/engine/core-modules/logic-function/logic-function-drivers/constants/logic-function-executor-tmpdir-folder';
@@ -20,6 +22,13 @@ import { ConsoleListener } from 'src/engine/core-modules/logic-function/logic-fu
import { TemporaryDirManager } from 'src/engine/core-modules/logic-function/logic-function-drivers/utils/temporary-dir-manager';
import { HANDLER_NAME_REGEX } from 'src/engine/metadata-modules/logic-function/constants/handler.contant';
import { LogicFunctionExecutionStatus } from 'src/engine/metadata-modules/logic-function/dtos/logic-function-execution-result.dto';
import { LogicFunctionExecutionMode } from 'src/engine/metadata-modules/logic-function/logic-function.entity';
import {
LogicFunctionException,
LogicFunctionExceptionCode,
} from 'src/engine/metadata-modules/logic-function/logic-function.exception';
import { type FlatLogicFunction } from 'src/engine/metadata-modules/logic-function/types/flat-logic-function.type';
import { isLogicFunctionReadyForPrebuiltInstall } from 'src/engine/metadata-modules/logic-function/utils/is-logic-function-ready-for-prebuilt-install.util';
import { copyYarnEngineAndBuildDependencies } from 'src/engine/core-modules/application/application-package/utils/copy-yarn-engine-and-build-dependencies';
import type { LogicFunctionResourceService } from 'src/engine/core-modules/logic-function/logic-function-resource/logic-function-resource.service';
import type { SdkClientArchiveService } from 'src/engine/core-modules/sdk-client/sdk-client-archive.service';
@@ -30,6 +39,13 @@ const LAYER_BUILD_LOCK_RETRY_MS = 500;
const LAYER_BUILD_LOCK_MAX_RETRIES = 240;
const LAYER_BUILD_READY_SENTINEL = '.twenty-layer-ready';
const PREBUILT_BUNDLE_FILE_NAME = 'prebuilt-logic-function.mjs';
const PREBUILT_CHECKSUM_FILE_NAME = 'prebuilt-bundle.checksum';
const PREBUILT_INSTALL_LOCK_TTL_MS = 180_000;
const PREBUILT_INSTALL_LOCK_RETRY_MS = 1_000;
const PREBUILT_INSTALL_LOCK_MAX_RETRIES = 180;
export interface LocalDriverOptions {
logicFunctionResourceService: LogicFunctionResourceService;
sdkClientArchiveService: SdkClientArchiveService;
@@ -283,7 +299,20 @@ export class LocalDriver implements LogicFunctionDriver {
payload,
env,
timeoutMs = 900_000,
forceExecutionMode,
}: LogicFunctionExecuteParams): Promise<LogicFunctionExecuteResult> {
const executionMode = forceExecutionMode ?? flatLogicFunction.executionMode;
if (
executionMode === LogicFunctionExecutionMode.PREBUILT &&
!isLogicFunctionReadyForPrebuiltInstall(flatLogicFunction)
) {
throw new LogicFunctionException(
`Cannot run logic function '${flatLogicFunction.id}' in PREBUILT mode: bundle is not installed`,
LogicFunctionExceptionCode.LOGIC_FUNCTION_PREBUILT_BUNDLE_NOT_INSTALLED,
);
}
await this.createLayerIfNotExist({
flatApplication,
applicationUniversalIdentifier,
@@ -300,20 +329,25 @@ export class LocalDriver implements LogicFunctionDriver {
try {
const { sourceTemporaryDir } = await temporaryDirManager.init();
const inMemoryBuiltHandlerPath =
await this.logicFunctionResourceService.copyBuiltCodeInMemory({
workspaceId: flatLogicFunction.workspaceId,
applicationUniversalIdentifier,
builtHandlerPath: flatLogicFunction.builtHandlerPath,
inMemoryDestinationPath: sourceTemporaryDir,
});
await this.assembleNodeModules({
sourceTemporaryDir,
flatApplication,
applicationUniversalIdentifier,
});
const inMemoryBuiltHandlerPath =
executionMode === LogicFunctionExecutionMode.PREBUILT
? await this.copyPrebuiltBundleIntoExecutionDir({
flatLogicFunction,
sourceTemporaryDir,
})
: await this.logicFunctionResourceService.copyBuiltCodeInMemory({
workspaceId: flatLogicFunction.workspaceId,
applicationUniversalIdentifier,
builtHandlerPath: flatLogicFunction.builtHandlerPath,
inMemoryDestinationPath: sourceTemporaryDir,
});
let logs = '';
const consoleListener = new ConsoleListener();
@@ -561,4 +595,114 @@ export class LocalDriver implements LogicFunctionDriver {
child.on('close', () => clearTimeout(t));
});
}
private getPrebuiltBundleDir(flatLogicFunction: FlatLogicFunction): string {
return join(
LOGIC_FUNCTION_EXECUTOR_TMPDIR_FOLDER,
'prebuilt',
flatLogicFunction.id,
);
}
private getInstalledBundlePath(flatLogicFunction: FlatLogicFunction): string {
return join(
this.getPrebuiltBundleDir(flatLogicFunction),
PREBUILT_BUNDLE_FILE_NAME,
);
}
private async copyPrebuiltBundleIntoExecutionDir({
flatLogicFunction,
sourceTemporaryDir,
}: {
flatLogicFunction: FlatLogicFunction;
sourceTemporaryDir: string;
}): Promise<string> {
const installedBundlePath = this.getInstalledBundlePath(flatLogicFunction);
const localBundlePath = join(sourceTemporaryDir, PREBUILT_BUNDLE_FILE_NAME);
await fs.copyFile(installedBundlePath, localBundlePath);
return localBundlePath;
}
private getInstalledChecksumPath(
flatLogicFunction: FlatLogicFunction,
): string {
return join(
this.getPrebuiltBundleDir(flatLogicFunction),
PREBUILT_CHECKSUM_FILE_NAME,
);
}
private getPrebuiltInstallLockKey(flatLogicFunction: FlatLogicFunction) {
return `local-install:${flatLogicFunction.id}`;
}
async installPrebuiltBundle({
flatLogicFunction,
applicationUniversalIdentifier,
}: LogicFunctionInstallPrebuiltBundleParams): Promise<void> {
if (!isNonEmptyString(flatLogicFunction.checksum)) {
throw new LogicFunctionException(
`Cannot install prebuilt bundle for function '${flatLogicFunction.id}' without a checksum`,
LogicFunctionExceptionCode.LOGIC_FUNCTION_PREBUILT_BUNDLE_NOT_INSTALLED,
);
}
const checksum = flatLogicFunction.checksum;
await this.cacheLockService.withLock(
async () => {
const prebuiltDir = this.getPrebuiltBundleDir(flatLogicFunction);
await fs.mkdir(prebuiltDir, { recursive: true });
await this.logicFunctionResourceService.copyBuiltCodeInMemory({
workspaceId: flatLogicFunction.workspaceId,
applicationUniversalIdentifier,
builtHandlerPath: flatLogicFunction.builtHandlerPath,
inMemoryDestinationPath: prebuiltDir,
});
const downloadedPath = join(
prebuiltDir,
flatLogicFunction.builtHandlerPath,
);
const targetPath = this.getInstalledBundlePath(flatLogicFunction);
if (downloadedPath !== targetPath) {
await fs.mkdir(dirname(targetPath), { recursive: true });
await fs.rename(downloadedPath, targetPath);
}
await fs.writeFile(
this.getInstalledChecksumPath(flatLogicFunction),
checksum,
'utf8',
);
},
this.getPrebuiltInstallLockKey(flatLogicFunction),
{
ttl: PREBUILT_INSTALL_LOCK_TTL_MS,
ms: PREBUILT_INSTALL_LOCK_RETRY_MS,
maxRetries: PREBUILT_INSTALL_LOCK_MAX_RETRIES,
},
);
}
async getInstalledBundleChecksum(
flatLogicFunction: FlatLogicFunction,
): Promise<string | null> {
try {
const checksum = await fs.readFile(
this.getInstalledChecksumPath(flatLogicFunction),
'utf8',
);
return checksum.trim() || null;
} catch {
return null;
}
}
}
@@ -1,5 +1,6 @@
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
import { type LogicFunctionExecutionStatus } from 'src/engine/metadata-modules/logic-function/dtos/logic-function-execution-result.dto';
import { type LogicFunctionExecutionMode } from 'src/engine/metadata-modules/logic-function/logic-function.entity';
import { type FlatLogicFunction } from 'src/engine/metadata-modules/logic-function/types/flat-logic-function.type';
export type LogicFunctionExecuteError = {
@@ -23,6 +24,13 @@ export type LogicFunctionExecuteParams = {
payload: object;
env?: Record<string, string>;
timeoutMs?: number;
forceExecutionMode?: LogicFunctionExecutionMode;
};
export type LogicFunctionInstallPrebuiltBundleParams = {
flatLogicFunction: FlatLogicFunction;
flatApplication: FlatApplication;
applicationUniversalIdentifier: string;
};
export type LogicFunctionTranspileParams = {
@@ -43,6 +51,12 @@ export interface LogicFunctionDriver {
transpile(
params: LogicFunctionTranspileParams,
): Promise<LogicFunctionTranspileResult>;
installPrebuiltBundle(
params: LogicFunctionInstallPrebuiltBundleParams,
): Promise<void>;
getInstalledBundleChecksum(
flatLogicFunction: FlatLogicFunction,
): Promise<string | null>;
}
export enum LogicFunctionDriverType {
@@ -5,6 +5,7 @@ import { ApplicationRegistrationVariableEntity } from 'src/engine/core-modules/a
import { AuditModule } from 'src/engine/core-modules/audit/audit.module';
import { TokenModule } from 'src/engine/core-modules/auth/token/token.module';
import { BillingModule } from 'src/engine/core-modules/billing/billing.module';
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
import { LogicFunctionExecutorService } from 'src/engine/core-modules/logic-function/logic-function-executor/logic-function-executor.service';
import { SecretEncryptionModule } from 'src/engine/core-modules/secret-encryption/secret-encryption.module';
import { ThrottlerModule } from 'src/engine/core-modules/throttler/throttler.module';
@@ -20,6 +21,7 @@ import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache
SubscriptionsModule,
WorkspaceCacheModule,
BillingModule,
FeatureFlagModule,
TypeOrmModule.forFeature([ApplicationRegistrationVariableEntity]),
],
providers: [LogicFunctionExecutorService],
@@ -6,6 +6,7 @@ import {
DEFAULT_API_URL_NAME,
DEFAULT_APP_ACCESS_TOKEN_NAME,
} from 'twenty-shared/application';
import { FeatureFlagKey } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { Not, Repository } from 'typeorm';
import { v4 } from 'uuid';
@@ -26,6 +27,7 @@ import { LOGIC_FUNCTION_EXECUTED_EVENT } from 'src/engine/core-modules/audit/uti
import { ApplicationTokenService } from 'src/engine/core-modules/auth/token/services/application-token.service';
import { BillingUsageService } from 'src/engine/core-modules/billing/services/billing-usage.service';
import { BillingService } from 'src/engine/core-modules/billing/services/billing.service';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { LogicFunctionDriverFactory } from 'src/engine/core-modules/logic-function/logic-function-drivers/logic-function-driver.factory';
import { buildEnvVar } from 'src/engine/core-modules/logic-function/logic-function-executor/utils/build-env-var';
import { SecretEncryptionService } from 'src/engine/core-modules/secret-encryption/secret-encryption.service';
@@ -37,6 +39,11 @@ import { UsageResourceType } from 'src/engine/core-modules/usage/enums/usage-res
import { UsageUnit } from 'src/engine/core-modules/usage/enums/usage-unit.enum';
import { type UsageEvent } from 'src/engine/core-modules/usage/types/usage-event.type';
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
import { LogicFunctionExecutionMode } from 'src/engine/metadata-modules/logic-function/logic-function.entity';
import {
LogicFunctionException,
LogicFunctionExceptionCode,
} from 'src/engine/metadata-modules/logic-function/logic-function.exception';
import { FlatLogicFunction } from 'src/engine/metadata-modules/logic-function/types/flat-logic-function.type';
import { SubscriptionChannel } from 'src/engine/subscriptions/enums/subscription-channel.enum';
import { SubscriptionService } from 'src/engine/subscriptions/subscription.service';
@@ -76,6 +83,7 @@ export class LogicFunctionExecutorService {
private readonly workspaceEventEmitter: WorkspaceEventEmitter,
private readonly billingService: BillingService,
private readonly billingUsageService: BillingUsageService,
private readonly featureFlagService: FeatureFlagService,
@InjectRepository(ApplicationRegistrationVariableEntity)
private readonly applicationRegistrationVariableRepository: Repository<ApplicationRegistrationVariableEntity>,
) {}
@@ -86,12 +94,14 @@ export class LogicFunctionExecutorService {
payload,
userId,
userWorkspaceId,
executionMode,
}: {
logicFunctionId: string;
workspaceId: string;
payload: object;
userId?: string;
userWorkspaceId?: string;
executionMode?: LogicFunctionExecutionMode;
}): Promise<LogicFunctionExecuteResult> {
await this.throttleExecution(workspaceId);
@@ -111,6 +121,19 @@ export class LogicFunctionExecutorService {
const driver = this.logicFunctionDriverFactory.getCurrentDriver();
const effectiveExecutionMode = await this.resolveEffectiveExecutionMode({
workspaceId,
flatLogicFunction,
callerOverride: executionMode,
});
if (effectiveExecutionMode === LogicFunctionExecutionMode.PREBUILT) {
await this.assertPrebuiltBundleInstalled({
driver,
flatLogicFunction,
});
}
let resultLogicFunction: LogicFunctionExecuteResult;
try {
@@ -121,13 +144,15 @@ export class LogicFunctionExecutorService {
payload,
env: envVariables,
timeoutMs: flatLogicFunction.timeoutSeconds * 1_000,
forceExecutionMode: effectiveExecutionMode,
});
} catch (error) {
this.logger.error(
`Logic function execution failed: ` +
`functionId=${logicFunctionId}, ` +
`workspaceId=${workspaceId}, ` +
`driver=${driver.constructor.name}: ` +
`driver=${driver.constructor.name}, ` +
`mode=${effectiveExecutionMode}: ` +
`${error instanceof Error ? error.message : String(error)}`,
error instanceof Error ? error.stack : undefined,
);
@@ -144,6 +169,52 @@ export class LogicFunctionExecutorService {
return resultLogicFunction;
}
private async resolveEffectiveExecutionMode({
workspaceId,
flatLogicFunction,
callerOverride,
}: {
workspaceId: string;
flatLogicFunction: FlatLogicFunction;
callerOverride?: LogicFunctionExecutionMode;
}): Promise<LogicFunctionExecutionMode> {
if (isDefined(callerOverride)) {
return callerOverride;
}
const isPrebuiltModeEnabled =
await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IS_LOGIC_FUNCTION_PREBUILT_MODE_ENABLED,
workspaceId,
);
if (!isPrebuiltModeEnabled) {
return LogicFunctionExecutionMode.LIVE;
}
return flatLogicFunction.executionMode ?? LogicFunctionExecutionMode.LIVE;
}
private async assertPrebuiltBundleInstalled({
driver,
flatLogicFunction,
}: {
driver: ReturnType<LogicFunctionDriverFactory['getCurrentDriver']>;
flatLogicFunction: FlatLogicFunction;
}): Promise<void> {
const installedChecksum =
await driver.getInstalledBundleChecksum(flatLogicFunction);
if (installedChecksum !== flatLogicFunction.checksum) {
throw new LogicFunctionException(
`Prebuilt bundle is not installed for function '${flatLogicFunction.id}' ` +
`(installed=${installedChecksum ?? 'none'}, expected=${flatLogicFunction.checksum ?? 'none'}). ` +
`Rebuild and try again.`,
LogicFunctionExceptionCode.LOGIC_FUNCTION_PREBUILT_BUNDLE_NOT_INSTALLED,
);
}
}
async transpile(
params: LogicFunctionTranspileParams,
): Promise<LogicFunctionTranspileResult> {
@@ -1,6 +1,7 @@
import { type DynamicModule, Global, Module } from '@nestjs/common';
import { CacheLockModule } from 'src/engine/core-modules/cache-lock/cache-lock.module';
import { LOGIC_FUNCTION_DRIVER_FACTORY_TOKEN } from 'src/engine/core-modules/logic-function/logic-function-drivers/constants/logic-function-driver-factory.token';
import { LogicFunctionDriverFactory } from 'src/engine/core-modules/logic-function/logic-function-drivers/logic-function-driver.factory';
import { LogicFunctionResourceModule } from 'src/engine/core-modules/logic-function/logic-function-resource/logic-function-resource.module';
import { LogicFunctionTriggerModule } from 'src/engine/core-modules/logic-function/logic-function-trigger/logic-function-trigger.module';
@@ -24,9 +25,16 @@ export class LogicFunctionModule {
SdkClientModule,
WorkspaceCacheModule,
],
providers: [LogicFunctionDriverFactory],
providers: [
LogicFunctionDriverFactory,
{
provide: LOGIC_FUNCTION_DRIVER_FACTORY_TOKEN,
useExisting: LogicFunctionDriverFactory,
},
],
exports: [
LogicFunctionDriverFactory,
LOGIC_FUNCTION_DRIVER_FACTORY_TOKEN,
LogicFunctionResourceModule,
LogicFunctionTriggerModule,
LogicFunctionExecutorModule,