Fix latency spike on application lookup (#21042)

Fixes
https://discord.com/channels/1130383047699738754/1509645089062781058

Caching application entities to improve authentication latency
This commit is contained in:
martmull
2026-05-29 11:06:36 +02:00
committed by GitHub
parent 41832c8d82
commit 961745e9ba
7 changed files with 33 additions and 29 deletions
+2 -2
View File
@@ -1,7 +1,7 @@
import { type APP_LOCALES } from 'twenty-shared/translations';
import { type FlatApiKey } from 'src/engine/core-modules/api-key/types/flat-api-key.type';
import { type ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
import { type RawAuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
import { type FlatAuthContextUser } from 'src/engine/core-modules/auth/types/flat-auth-context-user.type';
import { type FlatUserWorkspace } from 'src/engine/core-modules/user-workspace/types/flat-user-workspace.type';
@@ -13,7 +13,7 @@ declare module 'express-serve-static-core' {
interface Request {
user?: FlatAuthContextUser | null;
apiKey?: FlatApiKey | null;
application?: ApplicationEntity | null;
application?: FlatApplication | null;
userWorkspace?: FlatUserWorkspace;
locale: keyof typeof APP_LOCALES;
workspace?: FlatWorkspace;
@@ -80,7 +80,9 @@ export class BackfillRelationJoinColumnIndexesCommand extends ActiveOrSuspendedW
const indexedFieldIds = new Set<string>();
for (const flatIndex of Object.values(flatIndexMaps.byUniversalIdentifier)) {
for (const flatIndex of Object.values(
flatIndexMaps.byUniversalIdentifier,
)) {
if (!isDefined(flatIndex)) {
continue;
}
@@ -155,10 +157,12 @@ export class BackfillRelationJoinColumnIndexesCommand extends ActiveOrSuspendedW
universalFlatIndexMetadata,
joinColumnName,
} of flatIndexBuildPlans) {
const { schemaName, tableName } = getWorkspaceSchemaContextForMigration({
workspaceId,
objectMetadata: flatObjectMetadata,
});
const { schemaName, tableName } = getWorkspaceSchemaContextForMigration(
{
workspaceId,
objectMetadata: flatObjectMetadata,
},
);
await this.workspaceSchemaManagerService.indexManager.createIndex({
queryRunner,
@@ -1,9 +1,9 @@
import { type ActorMetadata, FieldActorSource } from 'twenty-shared/types';
import { type ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
type BuildCreatedByFromApplicationArgs = {
application: ApplicationEntity;
application: FlatApplication;
};
export const buildCreatedByFromApplication = ({
application,
@@ -6,7 +6,6 @@ import { ApiKeyModule } from 'src/engine/core-modules/api-key/api-key.module';
import { ApplicationRegistrationModule } from 'src/engine/core-modules/application/application-registration/application-registration.module';
import { AppTokenEntity } from 'src/engine/core-modules/app-token/app-token.entity';
import { AppTokenService } from 'src/engine/core-modules/app-token/services/app-token.service';
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
import { ConnectionProviderModule } from 'src/engine/core-modules/application/connection-provider/connection-provider.module';
import { ConnectionProviderOAuthController } from 'src/engine/core-modules/application/connection-provider/connection-provider-oauth.controller';
@@ -90,7 +89,6 @@ import { JwtAuthStrategy } from './strategies/jwt.auth.strategy';
UserEntity,
AppTokenEntity,
ApiKeyEntity,
ApplicationEntity,
FeatureFlagEntity,
WorkspaceSSOIdentityProviderEntity,
KeyValuePairEntity,
@@ -17,7 +17,6 @@ import { JwtAuthStrategy } from './jwt.auth.strategy';
describe('JwtAuthStrategy', () => {
let strategy: JwtAuthStrategy;
let userWorkspaceRepository: any;
let applicationRepository: any;
let jwtWrapperService: any;
let permissionsService: any;
let workspaceCacheService: any;
@@ -30,21 +29,19 @@ describe('JwtAuthStrategy', () => {
let workspaceStore: Record<string, any>;
let userStore: Record<string, any>;
let applicationStore: Record<string, Record<string, any>>;
let apiKeyStore: Record<string, Record<string, any>>;
beforeEach(() => {
workspaceStore = {};
userStore = {};
applicationStore = {};
apiKeyStore = {};
userWorkspaceRepository = {
findOne: jest.fn(),
};
applicationRepository = {
findOne: jest.fn(),
};
jwtWrapperService = {
extractJwtFromRequest: jest.fn(() => () => 'token'),
resolveVerificationKey: jest.fn(async () => ({
@@ -80,6 +77,12 @@ describe('JwtAuthStrategy', () => {
};
}
if (cacheKeys.includes('flatApplicationMaps')) {
result.flatApplicationMaps = {
byId: applicationStore[workspaceId] ?? {},
};
}
if (cacheKeys.includes('apiKeyMap')) {
result.apiKeyMap = apiKeyStore[workspaceId] ?? {};
}
@@ -116,7 +119,6 @@ describe('JwtAuthStrategy', () => {
const createStrategy = () =>
new JwtAuthStrategy(
jwtWrapperService,
applicationRepository,
userWorkspaceRepository,
permissionsService,
workspaceCacheService,
@@ -346,9 +348,10 @@ describe('JwtAuthStrategy', () => {
workspaceId: validWorkspaceId,
};
workspaceStore[validWorkspaceId] = new WorkspaceEntity();
const mockWorkspace = new WorkspaceEntity();
applicationRepository.findOne.mockResolvedValue(null);
mockWorkspace.id = validWorkspaceId;
workspaceStore[validWorkspaceId] = mockWorkspace;
strategy = createStrategy();
@@ -3,13 +3,11 @@ import { PassportStrategy } from '@nestjs/passport';
import { InjectRepository } from '@nestjs/typeorm';
import { msg } from '@lingui/core/macro';
import { Strategy, type SecretOrKeyProvider } from 'passport-jwt';
import { type SecretOrKeyProvider, Strategy } from 'passport-jwt';
import { PermissionFlagType } from 'twenty-shared/constants';
import { assertIsDefinedOrThrow, isDefined } from 'twenty-shared/utils';
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
import { Repository } from 'typeorm';
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
import {
AuthException,
AuthExceptionCode,
@@ -36,8 +34,6 @@ import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/works
export class JwtAuthStrategy extends PassportStrategy(Strategy, 'jwt') {
constructor(
private readonly jwtWrapperService: JwtWrapperService,
@InjectRepository(ApplicationEntity)
private readonly applicationRepository: Repository<ApplicationEntity>,
@InjectRepository(UserWorkspaceEntity)
private readonly userWorkspaceRepository: Repository<UserWorkspaceEntity>,
private readonly permissionsService: PermissionsService,
@@ -375,9 +371,12 @@ export class JwtAuthStrategy extends PassportStrategy(Strategy, 'jwt') {
const applicationId = payload.sub ?? payload.applicationId;
const application = await this.applicationRepository.findOne({
where: { id: applicationId },
});
const { flatApplicationMaps } =
await this.workspaceCacheService.getOrRecompute(workspace.id, [
'flatApplicationMaps',
]);
const application = flatApplicationMaps.byId[applicationId];
if (!isDefined(application)) {
throw new AuthException(
@@ -1,10 +1,10 @@
import { type ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
import { type AuthProviderEnum } from 'src/engine/core-modules/workspace/types/workspace.type';
import { type FlatApiKey } from 'src/engine/core-modules/api-key/types/flat-api-key.type';
import { type FlatAuthContextUser } from 'src/engine/core-modules/auth/types/flat-auth-context-user.type';
import { type FlatUserWorkspace } from 'src/engine/core-modules/user-workspace/types/flat-user-workspace.type';
import { type FlatWorkspace } from 'src/engine/core-modules/workspace/types/flat-workspace.type';
import { type WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
export { AUTH_CONTEXT_USER_SELECT_FIELDS } from 'src/engine/core-modules/auth/constants/auth-context-user-select-fields.constants';
export { type FlatAuthContextUser as AuthContextUser } from 'src/engine/core-modules/auth/types/flat-auth-context-user.type';
@@ -15,7 +15,7 @@ export type RawAuthContext = {
workspaceMemberId?: string;
workspaceMember?: WorkspaceMemberWorkspaceEntity;
workspace?: FlatWorkspace;
application?: ApplicationEntity | null | undefined;
application?: FlatApplication | null | undefined;
userWorkspaceId?: string;
userWorkspace?: FlatUserWorkspace;
authProvider?: AuthProviderEnum;