fix: bound resumable teardown, image concurrency and delivery leases

TR-RR-06. dispose() now bounds its drain with a cleanupDeadlineMs from policy
and returns the result, so a non-cooperative mutation lock or provider can no
longer make teardown unbounded and an unproved drain is reported as still
CLOSING instead of closed over. The checkpoint store stays open in that case,
because something can still write to it. An abort is admitted physical work
like an upload, so it joins the tracked set rather than being stepped over.

TR-RR-07. The verification slot belongs to the raw verifier, not the wrapper.
Releasing it when the caller's wait expired let an abandoned verification keep
running while a new one was admitted, so repeated aborts produced more
concurrent physical work than the configured cap allows. The slot is now
released only once the raw tasks settle.

TR-RR-04. A presigned byte source owns a fetch reader and a capability lease and
its port requires close(); the delivery consumer never called it. The closeable
subtype is lost in the FileByteSource projection, so a holder keeps it from the
moment the lease exists and the outermost finally closes it exactly once — on
success, validation failure, writer failure and abort alike.

check:adapter-inventory now also fails if the shared abortable-operation
primitive has no production importers. It was safe to add only once the
presigned subsystems actually migrated onto it; a gate that fails CI for a
documented, unfixed defect reports the wrong thing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
DongHyeonka
2026-08-14 17:22:45 +09:00
co-authored by Claude Opus 5
parent 46e067e555
commit 5a76f95291
8 changed files with 336 additions and 27 deletions
@@ -77,7 +77,12 @@ export type ResumableUploadRuntime = ResumableUploadPort &
* closes the checkpoint store and cancellation channel, so success actually
* means quiescent. No current bootstrap consumer is assumed.
*/
dispose(): Promise<void>;
/**
* TR-RR-06. Closes admission and returns the *bounded* drain result. A
* failure means the runtime is still `CLOSING`: physical work the caller
* must not treat as finished is still in flight.
*/
dispose(): Promise<BrowserDataResult<void>>;
lifecycle(): "OPEN" | "CLOSING" | "CLOSED";
}>;
@@ -178,7 +183,7 @@ export function createResumableUploadRuntime<Capability>(
const activeOperations = new Set<Promise<unknown>>();
let closed = false;
let lifecycle: "OPEN" | "CLOSING" | "CLOSED" = "OPEN";
let drain: Promise<void> | null = null;
let drain: Promise<BrowserDataResult<void>> | null = null;
const cancelLocalUploads = (uploadKey: string): void => {
if (!SAFE_UPLOAD_KEY.test(uploadKey)) return;
for (const controller of localUploads.get(uploadKey) ?? []) {
@@ -280,17 +285,19 @@ export function createResumableUploadRuntime<Capability>(
dependencies.crossContextCancellation?.publish(uploadKey);
}
const combined = combineAbortSignals(input.signal, lifetime.signal);
const operation = dependencies.mutationLock.run(
uploadKey,
combined.signal,
async () =>
await executeAbort(dependencies, uploadKey, combined.signal),
);
// TR-RR-06. An abort is admitted physical work like an upload, so it is
// tracked from admission and `dispose()` cannot step over it.
const tracked = Promise.resolve(operation).catch(() => undefined);
activeOperations.add(tracked);
void tracked.finally(() => activeOperations.delete(tracked));
try {
return await dependencies.mutationLock.run(
uploadKey,
combined.signal,
async () =>
await executeAbort(
dependencies,
uploadKey,
combined.signal,
),
);
return await operation;
} catch (error) {
return mapLockFailure(error, "UPLOAD_ABORT");
} finally {
@@ -304,14 +311,14 @@ export function createResumableUploadRuntime<Capability>(
void startDrain();
},
dispose(): Promise<void> {
dispose(): Promise<BrowserDataResult<void>> {
return startDrain();
},
lifecycle: () => lifecycle,
});
function startDrain(): Promise<void> {
function startDrain(): Promise<BrowserDataResult<void>> {
drain ??= (async () => {
closed = true;
lifecycle = "CLOSING";
@@ -322,10 +329,32 @@ export function createResumableUploadRuntime<Capability>(
for (const controllers of localUploads.values()) {
for (const controller of controllers) controller.abort();
}
await Promise.allSettled([...activeOperations]);
// TR-RR-06. Bounded. A non-cooperative mutation lock or provider must not
// make teardown unbounded, and an unproved drain is reported as such
// rather than closed over.
let timer: ReturnType<typeof setTimeout> | undefined;
const expired = new Promise<"EXPIRED">((resolve) => {
timer = setTimeout(
() => resolve("EXPIRED"),
dependencies.policy.cleanupDeadlineMs,
);
});
const drained = await Promise.race([
Promise.allSettled([...activeOperations]).then(() => "DRAINED" as const),
expired,
]);
if (timer !== undefined) clearTimeout(timer);
if (drained === "EXPIRED") {
// The store stays open: something can still write a checkpoint.
return browserDataFailure("UNAVAILABLE", "UPLOAD_ABORT", {
retryable: true,
recovery: "RESUME",
});
}
// The store closes only after nothing can still write a checkpoint.
dependencies.checkpoints.close();
lifecycle = "CLOSED";
return browserDataSuccess(undefined);
})();
return drain;
}