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:<workspaceId>`), mirroring the install path. The rate-limit throttle stays outside the lock. --------- Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
+2
@@ -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,
|
||||
|
||||
+15
@@ -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<WorkspaceMigrationDTO> {
|
||||
const applicationRegistrationId = await this.findApplicationRegistrationId(
|
||||
manifest.application.universalIdentifier,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user