Skip to content

Commit effd349

Browse files
committed
perf(mothership): cache the superuser routing gate
getMothershipBaseURL joined user+settings on EVERY chat turn to learn what is true for almost everyone: not a superuser. The negative gate now sits in an LRU (max 50k, 60s TTL); real superusers still read fresh each turn so an environment flip applies immediately, and the settings PATCH invalidates the toggling user's entry (same-process write → reader). Claude-Session: https://claude.ai/code/session_01CgaxNAaeD3taGdghbXn17w
1 parent 800e89e commit effd349

3 files changed

Lines changed: 49 additions & 0 deletions

File tree

apps/sim/app/api/users/me/settings/route.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { parseRequest, validationErrorResponse } from '@/lib/api/server'
88
import { getSession } from '@/lib/auth'
99
import { generateRequestId } from '@/lib/core/utils/request'
1010
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
11+
import { invalidateSuperUserGate } from '@/lib/mothership/server/agent-url'
1112
import { defaultUserSettings, getUserSettings } from '@/lib/users/queries'
1213

1314
const logger = createLogger('UserSettingsAPI')
@@ -71,6 +72,10 @@ export const PATCH = withRouteHandler(async (request: NextRequest) => {
7172
},
7273
})
7374

75+
/* Chat-turn routing caches the superuser gate; a toggle here must reach the very
76+
next turn, so drop this user's cached entry (same-process write → reader). */
77+
if ('superUserModeEnabled' in validatedData) invalidateSuperUserGate(userId)
78+
7479
return NextResponse.json({ success: true }, { status: 200 })
7580
} catch (error: any) {
7681
logger.error(`[${requestId}] Settings update error`, error)

apps/sim/lib/mothership/server/agent-url.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { user } from '@sim/db/schema'
22
import { queueTableRows, resetDbChainMock } from '@sim/testing'
33
import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest'
44
import {
5+
clearSuperUserGate,
56
getMothershipBaseURL,
67
getMothershipSourceEnvHeaders,
78
MOTHERSHIP_SOURCE_ENV_HEADER,
@@ -36,6 +37,7 @@ describe('getMothershipBaseURL', () => {
3637
beforeEach(() => {
3738
vi.clearAllMocks()
3839
resetDbChainMock()
40+
clearSuperUserGate()
3941
envMock.COPILOT_SOURCE_ENV = undefined
4042
})
4143

@@ -50,6 +52,27 @@ describe('getMothershipBaseURL', () => {
5052
)
5153
})
5254

55+
it('caches the negative gate: a non-superuser skips the DB on repeat turns until invalidated', async () => {
56+
queueTableRows(user, [
57+
{ role: 'user', superUserModeEnabled: false, mothershipEnvironment: 'dev' },
58+
])
59+
await expect(getMothershipBaseURL({ userId: 'user-cache' })).resolves.toBe(
60+
'https://default.mothership.test'
61+
)
62+
// A superuser row is now queued — a cache hit never reads it and stays default.
63+
queueTableRows(user, [
64+
{ role: 'admin', superUserModeEnabled: true, mothershipEnvironment: 'dev' },
65+
])
66+
await expect(getMothershipBaseURL({ userId: 'user-cache' })).resolves.toBe(
67+
'https://default.mothership.test'
68+
)
69+
// Invalidation (the settings PATCH path) makes the next turn read fresh.
70+
clearSuperUserGate()
71+
await expect(getMothershipBaseURL({ userId: 'user-cache' })).resolves.toBe(
72+
'https://dev.mothership.test'
73+
)
74+
})
75+
5376
it('ignores stored and explicit environments for non-admin users', async () => {
5477
queueTableRows(user, [
5578
{ role: 'user', superUserModeEnabled: true, mothershipEnvironment: 'dev' },

apps/sim/lib/mothership/server/agent-url.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { db } from '@sim/db'
22
import { settings, user } from '@sim/db/schema'
33
import { eq } from 'drizzle-orm'
4+
import { LRUCache } from 'lru-cache'
45
import { type MothershipEnvironment, mothershipEnvironmentSchema } from '@/lib/api/contracts'
56
import { env } from '@/lib/core/config/env'
67
import { SIM_AGENT_API_URL, SIM_AGENT_API_URL_DEFAULT } from '@/lib/mothership/constants'
@@ -40,6 +41,23 @@ function getDefaultMothershipBaseURL(fallbackUrl?: string | null): string {
4041
return normalizeUrl(fallback) ?? normalizeUrl(SIM_AGENT_API_URL) ?? SIM_AGENT_API_URL_DEFAULT
4142
}
4243

44+
/**
45+
* Superuser-gate cache: almost every user is NOT an admin with superuser mode on, yet the
46+
* check joined user+settings on every chat turn. Caching only the negative gate keeps the
47+
* hot path DB-free while a real superuser's environment selection stays fresh per turn
48+
* (their env flip must apply immediately). A newly granted superuser waits at most one TTL.
49+
*/
50+
const superUserGate = new LRUCache<string, boolean>({ max: 50_000, ttl: 60_000 })
51+
52+
export function invalidateSuperUserGate(userId: string): void {
53+
superUserGate.delete(userId)
54+
}
55+
56+
/** Test-only: drops every cached gate so each case sees its own queued DB rows. */
57+
export function clearSuperUserGate(): void {
58+
superUserGate.clear()
59+
}
60+
4361
export async function getMothershipBaseURL(
4462
options: GetMothershipBaseURLOptions = {}
4563
): Promise<string> {
@@ -48,6 +66,8 @@ export async function getMothershipBaseURL(
4866
const { userId } = options
4967
if (!userId) return defaultUrl
5068

69+
if (superUserGate.get(userId) === false) return defaultUrl
70+
5171
const [row] = await db
5272
.select({
5373
role: user.role,
@@ -60,6 +80,7 @@ export async function getMothershipBaseURL(
6080
.limit(1)
6181

6282
const effectiveSuperUser = row?.role === 'admin' && (row.superUserModeEnabled ?? false)
83+
superUserGate.set(userId, effectiveSuperUser)
6384
if (!effectiveSuperUser) return defaultUrl
6485

6586
const selectedEnvironment = options.environment ?? row.mothershipEnvironment

0 commit comments

Comments
 (0)