Skip to content

S18.03: Dispose-on-empty block lifecycle #236

Description

@DavidCozens

Parent epic: #112 (E18: Flash Storage Support)

Scope

Behavioural change: blocks are disposed eagerly when their last record is MarkSent, rather than waiting for capacity pressure. Combined with an empty-check on Acquire, this gives flash drivers their preferred "erase-then-write" pattern under normal workloads instead of clustering erases at capacity boundaries, and provides crash-mid-write recovery.

This story also plumbs the success/failure result of BlockDevice_Acquire / BlockDevice_Dispose through BlockSequence_RotateToNextBlock / _DiscardOldestBlock, so a transient device-mutation failure does not leave the in-memory ring out of sync with on-disk state — see deferred concern from #243 below.

Behaviour changes:

  • Dispose-on-empty. When MarkSent advances the read cursor past the last record in a non-active read block (read block ≠ write block), BlockSequence disposes that block immediately. The active write block is never disposed by this path.
  • Acquire empty-check. When BlockSequence acquires a block, the BlockDevice asserts the block is empty. If stale content is found (e.g. from a power loss mid-Append before this story shipped), the block is disposed and re-acquired.
  • Success-only state advancement. RotateToNextBlock only publishes the new writeSequence / writePosition after Acquire returns success. DiscardOldestBlock only advances oldestSequence after Dispose returns success. On failure, sequence state stays unchanged so the next attempt starts from a coherent ring.

Acceptance criteria

BDD scenarios

  • Eager dispose on drain — Given a store with messages spanning two blocks, When all records in the older block are MarkSent, Then GetUsedBytes drops by one block size and the block is disposed.
  • Active block not disposed — Given the only block with unsent records is the active write block, When all its records are MarkSent, Then the block is NOT disposed (still active for writes).
  • Acquire disposes stale content — Given a block left non-empty by a previous (simulated crashed) run, When the store next acquires it, Then it is disposed and acquired empty.
  • Acquire failure leaves ring intact — Given an Acquire that fails (e.g. injected disk-full), When rotation runs, Then writeSequence / writePosition are unchanged and a subsequent successful Acquire rotates as if the failed call had never happened.
  • Dispose failure leaves ring intact — Given a Dispose that fails, When discard runs, Then oldestSequence is unchanged and the failed block remains in the ring (re-attempted on the next discard cycle).
  • No regression — pre-existing BDD scenarios remain green; capacity-threshold scenarios from S05.09: Configurable capacity threshold alert #111 still fire correctly under the new dispose pattern.

Docs and header surface

  • No public API changes — purely behavioural.
  • Behavioural change called out in any user-facing docs that describe storage retention timing.

Out of scope

  • Example flash block device (S18.04).
  • Wear-levelling counters (defer to flash driver).

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    storyStory issue

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions