You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Reclaim payload from unfinished ContentBlock uploads after one hour without new upload progress, while preserving other uploads and completed manifests. Library preparations use the same inactivity window. This keeps failed-upload storage under Grace's control and gives users a useful retry opportunity without charging them for inaccessible unfinished content.
Parent: Issue #554. Scott selected this cleanup implementation, approved the one-hour inactivity rule for Library preparations as well as ordinary uploads, and authorized implementation through review/fixes, Shape Review and CI. Stop before merge approval.
Delivery mode and baseline admissibility
Mainline slice from origin/main at 9b18c97ab342d319d3bdeaedc5f1d64571913cf5; eventual target main.
Remote main, fetched origin/main, local main and HEAD match. All 42 worktree registrations were inspected; unrelated work is preserved.
ADMISSIBLE: current Grace.Server.fsproj Release build succeeded with zero warnings/errors using isolated build output. No earlier branch is required ancestry.
Product V1; existing supported HTTP/F# SDK/CLI upload producers, non-reentrant Orleans actors, current Cosmos provider and Azure Blob storage. Actual local provider evidence uses Azurite. Ordinary retries, duplicate delivery, caller cancellation, process restart, delayed storage requests and lost responses are included.
One new partial-state lifecycle lives in the existing ContentBlockMetadata owner: temporary upload holds, guarded publication and cleanup. Extend the existing UploadSession lifecycle with its retry deadline; do not create a parallel content owner, reference count, SQL table, catalog or cleanup service.
Required:
One hour after Start or genuinely new confirmed content, new valid reuse ranges, or first manifest completion. Exact replay, reconfirming an already confirmed block, status reads, discovery, URI issuance and intents do not count as progress.
Library preparations use that same clock, replacing their fixed 15-minute expiry. Keep repository, operation, principal, hashes, permission, revision and accepted-receipt replay checks.
After expiry, reclaim whole blocks with no completed-manifest metadata and no remaining upload hold. Delete staging data and compact session evidence only after required block cleanup succeeds.
Preserve a full retry hour after observed process recovery or substantially overdue reminder processing. Persist the recovery grant; ordinary actor reactivation and duplicate callbacks must not keep extending it.
Target cleanup within the next 15 minutes when dependencies and reminder processing are healthy. This is a target, not measured capacity.
Excluded:
Any block with existing ContentBlockMetadata, including zero active counts, pending repository contributions, previously completed but unreferenced manifests and partially used shared blocks. Current metadata presence is monotonic. Existing 24-hour/size/churn/version compaction rules remain unchanged; no operational compaction worker is claimed.
A manifest selected by a completed session whose metadata merge is still pending remains protected by its upload hold. Do not discard that evidence to meet a cleanup target. Extra uploaded blocks omitted from the completed manifest can be reclaimed.
This is unfinished-upload cleanup, not general retirement of completed content or a guarantee that every unsuccessful later Library/repository operation leaves no retained storage.
No usage totals, minute facts, billing, new diagnostics, quotas, whole-file metadata correction, generic health-history service, cloud failover or load-test project. Inaccessible unfinished uploads remain non-chargeable; this child creates no billing producer.
Delayed empty-placeholder creation may leave a zero-byte object. Such overhead is internal; do not promise permanent object absence or treat an empty placeholder as a valid ContentBlock.
Primary invariant and source ownership
An expired upload may release only its own hold. Cleanup must never remove payload needed by another live upload or by a completed manifest. UploadSession decides its lifecycle; ContentBlockMetadata serializes holds, publication and whole-block deletion for (StoragePoolId, ContentBlockAddress). Blob ETags identify physical incarnations; they do not identify users of unchanged bytes.
Keep call direction session -> block -> storage/dedupe. No block-to-session callback while a session request is waiting. Existing BlockUploadIntents are the release inventory. Reuse claims already require metadata and need no new hold mechanism for this deletion boundary.
Algorithm readiness
Verdict: Simplified and demonstrated for the selected effect sequence. Before production edits, 26 checks passed in the disposable block prototype, 11 in the initial provider boundary test and 19 in the clock/reminder model.
Real Azurite effects ran in separate processes. They demonstrate that old IfNoneMatch payload writes can recreate deleted content; exact IfMatch payload writes cannot recreate a deleted object or overwrite its replacement. They exercise both arrival orders and lost upload/delete responses.
Session/block coordination, CAS decisions and clock delivery are explicitly modeled using local JSON. They are not hosted Grace actor results. Implementation must preserve these seams and add focused actor/provider coverage.
Reuse docs/design/Libraries.Storage-Experiment.md for unchanged Microsoft.Orleans.Persistence.Cosmos 10.2.2-hpk.1 conditional persistence: real stale-ETag rejection, changed failed working buffer, duplicate-create rejection, completed-write response loss and fresh reads. Current actor store registration uses the same provider. This does not establish serialization of the new records; build/codecs and current-head tests must cover them.
Captures: C:/Source/Grace/artifacts/upload-cleanup-owner-preflight-20260910/decision.json, its run-d4dee37b28a946beb7a5f2f38e2407b0, and C:/Source/Grace/artifacts/upload-cleanup-readiness-20260910/clock-90190ff298284ed7a71a3adc877805c3. Add a portable checked evidence bundle and concise design report to this PR.
Required order:
Durably register the existing session block intent before requesting a hold. Session lifecycle revalidation and block calls run inside the serialized session turn.
Block owner persists the hold and exact placement before storage work. Retired session/intent identities cannot reacquire, even when release arrived before acquisition.
Only an empty placeholder may be created with IfNoneMatch=*. After creating/observing it, successfully persist the exact preparation ETag before any nonempty payload PUT. Payload writes use that exact IfMatch ETag. A stale or uncertain state write must abort before follow-on storage effects; never continue from a changed, failed working buffer.
A lost upload response is reconciled by validating complete stored ContentBlock bytes and equivalence while the hold remains. Empty or corrupt content is never upload success. Keep existing route, address, encoding and placement validation.
Persist session expiry/terminal state before releasing its recorded holds. Preserve completed-manifest holds until metadata merge is durable. Store retirement even if acquisition has not arrived.
Only Metadata.None plus no holds permits a durable deletion reservation. Exclude new holds and metadata acceptance while unresolved. Observe the blob, persist its exact deletion ETag, then conditionally delete. On 412, inspect current state; never equate 412 with absence. If a delayed payload won first, persist the new deletion ETag before retrying. Retain failures for retry.
Persist cleanup completion before compacting the session. Retain released-intent history after payload removal. A late request must either be rejected or start a new legitimate upload; it cannot restore an expired hold.
Retry clock and reminders
Keep one RetryExpiresAt on UploadSession. Remove the independent expiry from its Library binding; keep the existing public Library preparation response's ExpiresAt, populated from the session clock.
Use one deterministic immutable reminder identity per session/deadline. Establish the first durable reminder before accepting Started. A failed Start can leave a harmless reminder.
Progress changes the deadline. The existing callback observes it and creates/ensures the next distinct reminder before acknowledging the old one. Duplicate old callbacks must not overwrite or delete the current reminder. Activation or replay may repair the same reminder without extending time.
Select a 120-second tolerance above the normal 60-second poll. A due callback beyond that tolerance, or a live retry window last advanced before the current process startup, grants one hour from the first observed recovery. Persist that event. Do not use actor activation time or revive a durably closed window.
Evaluate recovery in the upload retry/preparation path before rejecting it, and in reminder processing. Explain that this detects restart/overdue processing conservatively; it does not measure an outage occurring entirely between observations. Overload or process movement can retain data longer.
First completion keeps the full hour for Library acceptance. Completed preparation expiry closes its retry window before temporary-state cleanup; metadata-present blocks remain protected. A closed/compacted preparation reports PreparedContentExpired rather than a missing-binding exception. Already accepted Library replay continues to check its permanent receipt before resolving the temporary upload.
Acceptance and required failures
Upload valid blocks through the storage route; retry without resending already confirmed content while the window is open. Show a long active upload exceeding one hour, with progress extending its deadline.
Demonstrate that duplicate confirmation under a different operation ID and polling do not extend retention. Cover new reuse-range progress without changing its membership rules.
Expire an idle session, release only its holds and reclaim eligible real payload. Two sessions sharing a block retain it until both release. Completed metadata at zero count retains it.
Restart after publication before session confirmation, before/after deletion reservation, after delete before acknowledgment and around reminder replacement. Assert persisted residue and eventual result, not just error responses.
Exercise delayed acquire after release, delayed payload after deletion/replacement and payload-before-delete ordering. A stale provider/state revision must not win.
Exercise Library preparation across 15 minutes, progress, completion, expiry and restart. Keep permission/revision rejection and accepted operation replay after temporary cleanup.
No observation or RepositoryStorageBytesMinute producer appears; no missing data becomes zero.
Contract propagation and paths
Owned production seams: src/Grace.Types/{UploadSession,ContentBlockMetadata,Reminder}.Types.fs; src/Grace.Actors/{UploadSession,ContentBlockMetadata,Services,Interfaces,Constants,LibraryTransfer,RepositoryLibrary}.Actor.fs; src/Grace.Server/{Storage,Library}.Server.fs. A small helper file in these existing projects is allowed if it concentrates this same lifecycle; project compile entries may change, with no new project/package.
Owned consumers/evidence: focused affected Types/Server.Unit/Server integration tests and their project entries; existing SDK/CLI upload or Library callers only where the same changed DTO/expiry requires propagation; Orleans generated output and existing OpenAPI/client generation surfaces only for this contract. Keep routes and authorization unchanged. Check generated client representation instead of assuming internal types are invisible.
Owned docs: docs/Operations.md, the upload-cleanup sections of docs/adr/0001-content-addressed-storage-contentblocks.md, relevant Library design/contract docs, docs/design/UploadCleanup.Preflight.*, and nearby AGENTS guidance contradicted by the newly approved cleanup behavior. README/CONTRIBUTING only if an advertised command/workflow changes.
Forbidden: unrelated branches, worktrees, dirty files and generated outputs; root undefined/; detached C:/Source/Grace-worktrees/ops1068 and its retained dist; historical Issue #957 worktree including all four dirty files. No reset, clean, prune, or unrelated fixes.
Run, validation and completion
Tier 2, controller/worker. Root coordinates this issue; one worker owns implementation and all local corrections and accepted R1 repairs. All children use fork_turns=none; no nested agents.
Format touched F# first. Build the matching Release test projects before focused tests with --no-build. Run meaningful state/codec and provider tests; required GitHub Validate covers the final head. No routine local Fast/Full duplication.
One R1 discovery review with finite findings; classify/freeze once. One consolidated repair pass by the same owner; R2 only to verify those repairs/direct regressions. Independent HTML Shape Review, refreshed if representations change. No automatic R3.
Stop if implementation contradicts the selected storage fence, needs a second content owner/lifecycle, broadens into completed-content GC or health-history infrastructure, changes accepted accounting/Library authorization, or requires a third prerequisite before the owner-usage tracer. Routine technical choices and focused fixes remain authorized.
Staging fence refinement before the first candidate
Initial implementation inspection found that GetContentBlockUploadUri could issue a valid SAS at minute 59 while session cleanup ran at minute 60. The supported SDK uses an unconditional PUT with a 60-minute network timeout. A deadline cap fixes a new late arrival but cannot justify assuming an already accepted request was cancelled.
Under Scott's instruction to make the necessary decisions and finish the agreed cleanup, extend the demonstrated exact-ETag sequence to staging inside the existing UploadSession owner. This replaces the earlier assumption that the existing staging delete helper alone was sufficient. The base, delivery mode, primary outcome and existing owners remain unchanged.
Retain prepared staging placement/ETag in the existing intent/event state. Within the session turn, check its lifecycle and intent, create/observe only an empty staging placeholder, and successfully persist its exact ETag before returning a grant. Failed or stale session writes cannot return a newly prepared grant.
Cap the SAS expiry at the earlier of 15 minutes from issue and the session retry deadline. URI issuance still is not progress.
The existing URI response already carries Grace metadata in its fragment. Carry the prepared ContentBlock ETag there too; the SDK places it in the existing StoragePlacement.ETag and requires it for a conditional single PUT. Preserve the current route/response shape. Document the new required grant metadata; do not add a fallback for imagined old production grants. Whole-file upload and read grants are unchanged.
Cleanup retains the recorded intent until deletion succeeds. A late payload with the old staging ETag cannot recreate a deleted object or overwrite its replacement. Delayed bootstrap creation may leave only zero bytes, which remains internal overhead.
Extend owned paths to the exact staging URI creation helper in Services.Actor.fs, CAS upload helper in Storage.SDK.fs, and matching SDK/hosted grant-consumer tests and documentation. This is the same upload publication/cleanup sequence, not a second service, owner or content ledger.
The additional disposable provider run passed 15 checks, including four real SAS controls: unconditional valid SAS recreates deleted staging; exact-ETag SAS upload fails after deletion; old ETag fails after replacement; current ETag succeeds. Source/results/receipt: C:/Source/Grace/artifacts/upload-cleanup-readiness-20260910/run-5efc30c2238e42998bf8946027851f63. This tests storage conditions with actual signed requests, not a suspended in-flight socket or new Grace actor integration. The earlier exact-ETag ordering model supplies the persist-before-grant requirement; current-head tests must exercise its integration.
Reclaim unfinished ContentBlock uploads
Outcome
Reclaim payload from unfinished ContentBlock uploads after one hour without new upload progress, while preserving other uploads and completed manifests. Library preparations use the same inactivity window. This keeps failed-upload storage under Grace's control and gives users a useful retry opportunity without charging them for inaccessible unfinished content.
Parent: Issue #554. Scott selected this cleanup implementation, approved the one-hour inactivity rule for Library preparations as well as ordinary uploads, and authorized implementation through review/fixes, Shape Review and CI. Stop before merge approval.
Delivery mode and baseline admissibility
origin/mainat9b18c97ab342d319d3bdeaedc5f1d64571913cf5; eventual targetmain.Grace.Server.fsprojRelease build succeeded with zero warnings/errors using isolated build output. No earlier branch is required ancestry.Supported world and scope
Product V1; existing supported HTTP/F# SDK/CLI upload producers, non-reentrant Orleans actors, current Cosmos provider and Azure Blob storage. Actual local provider evidence uses Azurite. Ordinary retries, duplicate delivery, caller cancellation, process restart, delayed storage requests and lost responses are included.
One new partial-state lifecycle lives in the existing ContentBlockMetadata owner: temporary upload holds, guarded publication and cleanup. Extend the existing UploadSession lifecycle with its retry deadline; do not create a parallel content owner, reference count, SQL table, catalog or cleanup service.
Required:
Excluded:
Primary invariant and source ownership
An expired upload may release only its own hold. Cleanup must never remove payload needed by another live upload or by a completed manifest. UploadSession decides its lifecycle; ContentBlockMetadata serializes holds, publication and whole-block deletion for
(StoragePoolId, ContentBlockAddress). Blob ETags identify physical incarnations; they do not identify users of unchanged bytes.Keep call direction session -> block -> storage/dedupe. No block-to-session callback while a session request is waiting. Existing BlockUploadIntents are the release inventory. Reuse claims already require metadata and need no new hold mechanism for this deletion boundary.
Algorithm readiness
Verdict: Simplified and demonstrated for the selected effect sequence. Before production edits, 26 checks passed in the disposable block prototype, 11 in the initial provider boundary test and 19 in the clock/reminder model.
IfNoneMatchpayload writes can recreate deleted content; exactIfMatchpayload writes cannot recreate a deleted object or overwrite its replacement. They exercise both arrival orders and lost upload/delete responses.docs/design/Libraries.Storage-Experiment.mdfor unchangedMicrosoft.Orleans.Persistence.Cosmos 10.2.2-hpk.1conditional persistence: real stale-ETag rejection, changed failed working buffer, duplicate-create rejection, completed-write response loss and fresh reads. Current actor store registration uses the same provider. This does not establish serialization of the new records; build/codecs and current-head tests must cover them.C:/Source/Grace/artifacts/upload-cleanup-owner-preflight-20260910/decision.json, itsrun-d4dee37b28a946beb7a5f2f38e2407b0, andC:/Source/Grace/artifacts/upload-cleanup-readiness-20260910/clock-90190ff298284ed7a71a3adc877805c3. Add a portable checked evidence bundle and concise design report to this PR.Required order:
IfNoneMatch=*. After creating/observing it, successfully persist the exact preparation ETag before any nonempty payload PUT. Payload writes use that exactIfMatchETag. A stale or uncertain state write must abort before follow-on storage effects; never continue from a changed, failed working buffer.Metadata.Noneplus no holds permits a durable deletion reservation. Exclude new holds and metadata acceptance while unresolved. Observe the blob, persist its exact deletion ETag, then conditionally delete. On412, inspect current state; never equate412with absence. If a delayed payload won first, persist the new deletion ETag before retrying. Retain failures for retry.Retry clock and reminders
RetryExpiresAton UploadSession. Remove the independent expiry from its Library binding; keep the existing public Library preparation response'sExpiresAt, populated from the session clock.Acceptance and required failures
Contract propagation and paths
Owned production seams:
src/Grace.Types/{UploadSession,ContentBlockMetadata,Reminder}.Types.fs;src/Grace.Actors/{UploadSession,ContentBlockMetadata,Services,Interfaces,Constants,LibraryTransfer,RepositoryLibrary}.Actor.fs;src/Grace.Server/{Storage,Library}.Server.fs. A small helper file in these existing projects is allowed if it concentrates this same lifecycle; project compile entries may change, with no new project/package.Owned consumers/evidence: focused affected Types/Server.Unit/Server integration tests and their project entries; existing SDK/CLI upload or Library callers only where the same changed DTO/expiry requires propagation; Orleans generated output and existing OpenAPI/client generation surfaces only for this contract. Keep routes and authorization unchanged. Check generated client representation instead of assuming internal types are invisible.
Owned docs:
docs/Operations.md, the upload-cleanup sections ofdocs/adr/0001-content-addressed-storage-contentblocks.md, relevant Library design/contract docs,docs/design/UploadCleanup.Preflight.*, and nearby AGENTS guidance contradicted by the newly approved cleanup behavior. README/CONTRIBUTING only if an advertised command/workflow changes.Forbidden: unrelated branches, worktrees, dirty files and generated outputs; root
undefined/; detachedC:/Source/Grace-worktrees/ops1068and its retained dist; historical Issue #957 worktree including all four dirty files. No reset, clean, prune, or unrelated fixes.Run, validation and completion
fork_turns=none; no nested agents.--no-build. Run meaningful state/codec and provider tests; required GitHub Validate covers the final head. No routine local Fast/Full duplication.Staging fence refinement before the first candidate
Initial implementation inspection found that
GetContentBlockUploadUricould issue a valid SAS at minute 59 while session cleanup ran at minute 60. The supported SDK uses an unconditional PUT with a 60-minute network timeout. A deadline cap fixes a new late arrival but cannot justify assuming an already accepted request was cancelled.Under Scott's instruction to make the necessary decisions and finish the agreed cleanup, extend the demonstrated exact-ETag sequence to staging inside the existing UploadSession owner. This replaces the earlier assumption that the existing staging delete helper alone was sufficient. The base, delivery mode, primary outcome and existing owners remain unchanged.
StoragePlacement.ETagand requires it for a conditional single PUT. Preserve the current route/response shape. Document the new required grant metadata; do not add a fallback for imagined old production grants. Whole-file upload and read grants are unchanged.Services.Actor.fs, CAS upload helper inStorage.SDK.fs, and matching SDK/hosted grant-consumer tests and documentation. This is the same upload publication/cleanup sequence, not a second service, owner or content ledger.The additional disposable provider run passed 15 checks, including four real SAS controls: unconditional valid SAS recreates deleted staging; exact-ETag SAS upload fails after deletion; old ETag fails after replacement; current ETag succeeds. Source/results/receipt:
C:/Source/Grace/artifacts/upload-cleanup-readiness-20260910/run-5efc30c2238e42998bf8946027851f63. This tests storage conditions with actual signed requests, not a suspended in-flight socket or new Grace actor integration. The earlier exact-ETag ordering model supplies the persist-before-grant requirement; current-head tests must exercise its integration.