- commit
- cb03d34aaf7d93602fcb0a3e5f012ae8ef55615d
- parent
- 6a6abf5c169ac9567fc5126f4227259ff98e52fb
- author
- Dami <dami.etoile@gmail.com>
| M | test/ | 148 | ++++++++++++++++-------------- |
@@ -385,76 +385,86 @@ describe("GRASP", () => {
385385 const path = gitRepositoryPath(npubEncode(pk(owner)), "promotion");386386 const requestId = "promotion-recovery";387387 const stub = env.RELAY.getByName(`grasp-promotion-${mode}`);388- await runInDurableObject(stub, async (relay) => {389- const bucket = relay.media;390- let published = false;391- let failed = false;392- const observed = new Proxy(bucket, { get(target, name) {393- if (name === "get") return (key: string, options?: R2GetOptions) => {394- if (published && key.endsWith("/root.json")) { failed = true; throw new Error("publication succeeded before promotion read failed"); }395- return target.get(key, options);396- };397- if (name === "put") return async (...args: unknown[]) => {398- const result = await Reflect.apply(target.put, target, args);399- if (String(args[0]).endsWith("/root.json") && result) published = true;400- return result;401- };402- const value = Reflect.get(target, name, target);403- return typeof value === "function" ? value.bind(target) : value;404- } });405- Object.defineProperty(relay, "media", { value: observed, configurable: true });406- try {407- const response = await relay.fetch(new Request(`http://${host}${path}/git-receive-pack`, {408- method: "POST",409- headers: { "content-type": "application/x-git-receive-pack-request", "X-Git-Request-Id": requestId },410- body: concat(packet(`${"0".repeat(40)} ${pack.commitID} refs/heads/main\0report-status\n`), flush(), pack.pack),411- }));412- expect(await response.text()).not.toContain("ok refs/heads/main");413- expect(published).toBe(true);414- expect(failed).toBe(true);415- } finally { Reflect.deleteProperty(relay, "media"); }416- const wal = await gitRepository(relay, storedRepository(relay, pk(owner), "promotion")!);417- expect((await wal.load()).sequence).toBe(1);418- expect(relay.sql.exec("SELECT id FROM grasp_pending WHERE id IN (?,?)", announcement.id, state.id).toArray()).toHaveLength(2);419- });420- const replay = await runInDurableObject(stub, async (relay) => {421- const bucket = relay.media;422- let roots = 0;423- const published: string[] = [];424- const broadcast = relay.broadcast.bind(relay);425- Object.defineProperty(relay, "broadcast", { configurable: true, value: (event: Parameters<typeof relay.broadcast>[0]) => {426- published.push(event.id);427- broadcast(event);428- } });429- const observed = new Proxy(bucket, { get(target, name) {430- if (name === "get") return (key: string, options?: R2GetOptions) => {431- // The replay loads the WAL, then promotion loads it again. Change432- // eligibility only after promotion's initial candidate check.433- if (key.endsWith("/root.json") && ++roots === 2) {434- if (mode === "expired") relay.sql.exec("UPDATE events SET expires=1 WHERE id=?", pr.id);435- else relay.settings.setEvent(pr.id, "hide");388+ const replay = await runInDurableObject(stub, async (relay, context) => {389+ await context.storage.deleteAlarm();390+ const deadline = Date.now() + 5000;391+ while (relay.repositoryAccess.busy && Date.now() < deadline) await new Promise((resolve) => setTimeout(resolve, 10));392+ return relay.repositoryAccess.run("git", async () => {393+ await context.storage.deleteAlarm();394+ // Both phases share one owner so an alarm cannot heal the injected395+ // interruption before the identical HTTP retry proves recovery.396+ {397+ const bucket = relay.media;398+ let published = false;399+ let failed = false;400+ const observed = new Proxy(bucket, { get(target, name) {401+ if (name === "get") return (key: string, options?: R2GetOptions) => {402+ if (published && key.endsWith("/root.json")) { failed = true; throw new Error("publication succeeded before promotion read failed"); }403+ return target.get(key, options);404+ };405+ if (name === "put") return async (...args: unknown[]) => {406+ const result = await Reflect.apply(target.put, target, args);407+ if (String(args[0]).endsWith("/root.json") && result) published = true;408+ return result;409+ };410+ const value = Reflect.get(target, name, target);411+ return typeof value === "function" ? value.bind(target) : value;412+ } });413+ Object.defineProperty(relay, "media", { value: observed, configurable: true });414+ try {415+ const response = await relay.fetch(new Request(`http://${host}${path}/git-receive-pack`, {416+ method: "POST",417+ headers: { "content-type": "application/x-git-receive-pack-request", "X-Git-Request-Id": requestId },418+ body: concat(packet(`${"0".repeat(40)} ${pack.commitID} refs/heads/main\0report-status\n`), flush(), pack.pack),419+ }));420+ expect(await response.text()).not.toContain("ok refs/heads/main");421+ expect(published).toBe(true);422+ expect(failed).toBe(true);423+ } finally { Reflect.deleteProperty(relay, "media"); }424+ const wal = await gitRepository(relay, storedRepository(relay, pk(owner), "promotion")!);425+ expect((await wal.load()).sequence).toBe(1);426+ expect(relay.sql.exec("SELECT id FROM grasp_pending WHERE id IN (?,?)", announcement.id, state.id).toArray()).toHaveLength(2);427+ }428+ {429+ const bucket = relay.media;430+ let roots = 0;431+ const published: string[] = [];432+ const broadcast = relay.broadcast.bind(relay);433+ Object.defineProperty(relay, "broadcast", { configurable: true, value: (event: Parameters<typeof relay.broadcast>[0]) => {434+ published.push(event.id);435+ broadcast(event);436+ } });437+ const observed = new Proxy(bucket, { get(target, name) {438+ if (name === "get") return (key: string, options?: R2GetOptions) => {439+ // The replay loads the WAL, then promotion loads it again. Change440+ // eligibility only after promotion's initial candidate check.441+ if (key.endsWith("/root.json") && ++roots === 2) {442+ if (mode === "expired") relay.sql.exec("UPDATE events SET expires=1 WHERE id=?", pr.id);443+ else relay.settings.setEvent(pr.id, "hide");444+ }445+ return target.get(key, options);446+ };447+ const value = Reflect.get(target, name, target);448+ return typeof value === "function" ? value.bind(target) : value;449+ } });450+ Object.defineProperty(relay, "media", { value: observed, configurable: true });451+ try {452+ const response = await relay.fetch(new Request(`http://${host}${path}/git-receive-pack`, {453+ method: "POST",454+ headers: { "content-type": "application/x-git-receive-pack-request", "X-Git-Request-Id": requestId },455+ body: concat(packet(`${"0".repeat(40)} ${pack.commitID} refs/heads/main\0report-status\n`), flush(), pack.pack),456+ }));457+ expect(roots).toBeGreaterThanOrEqual(2);458+ expect(published).toContain(announcement.id);459+ expect(published).toContain(state.id);460+ expect(published).not.toContain(pr.id);461+ return await response.text();462+ } finally {463+ Reflect.deleteProperty(relay, "media");464+ Reflect.deleteProperty(relay, "broadcast");436465 }437- return target.get(key, options);438- };439- const value = Reflect.get(target, name, target);440- return typeof value === "function" ? value.bind(target) : value;441- } });442- Object.defineProperty(relay, "media", { value: observed, configurable: true });443- try {444- const response = await relay.fetch(new Request(`http://${host}${path}/git-receive-pack`, {445- method: "POST",446- headers: { "content-type": "application/x-git-receive-pack-request", "X-Git-Request-Id": requestId },447- body: concat(packet(`${"0".repeat(40)} ${pack.commitID} refs/heads/main\0report-status\n`), flush(), pack.pack),448- }));449- expect(roots).toBeGreaterThanOrEqual(2);450- expect(published).toContain(announcement.id);451- expect(published).toContain(state.id);452- expect(published).not.toContain(pr.id);453- return await response.text();454- } finally {455- Reflect.deleteProperty(relay, "media");456- Reflect.deleteProperty(relay, "broadcast");457- }466+ }467+ }, () => { throw new Error("promotion fixture did not settle"); });458468 });459469 expect(replay).toContain("ok refs/heads/main");460470 await runInDurableObject(stub, async (relay) => {