File size: 15,079 Bytes
e249c6d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
import { afterEach, describe, expect, it } from "vitest";
import { createDeferred } from "../../../test/helpers/promise.js";
import { drainGlobalSingletonLifecycleState } from "../../shared/global-singleton.js";
import {
  beginForegroundSessionMaintenance,
  createSessionMaintenanceOwner,
  waitForSessionMaintenance,
} from "./coordinator.js";

afterEach(async () => {
  await drainGlobalSingletonLifecycleState("close");
});

describe("session maintenance ownership", () => {
  it("aborts optional work before admitting foreground and waits for its actual cleanup", async () => {
    const started = createDeferred();
    const aborted = createDeferred();
    const cleanupStarted = createDeferred();
    const finishCleanup = createDeferred();
    const events: string[] = [];
    const owner = createSessionMaintenanceOwner({
      sessionKey: "session:cancel",
      preemptible: true,
    });
    owner.signal.addEventListener("abort", () => aborted.resolve(), { once: true });
    const work = owner.track(
      owner.run(async () => {
        started.resolve();
        try {
          await aborted.promise;
          owner.assertCurrent();
        } finally {
          events.push("cleanup started");
          cleanupStarted.resolve();
          await finishCleanup.promise;
          events.push("cleanup finished");
        }
      }),
    );
    const rejection = expect(work).rejects.toMatchObject({ name: "AbortError" });
    await started.promise;

    const admission = beginForegroundSessionMaintenance("session:cancel").then((release) => {
      events.push("foreground admitted");
      return release;
    });
    try {
      await cleanupStarted.promise;
      expect(owner.signal.aborted).toBe(true);
      expect(events).toEqual(["cleanup started"]);
    } finally {
      finishCleanup.resolve();
      (await admission)();
      await rejection;
    }
    expect(events).toEqual(["cleanup started", "cleanup finished", "foreground admitted"]);
    await owner.done;
  });

  it("keeps optional work pending until every foreground reservation releases without blocking reads", async () => {
    const releaseFirst = await beginForegroundSessionMaintenance("session:pending");
    const releaseSecond = await beginForegroundSessionMaintenance("session:pending");
    const owner = createSessionMaintenanceOwner({
      sessionKey: "session:pending",
      preemptible: true,
    });
    let ran = false;
    const work = owner.track(
      owner.run(async () => {
        owner.assertCurrent();
        ran = true;
      }),
    );
    try {
      await waitForSessionMaintenance("session:pending");
      expect(ran).toBe(false);

      releaseFirst();
      releaseFirst();
      await waitForSessionMaintenance("session:pending");
      expect(ran).toBe(false);
      expect(owner.signal.aborted).toBe(false);
    } finally {
      releaseFirst();
      releaseSecond();
      await work;
    }
    expect(ran).toBe(true);
  });

  it("admits another session while non-preemptible maintenance keeps its own session waiting", async () => {
    const started = createDeferred();
    const finish = createDeferred();
    const owner = createSessionMaintenanceOwner({ sessionKey: "session:busy" });
    const work = owner.track(
      owner.run(async () => {
        started.resolve();
        await finish.promise;
      }),
    );
    await started.promise;
    let busyAdmitted = false;
    const busyAdmission = beginForegroundSessionMaintenance("session:busy").then((release) => {
      busyAdmitted = true;
      return release;
    });
    try {
      const releaseOther = await beginForegroundSessionMaintenance("session:other");
      releaseOther();
      await waitForSessionMaintenance("session:other");
      expect(busyAdmitted).toBe(false);
      expect(owner.signal.aborted).toBe(false);
    } finally {
      finish.resolve();
      await work;
      (await busyAdmission)();
    }
    expect(busyAdmitted).toBe(true);
  });

  it("allows nested maintenance reads to exclude their own active ancestors", async () => {
    const outer = createSessionMaintenanceOwner({ sessionKey: "session:nested" });
    const events: string[] = [];
    await outer.track(
      outer.run(async () => {
        const inner = createSessionMaintenanceOwner({ sessionKey: "session:nested" });
        await inner.track(
          inner.run(async () => {
            await waitForSessionMaintenance("session:nested");
            inner.assertCurrent();
            outer.assertCurrent();
            events.push("nested read completed");
          }),
        );
        await waitForSessionMaintenance("session:nested");
        events.push("outer read completed");
      }),
    );
    expect(events).toEqual(["nested read completed", "outer read completed"]);
    await waitForSessionMaintenance("session:nested");
  });

  it.each([true, false])(
    "serializes independent siblings and lets the older owner read with preemptible=%s",
    async (preemptible) => {
      const sessionKey = `session:siblings:${preemptible}`;
      const started = createDeferred();
      const allowRead = createDeferred();
      const readCompleted = createDeferred();
      const finishOlder = createDeferred();
      const stop = createDeferred();
      const events: string[] = [];
      const older = createSessionMaintenanceOwner({ sessionKey, preemptible });
      const olderWork = older.track(
        older.run(async () => {
          events.push("older started");
          started.resolve();
          await allowRead.promise;
          await Promise.race([waitForSessionMaintenance(sessionKey), stop.promise]);
          events.push("older read completed");
          readCompleted.resolve();
          await finishOlder.promise;
          events.push("older finished");
        }),
      );
      await started.promise;
      const younger = createSessionMaintenanceOwner({ sessionKey, preemptible: !preemptible });
      const youngerWork = younger.track(
        younger.run(async () => {
          events.push("younger started");
          await Promise.race([waitForSessionMaintenance(sessionKey), stop.promise]);
          events.push("younger read completed");
        }),
      );
      try {
        expect(events).toEqual(["older started"]);
        allowRead.resolve();
        await readCompleted.promise;
        expect(events).toEqual(["older started", "older read completed"]);
        finishOlder.resolve();
        await Promise.all([olderWork, youngerWork]);
        expect(events).toEqual([
          "older started",
          "older read completed",
          "older finished",
          "younger started",
          "younger read completed",
        ]);
      } finally {
        // Unblock fixture reads after an ordering failure so real owner cleanup still runs.
        stop.resolve();
        allowRead.resolve();
        finishOlder.resolve();
        await Promise.allSettled([olderWork, youngerWork]);
      }
    },
  );

  it.each(
    (["external dependent", "nested independent", "released parent"] as const).flatMap(
      (placement) => (["read", "foreground"] as const).map((action) => ({ placement, action })),
    ),
  )("orders child $action with $placement sibling", async ({ placement, action }) => {
    const sessionKey = `session:combined:${placement}:${action}`;
    const parentStarted = createDeferred();
    const allowChild = createDeferred();
    const childCreated = createDeferred();
    const childCompleted = createDeferred();
    const siblingStarted = createDeferred();
    const finishSibling = createDeferred();
    const finishParent = createDeferred();
    const events: string[] = [];
    const parent = createSessionMaintenanceOwner({ sessionKey });
    let child: ReturnType<typeof createSessionMaintenanceOwner> | undefined;
    let childWork: Promise<void> = Promise.resolve();
    let siblingWork: Promise<void> = Promise.resolve();
    const startSibling = () => {
      const sibling = createSessionMaintenanceOwner({ sessionKey });
      siblingWork = sibling.track(
        sibling.run(async () => {
          events.push("sibling started");
          siblingStarted.resolve();
          await finishSibling.promise;
          events.push("sibling completed");
        }),
      );
    };
    const parentWork = parent.track(
      parent.run(async () => {
        events.push("parent started");
        parentStarted.resolve();
        await allowChild.promise;
        if (placement === "nested independent") {
          startSibling();
          await siblingStarted.promise;
        } else if (placement === "released parent") {
          parent.releaseWrites();
          await siblingStarted.promise;
        }
        child = createSessionMaintenanceOwner({ sessionKey });
        childWork = child.track(
          child.run(async () => {
            events.push("child started");
            if (action === "read") {
              await waitForSessionMaintenance(sessionKey);
            } else {
              const release = await beginForegroundSessionMaintenance(sessionKey);
              release();
            }
            events.push("child completed");
            childCompleted.resolve();
          }),
        );
        childCreated.resolve();
        await childWork;
        events.push("parent resumed");
        await finishParent.promise;
        events.push("parent completed");
      }),
    );
    try {
      await parentStarted.promise;
      if (placement !== "nested independent") {
        startSibling();
      }
      allowChild.resolve();
      await childCreated.promise;
      // These owner waits have no I/O: drain the scheduled continuations without
      // relying on a test timeout to discover a circular dependency.
      await new Promise<void>((resolve) => {
        setImmediate(resolve);
      });
      if (placement === "external dependent") {
        expect([...events]).toEqual([
          "parent started",
          "child started",
          "child completed",
          "parent resumed",
        ]);
      } else {
        expect([...events]).toEqual(["parent started", "sibling started"]);
        finishSibling.resolve();
        await childCompleted.promise;
      }
      finishParent.resolve();
      await parentWork;
      await siblingStarted.promise;
      finishSibling.resolve();
      await siblingWork;
      const ordered =
        placement === "external dependent"
          ? ["child completed", "parent completed", "sibling started"]
          : ["sibling completed", "child started", "parent completed"];
      expect(events.filter((event) => ordered.includes(event))).toEqual(ordered);
    } finally {
      // Break a failed fixture's dependency cycle only after its ordering assertion;
      // actual tracked completion still settles every owner before teardown.
      allowChild.resolve();
      finishParent.resolve();
      finishSibling.resolve();
      parent.releaseWrites();
      child?.releaseWrites();
      await Promise.allSettled([parentWork, siblingWork, childWork]);
    }
    await waitForSessionMaintenance(sessionKey);
  });

  it("releases writes after disposal while retaining completion ownership of an external rerun", async () => {
    const sessionKey = "session:external-rerun";
    const disposing = createDeferred();
    const finishDisposal = createDeferred();
    const childRead = createDeferred();
    const finishChild = createDeferred();
    const stop = createDeferred();
    const events: string[] = [];
    const parent = createSessionMaintenanceOwner({ sessionKey });
    let childWork: Promise<void> = Promise.resolve();
    const parentWork = parent.track(
      parent.run(async () => {
        events.push("parent disposing");
        disposing.resolve();
        await finishDisposal.promise;
        parent.releaseWrites();
        expect(parent.assertCurrent).toThrow();
        events.push("parent writes released");
        await childWork;
        events.push("parent completed");
      }),
    );
    await disposing.promise;
    const child = createSessionMaintenanceOwner({ sessionKey });
    childWork = child.track(
      child.run(async () => {
        events.push("child started");
        await Promise.race([waitForSessionMaintenance(sessionKey), stop.promise]);
        events.push("child read completed");
        childRead.resolve();
        await finishChild.promise;
        events.push("child completed");
      }),
    );
    let parentDone = false;
    void parent.done.then(() => {
      parentDone = true;
    });
    try {
      expect(events).toEqual(["parent disposing"]);
      finishDisposal.resolve();
      await childRead.promise;
      expect(events).toEqual([
        "parent disposing",
        "parent writes released",
        "child started",
        "child read completed",
      ]);
      expect(parentDone).toBe(false);
      finishChild.resolve();
      await Promise.all([parentWork, childWork, parent.done]);
      expect(events.slice(-2)).toEqual(["child completed", "parent completed"]);
      expect(parentDone).toBe(true);
    } finally {
      stop.resolve();
      finishDisposal.resolve();
      finishChild.resolve();
      await Promise.allSettled([parentWork, childWork]);
    }
  });

  it.each(["fulfilled", "rejected"] as const)(
    "fences a retained assertion after tracked work is %s",
    async (outcome) => {
      const owner = createSessionMaintenanceOwner({ sessionKey: `session:settled:${outcome}` });
      const assertCurrent = owner.assertCurrent;
      const completion = createDeferred<string>();
      const work = owner.track(completion.promise);
      expect(assertCurrent).not.toThrow();
      if (outcome === "fulfilled") {
        completion.resolve("completed");
        await expect(work).resolves.toBe("completed");
      } else {
        const error = new Error("maintenance failed");
        const rejection = expect(work).rejects.toBe(error);
        completion.reject(error);
        await rejection;
      }

      await owner.done;
      expect(assertCurrent).toThrow();
      await waitForSessionMaintenance(`session:settled:${outcome}`);
    },
  );

  it.each(["close", "restart"] as const)(
    "aborts active maintenance and fences retained authority on process %s",
    async (event) => {
      const finish = createDeferred();
      const owner = createSessionMaintenanceOwner({ sessionKey: `session:lifecycle:${event}` });
      const assertCurrent = owner.assertCurrent;
      const work = owner.track(finish.promise);
      const rotation = drainGlobalSingletonLifecycleState(event);
      try {
        expect(owner.signal.aborted).toBe(true);
        expect(assertCurrent).toThrow();
      } finally {
        finish.resolve();
        await Promise.all([work, rotation]);
      }
      const successor = createSessionMaintenanceOwner({ sessionKey: `session:lifecycle:${event}` });
      await successor.track(
        successor.run(async () => {
          successor.assertCurrent();
        }),
      );
      expect(assertCurrent).toThrow();
    },
  );
});