examples: prove the at-least-once clause at a sink - #51
Merged
Conversation
The correctness doc promises at-least-once execution and tells external systems to hold stable idempotency keys, but nothing in the repository showed that duplicate arriving anywhere. A contract clause nobody can watch fire is decoration. This adds the artifact that fires it on demand and shows the documented remedy absorbing it. examples/at_least_once stages one actor turn whose effect writes to an external sink file. The first effect worker crashes between the sink write and the acknowledgement. A second worker runs ProcessRegistry.cleanup_dead after the liveness threshold, reclaims the released effect, and delivers again. With deduplication off the sink reads 2, both deliveries carrying the same context.id at attempts 1 and 2. With a guard on that id the sink reads 1. The actor state commits exactly once in both runs, which is the sharpest line of the proof: the state machine kept its exactly-once story while the outside world saw two. bundle exec rake at_least_once runs it, CI runs it in the SQLite job, and docs/correctness.md links it from the handler idempotency section it makes observable. The sink module carries unit coverage for both guard modes. This is the Ruby counterpart of solid-objects-js#26.
Owner
Author
|
CI reproduced the proof on Ubuntu in the sqlite job, so the crash timing is not a macOS artifact: sqlite job log. All checks pass. |
Greptile flagged the JS counterpart for reading every failure as an empty sink, which lets the deduplication phase forget the effect id and still pass. The Ruby sink already rescues only Errno::ENOENT, and the Ruby effect worker already fails when no effect became claimable, so neither defect exists here. Both properties were incidental until now. These two tests make them deliberate: a truncated JSON file and a path that is a directory must raise rather than read as a first run. Widening the rescue to StandardError fails both.
Owner
Author
|
Greptile reviewed the JS counterpart (cardmagic/solid-objects-js#26) and raised two P2 findings. I checked both against this PR:
Both properties were incidental rather than pinned, so b8ea47a adds two tests: a truncated JSON file and a path that is a directory must raise instead of reading as a first run. Widening the rescue to Restored, the file reads 5 runs, 9 assertions, 0 failures. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
docs/correctness.mdpromises at-least-once execution and tells externalsystems to hold stable idempotency keys. Nothing in the repository showed that
duplicate arriving anywhere. A contract clause nobody can watch fire is
decoration, so this adds the artifact that makes it fire on demand, and shows
the documented remedy absorbing it.
This is the Ruby counterpart of
solid-objects-js#26,
which implements
solid-objects-js#23.
Same proof, same shape, same numbers.
No runtime code changes. This is an example, a unit test, a CI step, and two
lines of prose.
What the demo does
bundle exec rake at_least_oncerunsexamples/at_least_once/demo.rbagainsta temporary SQLite file:
countand stages arecordeffect in the samecommit.
file, then calls
Process.exit!(1)between the sink write and theacknowledgement. The sink holds the delivery; the effect row never
completes.
SolidObjects::ProcessRegistry.cleanup_dead(production runs this ondead_process_cleanup_interval), reclaims the released effect, and deliversagain.
Phase one runs the sink with deduplication off. Phase two repeats the same
crash with a guard on the stable effect id.
The state commit happens exactly once in both phases. That is the sharpest
line of the proof: the actor kept its exactly-once story while the outside
world saw the delivery twice, and only a consumer-side guard on
context.idcollapsed it.
The crash point
context.idis stable across attempts andcontext.attemptreads 1 then 2,which is exactly what an external consumer needs in order to deduplicate.
Observed failures
The sink test was written first. With
examples/at_least_once/sink.rbabsent,bundle exec rake test TEST=test/unit/at_least_once_sink_test.rb:A missing file is a weak failure, so the guard was also removed from a
complete sink. The unit test then failed on the assertion under test rather
than on load:
The demo is not self-fulfilling either. With the same guard removed,
bundle exec rake at_least_oncefails at the remedy phase:Restoring the guard turns all three green.
Files
examples/at_least_once/sink.rbexamples/at_least_once/actor.rbDeliveryCounter#deliverincrements and emitsrecordexamples/at_least_once/boot.rbexamples/at_least_once/effect_worker.rbexamples/at_least_once/demo.rbtest/unit/at_least_once_sink_test.rbWiring
bundle exec rake at_least_onceruns it.bundle exec rake.docs/correctness.mdlinks it from the handler idempotency section it makesobservable.
CHANGELOG.mdrecords it under Unreleased.docs/roadmap.mdis untouched. The gem's claims about itself did not move;this makes an existing claim observable.
Effects
authorization inside its own process.
Validation
bundle exec rake(test, standard, rubocop, rbs, steep, security): pass.bundle exec rake at_least_once: four consecutive clean runs locally.