History

Skip the subject sighting read for a principal already seen this window

Rhys Sullivan committed 3 months ago

Commit ca65a6c1 parent 5044b512 files · +135 −3Browse files

Unified · Split

2 files
  1. packages/core/sdk/src/subject-registry.test.ts +136 −4
  2. packages/core/sdk/src/subject-registry.ts +0 −0
packages/core/sdk/src/subject-registry.test.ts+136−4
@@ -1,4 +1,4 @@
1import { describe, expect, it } from "@effect/vitest";1import { beforeEach, describe, expect, it } from "@effect/vitest";
2import { Effect } from "effect";2import { Effect } from "effect";
3import { withQueryContext } from "@executor-js/fumadb/query";3import { withQueryContext } from "@executor-js/fumadb/query";
4 4
@@ -7,7 +7,7 @@ import { AuthTemplateSlug, ConnectionName, IntegrationSlug, ToolName } from "./i · 2 unmodified lines
7import type { ExecutorOwnerPolicyContext } from "./owner-policy";7import type { ExecutorOwnerPolicyContext } from "./owner-policy";
8import { definePlugin } from "./plugin";8import { definePlugin } from "./plugin";
9import { createSqliteTestFumaDb, type SqliteTestFumaDb } from "./sqlite-test-db";9import { createSqliteTestFumaDb, type SqliteTestFumaDb } from "./sqlite-test-db";
10import { touchSubject } from "./subject-registry";10import { resetSubjectTouchCache, touchSubject } from "./subject-registry";
11import { makeTestWorkspaceHarness, memoryCredentialsPlugin } from "./test-config";11import { makeTestWorkspaceHarness, memoryCredentialsPlugin } from "./test-config";
12 12
13// `touchSubject` is the only writer of the `subject` table. Written against the13// `touchSubject` is the only writer of the `subject` table. Written against the
@@ -48,6 +48,14 @@ const storedSubjects = ( · 34 unmodified lines
48 }));48 }));
49 });49 });
50 50
51// `touchSubject` remembers, per process, which principals it already filed
52// inside the throttle window. Every case below reuses the same tenant/principal
53// against a FRESH database, so that memory has to be dropped between them or a
54// later case would skip the query its assertion depends on.
55beforeEach(() => {
56 resetSubjectTouchCache();
57});
58
51describe("touchSubject", () => {59describe("touchSubject", () => {
52 it.effect("creates the row on first sight", () =>60 it.effect("creates the row on first sight", () =>
53 withDb((db) =>61 withDb((db) =>
@@ -75,8 +83,8 @@ describe("touchSubject", () => { · 21 unmodified lines
75 });83 });
76 const [first] = yield* storedSubjects(db);84 const [first] = yield* storedSubjects(db);
77 85
78 // The hot path: N requests in quick succession must cost N reads and86 // The hot path: N requests in quick succession must cost zero writes,
79 // zero writes, so the persisted stamp is unchanged afterwards.87 // so the persisted stamp is unchanged afterwards.
80 yield* Effect.forEach([1, 2, 3], () =>88 yield* Effect.forEach([1, 2, 3], () =>
81 touchSubject(db.db, {89 touchSubject(db.db, {
82 tenant: TENANT,90 tenant: TENANT,
@@ -108,6 +116,11 @@ describe("touchSubject", () => { · 25 unmodified lines
108 set: { last_seen_at: BigInt(stale) },116 set: { last_seen_at: BigInt(stale) },
109 }),117 }),
110 );118 );
119 // Backdating the row stands in for elapsed time, and elapsed time ages
120 // the process-local sighting memory too — otherwise this would assert
121 // against a clock the memory never saw. Expiring it is what the passage
122 // of time does; the assertion below is unchanged.
123 resetSubjectTouchCache();
111 124
112 yield* touchSubject(db.db, {125 yield* touchSubject(db.db, {
113 tenant: TENANT,126 tenant: TENANT,
@@ -122,6 +135,125 @@ describe("touchSubject", () => { · 8 unmodified lines
122 ),135 ),
123 );136 );
124 137
138 it.effect("issues no query at all for a repeat sighting inside the throttle", () =>
139 withDb((db) =>
140 Effect.gen(function* () {
141 // THE regression this guards: the request seam runs `touchSubject` on
142 // every HTTP request and MCP session, so a sighting that still costs an
143 // indexed read per request adds a round-trip to every request in the
144 // product. Dropping the table after the first sighting makes any
145 // further query fail loudly — and `touchSubject` swallows storage
146 // failures, so a surviving query would be invisible in the row state.
147 // Counting statements is the only way to see it.
148 yield* touchSubject(db.db, {
149 tenant: TENANT,
150 externalId: "user_a",
151 lastSeenThrottleMs: THROTTLE_MS,
152 });
153
154 let queries = 0;
155 const client = db.client as { execute: (...args: never[]) => unknown };
156 const execute = client.execute.bind(client) as (...args: never[]) => unknown;
157 client.execute = (...args: never[]) => {
158 queries += 1;
159 return execute(...args);
160 };
161
162 yield* Effect.forEach([1, 2, 3], () =>
163 touchSubject(db.db, {
164 tenant: TENANT,
165 externalId: "user_a",
166 lastSeenThrottleMs: THROTTLE_MS,
167 }),
168 );
169
170 client.execute = execute as typeof client.execute;
171 expect(queries).toBe(0);
172 }),
173 ),
174 );
175
176 it.effect("re-reads once the remembered sighting ages past the throttle", () =>
177 withDb((db) =>
178 Effect.gen(function* () {
179 // The memory must expire with the throttle, not pin a principal as
180 // "recently seen" forever: `last_seen_at` still has to advance for a
181 // principal who keeps calling across windows.
182 yield* touchSubject(db.db, {
183 tenant: TENANT,
184 externalId: "user_a",
185 lastSeenThrottleMs: THROTTLE_MS,
186 });
187 const [first] = yield* storedSubjects(db);
188
189 // A zero throttle makes every remembered sighting instantly stale,
190 // which is what a caller crossing the window observes.
191 yield* touchSubject(db.db, {
192 tenant: TENANT,
193 externalId: "user_a",
194 lastSeenThrottleMs: 0,
195 });
196
197 const rows = yield* storedSubjects(db);
198 expect(rows).toHaveLength(1);
199 expect(rows[0]?.lastSeenMs).toBeGreaterThanOrEqual(first?.lastSeenMs ?? 0);
200 }),
201 ),
202 );
203
204 it.effect("remembers a first sighting per tenant, not per principal", () =>
205 withDb((db) =>
206 Effect.gen(function* () {
207 // One account acting in two orgs is two rows. A memory keyed on the
208 // principal alone would file the first tenant and skip the second,
209 // losing the row the other tenant's admin view depends on.
210 yield* touchSubject(db.db, {
211 tenant: TENANT,
212 externalId: "user_a",
213 lastSeenThrottleMs: THROTTLE_MS,
214 });
215 yield* touchSubject(db.db, {
216 tenant: OTHER_TENANT,
217 externalId: "user_a",
218 lastSeenThrottleMs: THROTTLE_MS,
219 });
220
221 expect((yield* storedSubjects(db, OTHER_TENANT)).map((row) => row.externalId)).toEqual([
222 "user_a",
223 ]);
224 }),
225 ),
226 );
227
228 it.effect("does not remember a sighting that failed to persist", () =>
229 withDb((db) =>
230 Effect.gen(function* () {
231 // A storage failure is swallowed so the request survives. It must not
232 // also be recorded as a successful sighting, or the principal would
233 // stay unfiled for a whole throttle window after the fault cleared.
234 yield* Effect.promise(() => db.client.execute("DROP TABLE subject"));
235 yield* touchSubject(db.db, {
236 tenant: TENANT,
237 externalId: "user_a",
238 lastSeenThrottleMs: THROTTLE_MS,
239 });
240
241 yield* Effect.promise(() =>
242 db.client.execute(
243 "CREATE TABLE subject (row_id text PRIMARY KEY NOT NULL, tenant text NOT NULL, external_id text NOT NULL, created_at integer NOT NULL, last_seen_at blob, status text)",
244 ),
245 );
246 yield* touchSubject(db.db, {
247 tenant: TENANT,
248 externalId: "user_a",
249 lastSeenThrottleMs: THROTTLE_MS,
250 });
251
252 expect((yield* storedSubjects(db)).map((row) => row.externalId)).toEqual(["user_a"]);
253 }),
254 ),
255 );
256
125 it.effect("records nothing for a pure-org executor", () =>257 it.effect("records nothing for a pure-org executor", () =>
126 withDb((db) =>258 withDb((db) =>
127 Effect.gen(function* () {259 Effect.gen(function* () {
packages/core/sdk/src/subject-registry.ts+0−0

Binary file not shown.