fix(server): bypass workspace cache in onboardingStatus resolver (#20322)
## Summary
In multi-instance deployments, `coreEntityCacheService` memoizes the
workspace entity per server for ~10s, bypassing Redis hash invalidation.
After `activateWorkspace`, if the next `currentUser` query is routed to
a stale replica, the server returns `onboardingStatus:
WORKSPACE_ACTIVATION` and `workspaceMember: null`, the client redirects
to `/create/profile`, and submitting the form throws "User is not logged
in". Reproduces on prod/staging only (local dev = single instance).
Fix: in `OnboardingService.getOnboardingStatus({ user, workspaceId })`,
read the workspace directly from `WorkspaceEntity` repository (bypassing
the per-instance core entity cache) so `onboardingStatus` reflects the
freshest `activationStatus` right after `activateWorkspace`, even when
the request hits a replica with a stale cached workspace.
## Test plan
- Prod/staging: sign up + create workspace, verify `/create/profile`
works and form submits.
- Local: regression on the full onboarding flow.
This commit is contained in:
@@ -1,13 +1,20 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { BillingModule } from 'src/engine/core-modules/billing/billing.module';
|
||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||
import { OnboardingResolver } from 'src/engine/core-modules/onboarding/onboarding.resolver';
|
||||
import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service';
|
||||
import { UserVarsModule } from 'src/engine/core-modules/user/user-vars/user-vars.module';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
|
||||
@Module({
|
||||
imports: [BillingModule, UserVarsModule, FeatureFlagModule],
|
||||
imports: [
|
||||
BillingModule,
|
||||
UserVarsModule,
|
||||
FeatureFlagModule,
|
||||
TypeOrmModule.forFeature([WorkspaceEntity]),
|
||||
],
|
||||
exports: [OnboardingService],
|
||||
providers: [OnboardingService, OnboardingResolver],
|
||||
})
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
|
||||
import { type QueryRunner } from 'typeorm';
|
||||
import { type QueryRunner, Repository } from 'typeorm';
|
||||
|
||||
import { BillingService } from 'src/engine/core-modules/billing/services/billing.service';
|
||||
import { OnboardingStatus } from 'src/engine/core-modules/onboarding/enums/onboarding-status.enum';
|
||||
@@ -32,6 +33,8 @@ export class OnboardingService {
|
||||
private readonly billingService: BillingService,
|
||||
private readonly userVarsService: UserVarsService<OnboardingKeyValueTypeMap>,
|
||||
private readonly twentyConfigService: TwentyConfigService,
|
||||
@InjectRepository(WorkspaceEntity)
|
||||
private readonly workspaceRepository: Repository<WorkspaceEntity>,
|
||||
) {}
|
||||
|
||||
private isWorkspaceActivationPending(workspace: WorkspaceEntity) {
|
||||
@@ -40,7 +43,25 @@ export class OnboardingService {
|
||||
);
|
||||
}
|
||||
|
||||
async getOnboardingStatus(user: UserEntity, workspace: WorkspaceEntity) {
|
||||
async getOnboardingStatus({
|
||||
user,
|
||||
workspaceId,
|
||||
}: {
|
||||
user: UserEntity;
|
||||
workspaceId: string;
|
||||
}): Promise<OnboardingStatus | null> {
|
||||
// We always read the workspace directly from the database here (bypassing
|
||||
// the per-instance core entity cache) so that onboardingStatus reflects the
|
||||
// freshest activationStatus right after activateWorkspace, even when a
|
||||
// sibling server instance still has a stale cached workspace.
|
||||
const workspace = await this.workspaceRepository.findOne({
|
||||
where: { id: workspaceId },
|
||||
});
|
||||
|
||||
if (!isDefined(workspace)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (
|
||||
await this.billingService.isSubscriptionIncompleteOnboardingStatus(
|
||||
workspace.id,
|
||||
|
||||
@@ -557,7 +557,10 @@ export class UserResolver {
|
||||
): Promise<OnboardingStatus | null> {
|
||||
if (!workspace) return null;
|
||||
|
||||
return this.onboardingService.getOnboardingStatus(user, workspace);
|
||||
return this.onboardingService.getOnboardingStatus({
|
||||
user,
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
}
|
||||
|
||||
@ResolveField(() => WorkspaceEntity, {
|
||||
|
||||
Reference in New Issue
Block a user