From 84ba7eb8dc030413cfc3cccd4424819adb3e149d Mon Sep 17 00:00:00 2001 From: martmull Date: Fri, 5 Jun 2026 17:30:55 +0200 Subject: [PATCH] fix(app-dev): serialize dev sync per workspace with a cache lock (#21250) Split out of #21240. Stacked on #21249 (review/merge that first). Concurrent `syncApplication` calls on the same workspace could interleave their metadata migrations and leave metadata partially applied. Wrap the manifest sync in a per-workspace cache lock (`app-sync:`), mirroring the install path. The rate-limit throttle stays outside the lock. --------- Co-authored-by: Charles Bochet --- .../application-development.module.ts | 2 ++ .../application-development.resolver.ts | 15 +++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/packages/twenty-server/src/engine/core-modules/application/application-development/application-development.module.ts b/packages/twenty-server/src/engine/core-modules/application/application-development/application-development.module.ts index 5e768891bd..3837a4e24a 100644 --- a/packages/twenty-server/src/engine/core-modules/application/application-development/application-development.module.ts +++ b/packages/twenty-server/src/engine/core-modules/application/application-development/application-development.module.ts @@ -5,6 +5,7 @@ import { ApplicationManifestModule } from 'src/engine/core-modules/application/a import { ApplicationModule } from 'src/engine/core-modules/application/application.module'; import { ApplicationDevelopmentResolver } from 'src/engine/core-modules/application/application-development/application-development.resolver'; import { TokenModule } from 'src/engine/core-modules/auth/token/token.module'; +import { CacheLockModule } from 'src/engine/core-modules/cache-lock/cache-lock.module'; import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; import { FileStorageModule } from 'src/engine/core-modules/file-storage/file-storage.module'; import { ThrottlerModule } from 'src/engine/core-modules/throttler/throttler.module'; @@ -17,6 +18,7 @@ import { WorkspaceMigrationGraphqlApiExceptionInterceptor } from 'src/engine/wor ApplicationModule, ApplicationManifestModule, ApplicationRegistrationModule, + CacheLockModule, FeatureFlagModule, SdkClientModule, TokenModule, diff --git a/packages/twenty-server/src/engine/core-modules/application/application-development/application-development.resolver.ts b/packages/twenty-server/src/engine/core-modules/application/application-development/application-development.resolver.ts index daddd479ba..2c21f4ccb9 100644 --- a/packages/twenty-server/src/engine/core-modules/application/application-development/application-development.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/application/application-development/application-development.resolver.ts @@ -33,6 +33,7 @@ import { } from 'src/engine/core-modules/application/application.exception'; import { ApplicationService } from 'src/engine/core-modules/application/application.service'; import { ApplicationTokenService } from 'src/engine/core-modules/auth/token/services/application-token.service'; +import { CacheLockService } from 'src/engine/core-modules/cache-lock/cache-lock.service'; import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service'; import { validateFilePath } from 'src/engine/core-modules/file-storage/utils/validate-file-path.util'; import { FileDTO } from 'src/engine/core-modules/file/dtos/file.dto'; @@ -52,6 +53,8 @@ import { streamToBuffer } from 'src/utils/stream-to-buffer'; const APP_DEV_RATE_LIMIT_MAX = 30; const APP_DEV_RATE_LIMIT_WINDOW_MS = 30_000; +const APP_SYNC_LOCK_OPTIONS = { ttl: 60_000, ms: 500, maxRetries: 120 }; + @UsePipes(ResolverValidationPipe) @MetadataResolver() @UseInterceptors(WorkspaceMigrationGraphqlApiExceptionInterceptor) @@ -71,6 +74,7 @@ export class ApplicationDevelopmentResolver { private readonly sdkClientGenerationService: SdkClientGenerationService, private readonly twentyConfigService: TwentyConfigService, private readonly throttlerService: ThrottlerService, + private readonly cacheLockService: CacheLockService, ) {} @Mutation(() => DevelopmentApplicationDTO) @@ -137,6 +141,17 @@ export class ApplicationDevelopmentResolver { workspaceId, ); + return this.cacheLockService.withLock( + () => this.applyManifestSync(manifest, workspaceId), + `app-sync:${workspaceId}`, + APP_SYNC_LOCK_OPTIONS, + ); + } + + private async applyManifestSync( + manifest: ApplicationInput['manifest'], + workspaceId: string, + ): Promise { const applicationRegistrationId = await this.findApplicationRegistrationId( manifest.application.universalIdentifier, );