Skip to content

feat(blob): blob lease support for the Azure Functions storage backplane - #183

Merged
hectorvent merged 6 commits into
floci-io:mainfrom
cmcconomyfwig:feat/durable-storage-backplane
Aug 27, 2026
Merged

feat(blob): blob lease support for the Azure Functions storage backplane#183
hectorvent merged 6 commits into
floci-io:mainfrom
cmcconomyfwig:feat/durable-storage-backplane

Conversation

@cmcconomyfwig

@cmcconomyfwig cmcconomyfwig commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Closes #136

⚠️ Stacked on #182 — this branch includes the Table OData error-envelope commit from #182 (fix(table): wrap error responses in the OData error envelope), because the Durable Functions scenario needs both fixes to work end to end. Please merge #182 first; I'll rebase this branch onto main afterwards so it shows only the blob-lease commit. (If you'd rather take both in one go, merging this PR as-is also lands #182's change verbatim.)

What this is

The storage-binding half of #136. That issue's full ask — durable/timer triggers inside floci-az's own hosted-function runtime — is an architectural feature for the maintainers. But the reason fleets like ours can't use floci-az at all for durable/timer workloads today is narrower: the Azure Functions host runs fine in our own containers, pointed at floci-az as its AzureWebJobsStorage backplane — except that backplane is missing its coordination primitive. Blob leases are not implemented (comp=lease → 501), and Get Blob hardcodes x-ms-lease-state: available.

Blob leases are what the Functions host runs on:

  • WebJobs singleton locks (every timer trigger takes one) are blob lease acquire/renew/release cycles on azure-webjobs-hosts/locks/...
  • Durable Functions partition management acquires, renews, steals (break + acquire), and hands off (change) blob leases
  • All lease-holder writes are lease-conditioned (x-ms-lease-id), which only works if the emulator enforces the guards

Change

  • BlobLease — one lease's state as a pure function of (lease, now): LEASED / EXPIRED / BREAKING / BROKEN. Expiry and break timing never mutate anything, so the state machine is unit-testable with synthetic clocks.
  • BlobLeaseService (@ApplicationScoped, same pattern as UserDelegationKeyService) — Lease Blob dispatch and the write guard. Lease state is an in-memory map: like a real lease it's transient runtime state, and an emulator restart is equivalent to every lease having expired.
  • BlobServiceHandler — routes PUT ?comp=lease (blob-level), enforces the lease guard on every write path (PutBlob, SetBlobMetadata, DeleteBlob, PutBlock, PutBlockList), reports real lease state in Get Blob/Properties, and drops lease state when the blob/container is deleted or reset.

Status/error codes follow the Lease Blob contract:

Scenario Response
acquire on leased blob 409 LeaseAlreadyPresent
re-acquire with same proposed id 201 (idempotent, SDK retry path)
renew/change/release with wrong id 409 LeaseIdMismatchWithLeaseOperation
lease op with no lease 409 LeaseNotPresentWithLeaseOperation
renew after break 409 LeaseIsBrokenAndCannotBeRenewed
acquire duration outside −1 / 15–60 400 InvalidHeaderValue
write to leased blob, no lease id 412 LeaseIdMissing
write to leased blob, wrong id 412 LeaseIdMismatchWithBlobOperation
write with lease id, no lease 412 LeaseNotPresentWithBlobOperation

Deliberately out of scope: container leases (nothing in the Functions host path needs them) — restype=container&comp=lease still 501s honestly rather than half-working.

Tests

  • Unit: BlobLeaseTest (15 HTTP-level tests: lifecycle, error codes, write guards, lease-dies-with-blob) and BlobLeaseStateTest (6 pure state-machine tests: expiry, renew-resets, break windows). All watched fail against main (501s / hardcoded headers) before implementing. Full suite: 575 tests green.
  • Compat (sdk-test-java): 4 new BlobCompatibilityTest cases using the real BlobLeaseClient — lifecycle, write guards, steal-via-break, change handoff. All 4 error against the released 0.10.0 image and pass against this branch.
  • BlobCompDispatchTest: lease removed from the unimplemented-comp list (that guard's job — don't corrupt blobs via putBlob fallthrough — is now covered by the real implementation).

The Java compat pass caught a real cross-SDK bug in my first cut: the acquire response carried an ISO-8601 Last-Modified, which .NET tolerates but the Java SDK parses strictly (RFC1123) and crashes on. Lease responses now echo the blob's actual ETag/Last-Modified in wire format.

Verification against real SDKs

A .NET Azure.Storage.Blobs 12.24 harness (the SDK family the Functions host uses) runs a 12-step sequence — acquire-infinite, props, write-no-lease, write-wrong-lease, write-with-lease, renew, competing acquire, change, break, steal-after-break, props, content — against Azurite 3.35.0 and against this branch. Output is line-for-line identical:

acquire-infinite: 201 ok
props-leased: Locked/Leased/Infinite
write-no-lease: 412 LeaseIdMissing
write-wrong-lease: 412 LeaseIdMismatchWithBlobOperation
write-with-lease: 201 ok
renew: 200 ok
second-acquire-conflict: 409 LeaseAlreadyPresent
change: 200 proposed-id
break: 202 remaining=0
steal-after-break: 201-then-release ok
props-released: Unlocked/Available
content: renewed-content

Next step on our side once this and #182 ship: run a real Durable Functions fleet (fresh start and restart-with-existing-state) against floci-az as AzureWebJobsStorage and report back on #136 with whatever the host surfaces next, if anything.

🤖 Generated with Claude Code

@greptile-apps

greptile-apps Bot commented Aug 2, 2026

Copy link
Copy Markdown

Greptile Summary

The PR adds in-memory Azure Blob lease lifecycle support and lease-conditioned writes for the Azure Functions storage backplane. The latest follow-up commits also serialize related blob and container mutations to close the previously reported races.

  • Implements acquire, renew, change, release, and break operations with lease-state response headers.
  • Enforces lease IDs across blob writes, metadata updates, deletion, block staging, and block commits.
  • Adds state-machine, HTTP-level, and Java SDK compatibility coverage.
  • Moves container and blob mutation preconditions into a shared exclusive section.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains; the previously reported validation, lease atomicity, container lifecycle, and PutBlob authorization races are addressed in the current code.

Important Files Changed

Filename Overview
src/main/java/io/floci/az/services/blob/BlobLease.java Defines the immutable lease state machine and correctly caps break completion at a fixed lease’s natural expiry.
src/main/java/io/floci/az/services/blob/BlobLeaseService.java Implements validated lease transitions, write guards, response headers, and the shared reentrant monitor used to linearize lease and blob mutations.
src/main/java/io/floci/az/services/blob/BlobServiceHandler.java Routes blob lease requests, enforces lease guards, reports lease state, and synchronizes blob/container mutation preconditions with their store updates.
src/test/java/io/floci/az/services/BlobLeaseTest.java Adds HTTP-level coverage for lease lifecycle, protocol errors, guarded writes, and lease cleanup.
src/test/java/io/floci/az/services/blob/BlobLeaseStateTest.java Covers expiry, renewal, break timing, and fixed-lease break bounds with synthetic clocks.
compatibility-tests/sdk-test-java/src/test/java/io/floci/az/compat/BlobCompatibilityTest.java Adds real Azure Java SDK compatibility coverage for lifecycle, write guards, lease stealing, and ownership handoff.

Sequence Diagram

sequenceDiagram
    participant SDK as Azure SDK / Functions Host
    participant Handler as BlobServiceHandler
    participant Lease as BlobLeaseService
    participant Store as StorageBackend
    SDK->>Handler: "PUT blob?comp=lease"
    Handler->>Lease: exclusively()
    Lease->>Store: Verify blob exists
    Lease->>Lease: Validate and transition lease
    Lease-->>SDK: Lease response and headers
    SDK->>Handler: Lease-conditioned blob mutation
    Handler->>Lease: exclusively()
    Lease->>Lease: Validate current lease ID
    Lease->>Store: Apply mutation atomically
    Handler-->>SDK: Azure-compatible response
Loading

Reviews (9): Last reviewed commit: "fix(blob): classify PutBlob authorizatio..." | Re-trigger Greptile

Comment thread src/main/java/io/floci/az/services/blob/BlobLeaseService.java Outdated
Comment thread src/main/java/io/floci/az/services/blob/BlobLeaseService.java
Comment thread src/main/java/io/floci/az/services/blob/BlobLeaseService.java
cmcconomyfwig added a commit to cmcconomyfwig/floci-az that referenced this pull request Aug 3, 2026
Review follow-ups on floci-io#183 (all three findings verified real):

- Guard/mutation atomicity: replace the check-then-act validateWrite
  pattern with BlobLeaseService.guardedWrite(), which runs the lease
  guard AND the store mutation under the same monitor as the (already
  synchronized) lease operations. A competing acquire/break can no
  longer slip between the guard and the write — the interleaving the
  Durable partition-steal path exercises. validateWrite is now private.

- change without x-ms-proposed-lease-id previously stored a lease with
  a null id; the next renew then NPE'd into a 500 with the lease stuck
  forever. renew/change/release now require x-ms-lease-id and change
  requires x-ms-proposed-lease-id (400 MissingRequiredHeader), and
  proposed ids must be GUIDs (400 InvalidHeaderValue) so a malformed id
  can never enter the lease store.

- x-ms-lease-break-period was clamped at 0 and unbounded above,
  allowing a lease stuck breaking arbitrarily long. Out-of-range
  values (outside 0-60) now return 400 InvalidHeaderValue.

Four new regression tests (watched fail first). Full suite 579 green;
Java BlobLeaseClient compat and the .NET Azure.Storage.Blobs harness
re-verified against the rebuilt build (still line-for-line with
Azurite 3.35.0).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@cmcconomyfwig

Copy link
Copy Markdown
Contributor Author

All three findings verified against the code and fixed in f8f336c:

1. Guard/mutation race (TOCTOU). Confirmed: handleLeaseOp was synchronized but the write guard wasn't, so an acquire could land between validateWrite and the store mutation — and the Durable partition-steal path (break + acquire racing the old holder's lease-conditioned writes) is precisely that interleaving. Fixed by replacing the check-then-act pattern with BlobLeaseService.guardedWrite(request, key, mutation), which runs the guard and the mutation under the same monitor as handleLeaseOp. All five write paths (PutBlob, SetBlobMetadata, DeleteBlob, PutBlock, PutBlockList) now go through it; validateWrite is no longer reachable on its own (made private). On the test-coverage note: a deterministic unit test for the interleaving isn't practical without injecting pause points into production code — the fix removes the race by construction (single monitor), which the guardedWrite javadoc states as the invariant.

2. Malformed change/missing headers. Confirmed, and it was worse than flagged: change without x-ms-proposed-lease-id stored a lease with a null id, and the next renew NPE'd into a 500 with the lease permanently stuck. Now: renew/change/release without x-ms-lease-id400 MissingRequiredHeader; change without a proposed id → 400 MissingRequiredHeader; non-GUID proposed ids (acquire and change) → 400 InvalidHeaderValue — so a malformed id can never enter the lease store.

3. Break period validation. Confirmed: negative values were clamped and large values accepted, allowing a lease stuck in breaking for arbitrary time. Now x-ms-lease-break-period outside 0–60 → 400 InvalidHeaderValue, per the Lease Blob contract.

New regression tests (all watched fail before the fix): rejected change leaves the lease intact and renewable, missing-header 400s for all three ops, break period 99999/-1 rejected with the lease still active, malformed proposed id rejected on acquire and change. Suite: 579 tests green; the Java BlobLeaseClient compat tests and the .NET Azure.Storage.Blobs harness (line-for-line vs Azurite 3.35.0) both re-verified against the rebuilt build.

Comment thread src/main/java/io/floci/az/services/blob/BlobServiceHandler.java Outdated
cmcconomyfwig added a commit to cmcconomyfwig/floci-az that referenced this pull request Aug 3, 2026
Review follow-up on floci-io#183: deleteContainer swept blob and lease state
outside the monitor that lease operations and guarded writes hold, so a
concurrent acquire could install a lease for a just-deleted blob (a
recreated blob would inherit it) and a guarded write could re-put blob
data into a deleted container after the sweep.

Generalize guardedWrite into BlobLeaseService.exclusively(): every
mutation of blob or lease state now runs its precondition checks
(container/blob existence, conditional headers, lease guard) AND its
store mutation inside the single lease monitor — putBlob,
setBlobMetadata, deleteBlob, putBlock, putBlockList, leaseBlob's
existence check, and the deleteContainer sweep. This also restores
Azure's natural check order (404 before the lease 412s, which
guardedWrite inverted). The remaining lease-map maintenance hooks
(onBlobDeleted/onContainerDeleted/clear) are synchronized on the same
monitor.

New sequential regression test pins the sweep invariant: lease, delete
container, recreate container+blob, write and re-acquire must succeed.
Full suite 580 green; Java BlobLeaseClient compat and the .NET
Azure.Storage.Blobs harness re-verified (line-for-line with Azurite).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@hectorvent hectorvent added feature blob Azure Blob Storage functions Azure Functions labels Aug 6, 2026
@hectorvent

Copy link
Copy Markdown
Contributor

Thank you for building this out, and for flagging the stack on #182 in the description yourself, that made the shared history easy to follow.

(blocking) Commits db24941, b6e0788, f8f336c and 7ca231f carry an AI co-author trailer; this repo keeps commit attribution with human contributors (see AGENTS.md). Would you amend or squash those and force push so the attribution is yours alone?

(blocking) This PR also edits CHANGELOG.md, which became generated by semantic release shortly after you opened it, so hand edits conflict with the release commit. Would you drop that hunk? Your change is still credited automatically.

Heads up before you rebase: we merged #166 and #134 today, so this now conflicts on BlobServiceHandler.java as well as two test files. That is our doing, sorry for the churn. Also the description says "Refs #136", so the issue will not auto close; "Closes #136" would wire that up.

cmcconomyfwig added a commit to cmcconomyfwig/floci-az that referenced this pull request Aug 13, 2026
Review follow-ups on floci-io#183 (all three findings verified real):

- Guard/mutation atomicity: replace the check-then-act validateWrite
  pattern with BlobLeaseService.guardedWrite(), which runs the lease
  guard AND the store mutation under the same monitor as the (already
  synchronized) lease operations. A competing acquire/break can no
  longer slip between the guard and the write — the interleaving the
  Durable partition-steal path exercises. validateWrite is now private.

- change without x-ms-proposed-lease-id previously stored a lease with
  a null id; the next renew then NPE'd into a 500 with the lease stuck
  forever. renew/change/release now require x-ms-lease-id and change
  requires x-ms-proposed-lease-id (400 MissingRequiredHeader), and
  proposed ids must be GUIDs (400 InvalidHeaderValue) so a malformed id
  can never enter the lease store.

- x-ms-lease-break-period was clamped at 0 and unbounded above,
  allowing a lease stuck breaking arbitrarily long. Out-of-range
  values (outside 0-60) now return 400 InvalidHeaderValue.

Four new regression tests (watched fail first). Full suite 579 green;
Java BlobLeaseClient compat and the .NET Azure.Storage.Blobs harness
re-verified against the rebuilt build (still line-for-line with
Azurite 3.35.0).
cmcconomyfwig added a commit to cmcconomyfwig/floci-az that referenced this pull request Aug 13, 2026
Review follow-up on floci-io#183: deleteContainer swept blob and lease state
outside the monitor that lease operations and guarded writes hold, so a
concurrent acquire could install a lease for a just-deleted blob (a
recreated blob would inherit it) and a guarded write could re-put blob
data into a deleted container after the sweep.

Generalize guardedWrite into BlobLeaseService.exclusively(): every
mutation of blob or lease state now runs its precondition checks
(container/blob existence, conditional headers, lease guard) AND its
store mutation inside the single lease monitor — putBlob,
setBlobMetadata, deleteBlob, putBlock, putBlockList, leaseBlob's
existence check, and the deleteContainer sweep. This also restores
Azure's natural check order (404 before the lease 412s, which
guardedWrite inverted). The remaining lease-map maintenance hooks
(onBlobDeleted/onContainerDeleted/clear) are synchronized on the same
monitor.

New sequential regression test pins the sweep invariant: lease, delete
container, recreate container+blob, write and re-acquire must succeed.
Full suite 580 green; Java BlobLeaseClient compat and the .NET
Azure.Storage.Blobs harness re-verified (line-for-line with Azurite).
@cmcconomyfwig
cmcconomyfwig force-pushed the feat/durable-storage-backplane branch 2 times, most recently from 7ca231f to 8cd84e7 Compare August 13, 2026 15:11
@cmcconomyfwig

Copy link
Copy Markdown
Contributor Author

Both blocking items addressed, and the stack is rebased onto current main:

On Refs #136 vs Closes #136: that was deliberate rather than an oversight — this PR is only the storage-binding half. #136's actual ask (durable/timer triggers inside floci-az's own hosted-function runtime) is still open even with leases in place, and the plan in the description is to run a real Durable fleet against this backplane and report findings back on that issue. If you'd rather treat BYO-host-against-the-backplane as resolving #136, say the word and I'll flip it to Closes.

@hectorvent

Copy link
Copy Markdown
Contributor

Thank you, and apologies this took me a while to review properly.

I have now gone through the lease behaviour against the blob spec and it checks out across the board: acquire returns 201, renew, change and release return 200, and break returns 202, which is exactly what the spec declares for each. The duration validation matches too, 15 to 60 seconds or -1 for infinite, and rejecting outside that with InvalidHeaderValue. Modelling the state machine explicitly, including refusing a renew after an explicit break, is the part I would most expect to be missed.

Two mechanical things left, neither about the lease work:

(blocking) We merged #185 today and it conflicts this branch on TableServiceTest.java, one file. That churn is ours.

The red check is the same node table test as #182, which resolves once that one is updated. Small thing: the description says "Refs #136", so the issue will not auto close; "Closes #136" would wire it up.

cmcconomyfwig added a commit to cmcconomyfwig/floci-az that referenced this pull request Aug 18, 2026
Review follow-ups on floci-io#183 (all three findings verified real):

- Guard/mutation atomicity: replace the check-then-act validateWrite
  pattern with BlobLeaseService.guardedWrite(), which runs the lease
  guard AND the store mutation under the same monitor as the (already
  synchronized) lease operations. A competing acquire/break can no
  longer slip between the guard and the write — the interleaving the
  Durable partition-steal path exercises. validateWrite is now private.

- change without x-ms-proposed-lease-id previously stored a lease with
  a null id; the next renew then NPE'd into a 500 with the lease stuck
  forever. renew/change/release now require x-ms-lease-id and change
  requires x-ms-proposed-lease-id (400 MissingRequiredHeader), and
  proposed ids must be GUIDs (400 InvalidHeaderValue) so a malformed id
  can never enter the lease store.

- x-ms-lease-break-period was clamped at 0 and unbounded above,
  allowing a lease stuck breaking arbitrarily long. Out-of-range
  values (outside 0-60) now return 400 InvalidHeaderValue.

Four new regression tests (watched fail first). Full suite 579 green;
Java BlobLeaseClient compat and the .NET Azure.Storage.Blobs harness
re-verified against the rebuilt build (still line-for-line with
Azurite 3.35.0).
cmcconomyfwig added a commit to cmcconomyfwig/floci-az that referenced this pull request Aug 18, 2026
Review follow-up on floci-io#183: deleteContainer swept blob and lease state
outside the monitor that lease operations and guarded writes hold, so a
concurrent acquire could install a lease for a just-deleted blob (a
recreated blob would inherit it) and a guarded write could re-put blob
data into a deleted container after the sweep.

Generalize guardedWrite into BlobLeaseService.exclusively(): every
mutation of blob or lease state now runs its precondition checks
(container/blob existence, conditional headers, lease guard) AND its
store mutation inside the single lease monitor — putBlob,
setBlobMetadata, deleteBlob, putBlock, putBlockList, leaseBlob's
existence check, and the deleteContainer sweep. This also restores
Azure's natural check order (404 before the lease 412s, which
guardedWrite inverted). The remaining lease-map maintenance hooks
(onBlobDeleted/onContainerDeleted/clear) are synchronized on the same
monitor.

New sequential regression test pins the sweep invariant: lease, delete
container, recreate container+blob, write and re-acquire must succeed.
Full suite 580 green; Java BlobLeaseClient compat and the .NET
Azure.Storage.Blobs harness re-verified (line-for-line with Azurite).
@cmcconomyfwig
cmcconomyfwig force-pushed the feat/durable-storage-backplane branch from 8cd84e7 to 8370b32 Compare August 18, 2026 14:43
@cmcconomyfwig

Copy link
Copy Markdown
Contributor Author

Rebased onto current main and force-pushed. Two things worth flagging from the rebase:

Full unit suite green after the rebase: 748 tests, 0 failures. The red node check resolves via #182's test update, verified 11/11 locally against a running emulator.

The description now says Closes #136.

Comment thread src/main/java/io/floci/az/services/blob/BlobLeaseService.java
@hectorvent hectorvent added waiting-maintainer Contributor has answered; the review owes a reply and removed waiting-contributor labels Aug 20, 2026
@hectorvent

Copy link
Copy Markdown
Contributor

Thank you, everything from my last note is addressed: the rebase over #185 and #158, the Closes wiring, and the stack on #182 staying aligned. I also went through your three new commits and they hold up; capping the break period at a fixed lease's natural expiry is exactly what the spec prescribes, and running the authorization checks ahead of the lease monitor matches real Azure's error precedence (403 before 412).

One thing on my side, and it is ours not yours: the #198 merge (ADLS list paths) landed in BlobServiceHandler.java, which conflicts with this branch. That is the third time our merges have broken you here, sorry. The checks on your current head are running now and everything reviewed looks right, so after a rebase onto main and green checks this merges.

@hectorvent hectorvent added waiting-contributor and removed waiting-maintainer Contributor has answered; the review owes a reply labels Aug 20, 2026
Implement Lease Blob (comp=lease) — acquire, renew, change, release,
break — with real lease state reported by Get Blob/Properties and
lease-conditioned enforcement on every blob write path (PutBlob,
SetBlobMetadata, DeleteBlob, PutBlock, PutBlockList):

  - active lease + no x-ms-lease-id      -> 412 LeaseIdMissing
  - active lease + wrong id              -> 412 LeaseIdMismatchWithBlobOperation
  - no active lease + id supplied        -> 412 LeaseNotPresentWithBlobOperation
  - competing acquire                    -> 409 LeaseAlreadyPresent
  - renew/change/release id mismatch     -> 409 LeaseIdMismatchWithLeaseOperation
  - lease op with no lease               -> 409 LeaseNotPresentWithLeaseOperation
  - renew after break                    -> 409 LeaseIsBrokenAndCannotBeRenewed
  - acquire duration outside -1/15-60    -> 400 InvalidHeaderValue

Blob leases are the coordination primitive of the Azure Functions host:
WebJobs singleton and timer-trigger locks and Durable Functions
partition management are all blob leases. Together with the Table
OData error fix (floci-io#182), this lets a self-hosted Functions fleet point
AzureWebJobsStorage at floci-az for durable/timer/queue-triggered
workloads — the storage-binding side of floci-io#136. (Emulating those
triggers inside floci-az's own hosted-function runtime remains open.)

Design: lease state is an in-memory map in BlobLeaseService (an
emulator restart equals all leases expiring, matching lease semantics);
the BlobLease record is a pure function of (lease, now) so expiry and
break timing are unit-testable. Container leases stay unimplemented.

Verified: unit suite (BlobLeaseTest, BlobLeaseStateTest) red->green;
Java SDK compat (BlobLeaseClient lifecycle, write guards, steal-via-
break, change handoff) fails on 0.10.0 and passes here; a .NET
Azure.Storage.Blobs harness runs the same sequence against Azurite
3.35.0 and this build with line-for-line identical output.

Refs floci-io#136
Review follow-ups on floci-io#183 (all three findings verified real):

- Guard/mutation atomicity: replace the check-then-act validateWrite
  pattern with BlobLeaseService.guardedWrite(), which runs the lease
  guard AND the store mutation under the same monitor as the (already
  synchronized) lease operations. A competing acquire/break can no
  longer slip between the guard and the write — the interleaving the
  Durable partition-steal path exercises. validateWrite is now private.

- change without x-ms-proposed-lease-id previously stored a lease with
  a null id; the next renew then NPE'd into a 500 with the lease stuck
  forever. renew/change/release now require x-ms-lease-id and change
  requires x-ms-proposed-lease-id (400 MissingRequiredHeader), and
  proposed ids must be GUIDs (400 InvalidHeaderValue) so a malformed id
  can never enter the lease store.

- x-ms-lease-break-period was clamped at 0 and unbounded above,
  allowing a lease stuck breaking arbitrarily long. Out-of-range
  values (outside 0-60) now return 400 InvalidHeaderValue.

Four new regression tests (watched fail first). Full suite 579 green;
Java BlobLeaseClient compat and the .NET Azure.Storage.Blobs harness
re-verified against the rebuilt build (still line-for-line with
Azurite 3.35.0).
Review follow-up on floci-io#183: deleteContainer swept blob and lease state
outside the monitor that lease operations and guarded writes hold, so a
concurrent acquire could install a lease for a just-deleted blob (a
recreated blob would inherit it) and a guarded write could re-put blob
data into a deleted container after the sweep.

Generalize guardedWrite into BlobLeaseService.exclusively(): every
mutation of blob or lease state now runs its precondition checks
(container/blob existence, conditional headers, lease guard) AND its
store mutation inside the single lease monitor — putBlob,
setBlobMetadata, deleteBlob, putBlock, putBlockList, leaseBlob's
existence check, and the deleteContainer sweep. This also restores
Azure's natural check order (404 before the lease 412s, which
guardedWrite inverted). The remaining lease-map maintenance hooks
(onBlobDeleted/onContainerDeleted/clear) are synchronized on the same
monitor.

New sequential regression test pins the sweep invariant: lease, delete
container, recreate container+blob, write and re-acquire must succeed.
Full suite 580 green; Java BlobLeaseClient compat and the .NET
Azure.Storage.Blobs harness re-verified (line-for-line with Azurite).
Break Lease applies x-ms-lease-break-period only when it is shorter than
the lease's remaining time, but broken() stamped breakAt unconditionally
at now + period. A fixed lease with less time remaining than the
requested period therefore sat in BREAKING — rejecting reacquisition —
past the moment it should have expired, since stateAt() checks breakAt
before expiresAt.

Cap breakAt at expiresAt in broken(). The no-header default path already
derived the period from the remaining time; the explicit-header path now
honors the same bound. State-machine tests pin both directions: a 60s
break on a lease with 5s left completes at natural expiry, and a period
shorter than the remaining time is used as given.
@cmcconomyfwig
cmcconomyfwig force-pushed the feat/durable-storage-backplane branch from 3180591 to 7418904 Compare August 20, 2026 15:34
@cmcconomyfwig

Copy link
Copy Markdown
Contributor Author

Rebased onto current main and force-pushed. With #182 merged, the table commit dropped out of the stack, so the PR now shows only the four blob-lease commits. The #198 conflict was in putBlob — its DataLakeResourceType metadata now rides inside the lease-guarded mutation — and the new resource=filesystem list route is untouched by this branch. The lease code and tests are byte-identical to what you reviewed; the only movement was weaving #198 in.

Full unit suite green on the rebased head: 789 tests, 0 failures.

Comment thread src/main/java/io/floci/az/services/blob/BlobServiceHandler.java
createContainer checked and wrote the namespace sentinel outside the
monitor that deleteContainer sweeps under, so a create that observed
absence could re-put the sentinel after a concurrent deletion sweep —
resurrecting the container after DELETE answered 202 — and two
concurrent creates could both answer 201 where the contract gives one
ContainerAlreadyExists. ensureContainer (ARM provisioning) had the same
unsynchronized put.

Run both under the lease monitor via a void exclusively(Runnable)
overload, and move the admin-reset sweep (clear) under it too — the
last mutation outside the monitor — so the invariant stated in
exclusively()'s javadoc holds everywhere. Sequential behavior is
unchanged; the interleaving is eliminated by construction and the
existing container lifecycle tests pin the sequential contract.
Comment thread src/main/java/io/floci/az/services/blob/BlobServiceHandler.java
PutBlob chooses between create and write SAS authorization from blob
existence, but that read sat outside the monitor while the mutation
re-read state inside it. A create-only SAS (sp=c without w) that
observed absence could keep CREATE authorization and overwrite a blob a
concurrent request had just created, destroying its data.

Move the classification inside exclusively(), driven by the same
existing snapshot the mutation uses — one read now feeds authorization,
the conditional-header checks, and the creation-time stamp. Sequential
precedence is unchanged (403 before the container 404, then conditions,
then the lease guard), pinned by the existing create-only SAS overwrite
test; the body read stays outside the monitor so no I/O runs under the
lock, and the authorize checks are pure token computation.
@hectorvent hectorvent added waiting-maintainer Contributor has answered; the review owes a reply and removed waiting-contributor labels Aug 20, 2026
@hectorvent

Copy link
Copy Markdown
Contributor

Thank you for the quick turnaround, and for owning the two Greptile findings rather than arguing them. I checked both fixes and they are right: creating the container and classifying the PutBlob authorization now read the same snapshot the mutation uses, under the same monitor, and the body read stays outside the lock.

I also confirmed your claim the easy way: the four lease files are byte identical to the head I reviewed, so the earlier grounding against the blob spec (201 acquire, 200 renew, change and release, 202 break, 15 to 60 second durations, 0 to 60 second break period) still stands, and the #198 metadata hunk now rides inside the guarded write as it should.

All eleven checks are green on this head, including every compat suite. No blockers from my side, merging now. Thank you for seeing this one through three rebases that were our fault, not yours.

@hectorvent hectorvent added waiting-contributor and removed waiting-maintainer Contributor has answered; the review owes a reply labels Aug 26, 2026
@cmcconomyfwig

Copy link
Copy Markdown
Contributor Author

Excited to try the new release once it's ready, so I can remove some external container dependencies for azure!

@hectorvent
hectorvent merged commit e82139f into floci-io:main Aug 27, 2026
11 checks passed
@cmcconomyfwig
cmcconomyfwig deleted the feat/durable-storage-backplane branch August 31, 2026 12:53
@hectorvent

Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 0.12.0 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

thomhurst pushed a commit to thomhurst/floci-az that referenced this pull request Sep 1, 2026
# [0.12.0](floci-io/floci-az@0.11.0...0.12.0) (2026-09-01)

### Bug Fixes

* **blob:** default unusable blob content types to application/octet-stream ([floci-io#210](floci-io#210)) ([4fdbb7a](floci-io@4fdbb7a))
* **blob:** honor user delegation SAS signing layouts by version ([floci-io#202](floci-io#202)) ([8ca73e8](floci-io@8ca73e8))
* **blob:** preserve ADLS list directory identity ([floci-io#201](floci-io#201)) ([2a3635b](floci-io@2a3635b))
* **cosmos:** enable .NET SDK queries ([floci-io#220](floci-io#220)) ([8bde4d5](floci-io@8bde4d5)), closes [floci-io#216](floci-io#216)
* **cosmos:** support .NET transactional batch ([floci-io#222](floci-io#222)) ([5efd630](floci-io@5efd630)), closes [floci-io#217](floci-io#217)
* **eventhub:** make the AMQP data plane usable from the Azure SDKs (CBS + library patches) ([floci-io#237](floci-io#237)) ([5c8806e](floci-io@5c8806e)), closes [floci-io#128](floci-io#128)
* **eventhub:** route sends addressed the way the Azure SDKs address them ([floci-io#241](floci-io#241)) ([0d04662](floci-io@0d04662))
* **functions:** support Python v2 app-root packages ([floci-io#200](floci-io#200)) ([09d6a7c](floci-io@09d6a7c))
* honor Cosmos patch filter predicates ([floci-io#230](floci-io#230)) ([10dd22c](floci-io@10dd22c)), closes [floci-io#217](floci-io#217)
* honor X-Forwarded-Proto in /metadata/endpoints so URLs are https behind a TLS proxy ([floci-io#253](floci-io#253)) ([37aa829](floci-io@37aa829)), closes [floci-io#252](floci-io#252)
* return ARM errors for failed SQL operations ([floci-io#229](floci-io#229)) ([7519c7d](floci-io@7519c7d)), closes [floci-io#138](floci-io#138)
* **servicebus:** reap orphaned sidecars ([floci-io#219](floci-io#219)) ([c9f3915](floci-io@c9f3915)), closes [floci-io#218](floci-io#218)
* **servicebus:** report runtime counts ([floci-io#225](floci-io#225)) ([e869661](floci-io@e869661)), closes [floci-io#214](floci-io#214)
* **servicebus:** route .NET admin requests ([floci-io#221](floci-io#221)) ([c0e003f](floci-io@c0e003f)), closes [floci-io#215](floci-io#215)
* **servicebus:** support session DLQ access ([floci-io#223](floci-io#223)) ([5036cad](floci-io@5036cad))
* **servicebus:** unpack AMQP batches ([floci-io#224](floci-io#224)) ([bc01cee](floci-io@bc01cee))
* **table:** wrap error responses in the OData error envelope ([floci-io#182](floci-io#182)) ([96a0f6a](floci-io@96a0f6a)), closes [floci-io#181](floci-io#181)

### Features

* **aci:** add Azure Container Instances emulation (mocked ARM CRUD) ([floci-io#208](floci-io#208)) ([1377f74](floci-io@1377f74)), closes [floci-io#59](floci-io#59)
* add ADLS list paths support ([floci-io#198](floci-io#198)) ([7c47687](floci-io@7c47687))
* **blob:** blob lease support for the Azure Functions storage backplane ([floci-io#183](floci-io#183)) ([e82139f](floci-io@e82139f)), closes [floci-io#182](floci-io#182) [floci-io#136](floci-io#136)
* **compatibility-tests:** add Azure SDK for C++ compatibility suite ([floci-io#186](floci-io#186)) ([3c69159](floci-io@3c69159)), closes [floci-io#184](floci-io#184)
* **servicebus:** add duplicate detection ([floci-io#171](floci-io#171)) ([413a65a](floci-io@413a65a))
* **servicebus:** add entity message expiration ([floci-io#172](floci-io#172)) ([0648e1a](floci-io@0648e1a))
* **servicebus:** honor per-entity MaxDeliveryCount and LockDuration ([floci-io#207](floci-io#207)) ([4d39b93](floci-io@4d39b93))
* **servicebus:** start the default namespace on boot for a deterministic AMQP endpoint ([floci-io#250](floci-io#250)) ([69d3600](floci-io@69d3600))
* **servicebus:** support non-destructive message peeking ([floci-io#203](floci-io#203)) ([93f3b6d](floci-io@93f3b6d)), closes [floci-io#137](floci-io#137) [floci-io#137](floci-io#137)
* **sql:** add data-plane providers ([floci-io#204](floci-io#204)) ([d789831](floci-io@d789831)), closes [floci-io#138](floci-io#138)
* **sql:** provision managed servers async ([floci-io#205](floci-io#205)) ([aff2aca](floci-io@aff2aca)), closes [floci-io#138](floci-io#138)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEAT] Azure Function Storage-driven bindings (Durable, Timer)

2 participants