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>
123 lines
4.3 KiB
TypeScript
123 lines
4.3 KiB
TypeScript
export type ResumableUploadRuntimePolicy = Readonly<{
|
|
partSizeBytes: number;
|
|
maxFileBytes: number;
|
|
maxPartCount: number;
|
|
maxConcurrency: number;
|
|
maxInFlightBytes: number;
|
|
partBufferCopyFactor: number;
|
|
maxSourceChunkBytes: number;
|
|
maxRetries: number;
|
|
retryBaseDelayMs: number;
|
|
retryMaxDelayMs: number;
|
|
maxRetryAfterMs: number;
|
|
capabilityRefreshSkewMs: number;
|
|
maxSessionLifetimeMs: number;
|
|
providerAttemptTimeoutMs: number;
|
|
/**
|
|
* TR-RR-06. The bound `dispose()` applies to its drain. A non-cooperative
|
|
* mutation lock or provider would otherwise make teardown unbounded, so a
|
|
* caller could never learn whether the runtime was quiescent.
|
|
*/
|
|
cleanupDeadlineMs: number;
|
|
}>;
|
|
|
|
const MIB = 1024 * 1024;
|
|
const GIB = 1024 * MIB;
|
|
|
|
const ABSOLUTE_LIMITS = Object.freeze({
|
|
maxPartSizeBytes: 64 * MIB,
|
|
maxFileBytes: 100 * GIB,
|
|
maxPartCount: 10_000,
|
|
maxConcurrency: 8,
|
|
maxInFlightBytes: 256 * MIB,
|
|
maxPartBufferCopyFactor: 8,
|
|
maxSourceChunkBytes: 64 * MIB,
|
|
maxRetries: 8,
|
|
maxRetryDelayMs: 60_000,
|
|
maxRetryAfterMs: 60_000,
|
|
maxCapabilityRefreshSkewMs: 5 * 60_000,
|
|
maxSessionLifetimeMs: 7 * 24 * 60 * 60_000,
|
|
maxProviderAttemptTimeoutMs: 2 * 60_000,
|
|
});
|
|
|
|
const DEFAULT_POLICY: ResumableUploadRuntimePolicy = Object.freeze({
|
|
partSizeBytes: 5 * MIB,
|
|
maxFileBytes: 5 * GIB,
|
|
maxPartCount: 1_024,
|
|
maxConcurrency: 3,
|
|
maxInFlightBytes: 20 * MIB,
|
|
partBufferCopyFactor: 4,
|
|
maxSourceChunkBytes: 8 * MIB,
|
|
maxRetries: 3,
|
|
retryBaseDelayMs: 250,
|
|
retryMaxDelayMs: 5_000,
|
|
maxRetryAfterMs: 30_000,
|
|
capabilityRefreshSkewMs: 5_000,
|
|
maxSessionLifetimeMs: 24 * 60 * 60_000,
|
|
providerAttemptTimeoutMs: 30_000,
|
|
cleanupDeadlineMs: 10_000,
|
|
});
|
|
|
|
export function resolveResumableUploadRuntimePolicy(
|
|
input: Partial<ResumableUploadRuntimePolicy> = {},
|
|
): ResumableUploadRuntimePolicy {
|
|
const policy: ResumableUploadRuntimePolicy = Object.freeze({
|
|
...DEFAULT_POLICY,
|
|
...input,
|
|
});
|
|
if (
|
|
!positiveSafeInteger(policy.partSizeBytes) ||
|
|
policy.partSizeBytes > ABSOLUTE_LIMITS.maxPartSizeBytes ||
|
|
!positiveSafeInteger(policy.maxFileBytes) ||
|
|
policy.maxFileBytes > ABSOLUTE_LIMITS.maxFileBytes ||
|
|
!positiveSafeInteger(policy.maxPartCount) ||
|
|
policy.maxPartCount > ABSOLUTE_LIMITS.maxPartCount ||
|
|
!positiveSafeInteger(policy.maxConcurrency) ||
|
|
policy.maxConcurrency > ABSOLUTE_LIMITS.maxConcurrency ||
|
|
!positiveSafeInteger(policy.maxInFlightBytes) ||
|
|
policy.maxInFlightBytes > ABSOLUTE_LIMITS.maxInFlightBytes ||
|
|
!positiveSafeInteger(policy.partBufferCopyFactor) ||
|
|
policy.partBufferCopyFactor >
|
|
ABSOLUTE_LIMITS.maxPartBufferCopyFactor ||
|
|
policy.maxInFlightBytes <
|
|
policy.partSizeBytes * policy.partBufferCopyFactor ||
|
|
!positiveSafeInteger(policy.maxSourceChunkBytes) ||
|
|
policy.maxSourceChunkBytes >
|
|
ABSOLUTE_LIMITS.maxSourceChunkBytes ||
|
|
!nonNegativeSafeInteger(policy.maxRetries) ||
|
|
policy.maxRetries > ABSOLUTE_LIMITS.maxRetries ||
|
|
!positiveSafeInteger(policy.retryBaseDelayMs) ||
|
|
policy.retryBaseDelayMs > ABSOLUTE_LIMITS.maxRetryDelayMs ||
|
|
!positiveSafeInteger(policy.retryMaxDelayMs) ||
|
|
policy.retryMaxDelayMs > ABSOLUTE_LIMITS.maxRetryDelayMs ||
|
|
policy.retryBaseDelayMs > policy.retryMaxDelayMs ||
|
|
!nonNegativeSafeInteger(policy.maxRetryAfterMs) ||
|
|
policy.maxRetryAfterMs > ABSOLUTE_LIMITS.maxRetryAfterMs ||
|
|
!nonNegativeSafeInteger(policy.capabilityRefreshSkewMs) ||
|
|
policy.capabilityRefreshSkewMs >
|
|
ABSOLUTE_LIMITS.maxCapabilityRefreshSkewMs ||
|
|
!positiveSafeInteger(policy.maxSessionLifetimeMs) ||
|
|
policy.maxSessionLifetimeMs >
|
|
ABSOLUTE_LIMITS.maxSessionLifetimeMs ||
|
|
!positiveSafeInteger(policy.providerAttemptTimeoutMs) ||
|
|
policy.providerAttemptTimeoutMs >
|
|
ABSOLUTE_LIMITS.maxProviderAttemptTimeoutMs ||
|
|
!positiveSafeInteger(policy.cleanupDeadlineMs) ||
|
|
policy.cleanupDeadlineMs >
|
|
ABSOLUTE_LIMITS.maxProviderAttemptTimeoutMs * 2 ||
|
|
Math.ceil(policy.maxFileBytes / policy.partSizeBytes) >
|
|
policy.maxPartCount
|
|
) {
|
|
throw new TypeError("Resumable upload policy is invalid.");
|
|
}
|
|
return policy;
|
|
}
|
|
|
|
function positiveSafeInteger(value: number): boolean {
|
|
return Number.isSafeInteger(value) && value > 0;
|
|
}
|
|
|
|
function nonNegativeSafeInteger(value: number): boolean {
|
|
return Number.isSafeInteger(value) && value >= 0;
|
|
}
|