From a817a1a672cd15b3fd86b3cea5d3dc7404dc064d Mon Sep 17 00:00:00 2001 From: Michael Bromley Date: Wed, 12 Jul 2023 10:49:12 +0200 Subject: [PATCH] fix(core): Add timeout logic to session cache handling This prevents a bad session cache (e.g. Redis gets disconnected) from badly impacting response times. --- .../src/service/services/session.service.ts | 32 +++++++++++++------ 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/packages/core/src/service/services/session.service.ts b/packages/core/src/service/services/session.service.ts index 5e948b8071..1b46aa28f7 100644 --- a/packages/core/src/service/services/session.service.ts +++ b/packages/core/src/service/services/session.service.ts @@ -29,6 +29,7 @@ import { OrderService } from './order.service'; export class SessionService implements EntitySubscriberInterface { private sessionCacheStrategy: SessionCacheStrategy; private readonly sessionDurationInMs: number; + private readonly sessionCacheTimeoutMs = 50; constructor( private connection: TransactionalConnection, @@ -65,7 +66,7 @@ export class SessionService implements EntitySubscriberInterface { // session cache will be wrong, so we just clear the entire cache. It should however // be a very rare occurrence in normal operation, once initial setup is complete. if (event.entity instanceof Channel || event.entity instanceof Role) { - await this.sessionCacheStrategy.clear(); + await this.withTimeout(this.sessionCacheStrategy.clear()); } } } @@ -96,7 +97,7 @@ export class SessionService implements EntitySubscriberInterface { invalidated: false, }), ); - await this.sessionCacheStrategy.set(this.serializeSession(authenticatedSession)); + await this.withTimeout(this.sessionCacheStrategy.set(this.serializeSession(authenticatedSession))); return authenticatedSession; } @@ -115,7 +116,7 @@ export class SessionService implements EntitySubscriberInterface { // save the new session const newSession = await this.connection.rawConnection.getRepository(AnonymousSession).save(session); const serializedSession = this.serializeSession(newSession); - await this.sessionCacheStrategy.set(serializedSession); + await this.withTimeout(this.sessionCacheStrategy.set(serializedSession)); return serializedSession; } @@ -124,14 +125,14 @@ export class SessionService implements EntitySubscriberInterface { * Returns the cached session object matching the given session token. */ async getSessionFromToken(sessionToken: string): Promise { - let serializedSession = await this.sessionCacheStrategy.get(sessionToken); + let serializedSession = await this.withTimeout(this.sessionCacheStrategy.get(sessionToken)); const stale = !!(serializedSession && serializedSession.cacheExpiry < new Date().getTime() / 1000); const expired = !!(serializedSession && serializedSession.expires < new Date()); if (!serializedSession || stale || expired) { const session = await this.findSessionByToken(sessionToken); if (session) { serializedSession = this.serializeSession(session); - await this.sessionCacheStrategy.set(serializedSession); + await this.withTimeout(this.sessionCacheStrategy.set(serializedSession)); return serializedSession; } else { return; @@ -168,6 +169,19 @@ export class SessionService implements EntitySubscriberInterface { return serializedSession; } + /** + * If the session cache is taking longer than say 50ms then something is wrong - it is supposed to + * be very fast after all! So we will return undefined and let the request continue without a cached session. + */ + private withTimeout(maybeSlow: Promise | T): Promise { + return Promise.race([ + new Promise(resolve => + setTimeout(() => resolve(undefined), this.sessionCacheTimeoutMs), + ), + maybeSlow, + ]); + } + /** * Looks for a valid session with the given token and returns one if found. */ @@ -205,7 +219,7 @@ export class SessionService implements EntitySubscriberInterface { session.activeOrder = order; await this.connection.getRepository(ctx, Session).save(session, { reload: false }); const updatedSerializedSession = this.serializeSession(session); - await this.sessionCacheStrategy.set(updatedSerializedSession); + await this.withTimeout(this.sessionCacheStrategy.set(updatedSerializedSession)); return updatedSerializedSession; } return serializedSession; @@ -245,7 +259,7 @@ export class SessionService implements EntitySubscriberInterface { session.activeChannel = channel; await this.connection.rawConnection.getRepository(Session).save(session, { reload: false }); const updatedSerializedSession = this.serializeSession(session); - await this.sessionCacheStrategy.set(updatedSerializedSession); + await this.withTimeout(this.sessionCacheStrategy.set(updatedSerializedSession)); return updatedSerializedSession; } return serializedSession; @@ -261,7 +275,7 @@ export class SessionService implements EntitySubscriberInterface { .find({ where: { user: { id: user.id } } }); await this.connection.getRepository(ctx, AuthenticatedSession).remove(userSessions); for (const session of userSessions) { - await this.sessionCacheStrategy.delete(session.token); + await this.withTimeout(this.sessionCacheStrategy.delete(session.token)); } } @@ -273,7 +287,7 @@ export class SessionService implements EntitySubscriberInterface { const sessions = await this.connection.getRepository(ctx, Session).find({ where: { activeOrderId } }); await this.connection.getRepository(ctx, Session).remove(sessions); for (const session of sessions) { - await this.sessionCacheStrategy.delete(session.token); + await this.withTimeout(this.sessionCacheStrategy.delete(session.token)); } }