diff --git a/api/src/routes/auth.ts b/api/src/routes/auth.ts index 001a200..f8dad37 100644 --- a/api/src/routes/auth.ts +++ b/api/src/routes/auth.ts @@ -14,6 +14,7 @@ import { toDateTime } from '@app/shared/utils/time'; import { logger } from '../services/logging/logger'; const querystring = require('querystring'); import { performance } from 'perf_hooks'; +import { CacheService } from '../services/cache/cache'; function parseJwt(token) { @@ -189,8 +190,31 @@ passport.deserializeUser(function (user, cb) { let con; try { - let t; + //cache lookup + let t = performance.now(); + const cachedData: UserData | undefined = userCache.Get(memberID); + timings.cache_lookup = performance.now() - t; + if (cachedData) { + timings.total = performance.now() - start; + + logger.info( + 'profiling', + 'passport.deserializeUser (cache hit)', + { + memberId: memberID, + cache_hit: true, + source: 'cache', + total_ms: timings.total, + breakdown_ms: timings, + }, + 'profiling' + ); + + return cb(null, cachedData); + } + + //cache miss, db load t = performance.now(); con = await pool.getConnection(); timings.getConnection = performance.now() - t; @@ -202,30 +226,30 @@ passport.deserializeUser(function (user, cb) { ); timings.memberQuery = performance.now() - t; - const userData: { - id: number; - name: string; - roles: Role[]; - state: MemberState; - discord_id?: string; - } = userResults[0]; + const userData: UserData = userResults[0]; t = performance.now(); - const userRoles = await getUserRoles(memberID); + userData.roles = await getUserRoles(memberID) || []; timings.roles = performance.now() - t; - userData.roles = userRoles || []; t = performance.now(); userData.state = await getUserState(memberID); timings.state = performance.now() - t; - // 📊 PROFILING LOG + t = performance.now(); + userCache.Set(userData.id, userData); + timings.cache_set = performance.now() - t; + + timings.total = performance.now() - start; + logger.info( 'profiling', - 'passport.deserializeUser completed', + 'passport.deserializeUser (db load)', { memberId: memberID, - total_ms: performance.now() - start, + cache_hit: false, + source: 'db', + total_ms: timings.total, breakdown_ms: timings, }, 'profiling' @@ -243,14 +267,12 @@ passport.deserializeUser(function (user, cb) { } ); return cb(error); - } finally { if (con) con.release(); } }); }); - declare global { namespace Express { interface Request { @@ -265,5 +287,15 @@ declare global { } } +export interface UserData { + id: number; + name: string; + roles: Role[]; + state: MemberState; + discord_id?: string; +} -export const authRouter = router; \ No newline at end of file +const userCache = new CacheService(); + +export const authRouter = router; +export const memberCache = userCache; \ No newline at end of file diff --git a/api/src/services/cache/cache.ts b/api/src/services/cache/cache.ts new file mode 100644 index 0000000..22c6fb8 --- /dev/null +++ b/api/src/services/cache/cache.ts @@ -0,0 +1,19 @@ +export class CacheService { + private cacheMap: Map + + constructor() { + this.cacheMap = new Map(); + } + + public Get(key: Key): Value { + return this.cacheMap.get(key) + } + + public Set(key: Key, value: Value) { + this.cacheMap.set(key, value); + } + + public Invalidate(key: Key): boolean { + return this.cacheMap.delete(key); + } +} \ No newline at end of file diff --git a/api/src/services/db/memberService.ts b/api/src/services/db/memberService.ts index d1a2db5..a0776d3 100644 --- a/api/src/services/db/memberService.ts +++ b/api/src/services/db/memberService.ts @@ -1,6 +1,8 @@ import { Role } from "@app/shared/types/roles"; import pool from "../../db"; import { Member, MemberCardDetails, MemberLight, memberSettings, MemberState } from '@app/shared/types/member' +import { logger } from "../logging/logger"; +import { memberCache } from "../../routes/auth"; export async function getUserData(userID: number): Promise { const sql = `SELECT * FROM view_member_rank_unit_status_latest WHERE member_id = ?`; @@ -9,10 +11,16 @@ export async function getUserData(userID: number): Promise { } export async function setUserState(userID: number, state: MemberState) { - const sql = `UPDATE members + try { + const sql = `UPDATE members SET state = ? WHERE id = ?;`; - return await pool.query(sql, [state, userID]); + return await pool.query(sql, [state, userID]); + } catch (error) { + logger.error('app', 'Error setting user state', error); + } finally { + memberCache.Invalidate(userID); + } } export async function getUserState(user: number): Promise { diff --git a/api/src/services/db/rolesService.ts b/api/src/services/db/rolesService.ts index 7a857ec..efb58ee 100644 --- a/api/src/services/db/rolesService.ts +++ b/api/src/services/db/rolesService.ts @@ -1,12 +1,20 @@ import { MemberLight } from '@app/shared/types/member'; import pool from '../../db'; import { Role, RoleSummary } from '@app/shared/types/roles' +import { logger } from '../logging/logger'; +import { memberCache } from '../../routes/auth'; export async function assignUserGroup(userID: number, roleID: number) { - const sql = `INSERT INTO members_roles (member_id, role_id) VALUES (?, ?);`; - const params = [userID, roleID]; + try { + const sql = `INSERT INTO members_roles (member_id, role_id) VALUES (?, ?);`; + const params = [userID, roleID]; - return await pool.query(sql, params); + return await pool.query(sql, params); + } catch (error) { + logger.error('app', 'Failed to assign user group', error); + } finally { + memberCache.Invalidate(userID); + } } export async function createGroup(name: string, color: string, description: string) {