mirror of
https://github.com/morluto/rea.git
synced 2026-10-19 04:19:54 -05:00
fix(artifacts): serialize concurrent cleanup lease releases
Share the in-flight release for one exact owner, preserve failed attempts for retry, and notify dependent errors only after confirmed completion. Validation: concurrency control fails on3fcba0796 with two cleanup attempts; isolated foundation passes38 selected lease/scope/resource tests. Authored lifecycle controls, no native provider qualification.
This commit is contained in:
1 parent
3fcba07962
commit
16924e275e
2 files changed
+53
-7
No files matched your search
@@ -142,3 +142,39 @@ it.each([false, true])(
|
||||
expect(group.cleanupResources).toBeUndefined();
|
||||
},
|
||||
);
|
||||
|
||||
it("shares an in-flight release and permits one new attempt after failure", async () => {
|
||||
const pending: ((failure?: Error) => void)[] = [];
|
||||
let attempts = 0;
|
||||
const lease = new AnalysisCleanupLease(() => {
|
||||
attempts++;
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
pending.push((failure) =>
|
||||
failure === undefined ? resolve() : reject(failure),
|
||||
);
|
||||
});
|
||||
}, ["owned control"]);
|
||||
try {
|
||||
const first = lease.release();
|
||||
const shared = lease.release();
|
||||
const rejected = Promise.allSettled([first, shared]);
|
||||
await Promise.resolve();
|
||||
expect(attempts).toBe(1);
|
||||
const failure = new Error("controlled release failure");
|
||||
pending[0]!(failure);
|
||||
for (const outcome of await rejected)
|
||||
expect(outcome).toEqual({ status: "rejected", reason: failure });
|
||||
expect(lease.released).toBe(false);
|
||||
const retry = lease.release();
|
||||
const sharedRetry = lease.release();
|
||||
await Promise.resolve();
|
||||
expect(attempts).toBe(2);
|
||||
pending[1]!();
|
||||
await Promise.all([retry, sharedRetry]);
|
||||
expect(lease.released).toBe(true);
|
||||
await lease.release();
|
||||
expect(attempts).toBe(2);
|
||||
} finally {
|
||||
for (const settle of pending) settle();
|
||||
}
|
||||
});
|
||||
@@ -6,6 +6,7 @@ import type {
|
||||
/** Link one resource's confirmed release to its originating analysis error. */
|
||||
export class AnalysisCleanupLease {
|
||||
#released = false;
|
||||
#releasePromise: Promise<void> | undefined;
|
||||
#errors: WeakRef<AnalysisError>[] = [];
|
||||
readonly #groups = new Set<AnalysisCleanupLeaseGroup>();
|
||||
|
||||
@@ -56,13 +57,22 @@ export class AnalysisCleanupLease {
|
||||
}
|
||||
}
|
||||
|
||||
async release(): Promise<void> {
|
||||
if (this.#released) return;
|
||||
await this.cleanup();
|
||||
this.#released = true;
|
||||
this.#resolveBoundErrors();
|
||||
for (const group of this.#groups) group.ownerReleased(this);
|
||||
this.#groups.clear();
|
||||
release(): Promise<void> {
|
||||
if (this.#released) return Promise.resolve();
|
||||
if (this.#releasePromise !== undefined) return this.#releasePromise;
|
||||
const attempt = Promise.resolve()
|
||||
.then(async () => {
|
||||
await this.cleanup();
|
||||
this.#released = true;
|
||||
this.#resolveBoundErrors();
|
||||
for (const group of this.#groups) group.ownerReleased(this);
|
||||
this.#groups.clear();
|
||||
})
|
||||
.finally(() => {
|
||||
if (this.#releasePromise === attempt) this.#releasePromise = undefined;
|
||||
});
|
||||
this.#releasePromise = attempt;
|
||||
return attempt;
|
||||
}
|
||||
|
||||
/** Binding can follow a concurrent retry that already confirmed release. */
|
||||
|
||||
Reference in new issue
Block a user