persist: optimistic concurrency — conflict as a real result + 8 tests
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
concurrency.sx: persist/append-expect refuses an append when the stream
advanced past the caller's expected seq, returning {:conflict :expected
:actual} instead of crashing or overwriting. persist/conflict? + accessors.
Phase 2 complete, 54/54.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
24
lib/persist/concurrency.sx
Normal file
24
lib/persist/concurrency.sx
Normal file
@@ -0,0 +1,24 @@
|
||||
; persist/concurrency — optimistic concurrency for the log facet. The caller
|
||||
; passes the seq it believes is current (the last-seq it last observed). If the
|
||||
; stream has advanced since, the append is refused and a conflict VALUE is
|
||||
; returned — never a crash, never a silent overwrite. The caller re-reads the
|
||||
; tail and retries. This is the substrate-level answer to "two writers, one
|
||||
; stream": the loser gets a result it can act on.
|
||||
; Requires: lib/persist/log.sx.
|
||||
|
||||
(define
|
||||
persist/append-expect
|
||||
(fn
|
||||
(b stream expected type at data)
|
||||
(let
|
||||
((actual (persist/last-seq b stream)))
|
||||
(if
|
||||
(= actual expected)
|
||||
(persist/append b stream type at data)
|
||||
{:actual actual :expected expected :conflict true}))))
|
||||
|
||||
(define
|
||||
persist/conflict?
|
||||
(fn (r) (if (has-key? r :conflict) (get r :conflict) false)))
|
||||
(define persist/conflict-expected (fn (r) (get r :expected)))
|
||||
(define persist/conflict-actual (fn (r) (get r :actual)))
|
||||
@@ -13,7 +13,7 @@ if [ ! -x "$SX_SERVER" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SUITES=(event log kv project subscribe)
|
||||
SUITES=(event log kv project subscribe concurrency)
|
||||
|
||||
OUT_JSON="lib/persist/scoreboard.json"
|
||||
OUT_MD="lib/persist/scoreboard.md"
|
||||
@@ -32,6 +32,7 @@ run_suite() {
|
||||
(load "lib/persist/log.sx")
|
||||
(load "lib/persist/kv.sx")
|
||||
(load "lib/persist/project.sx")
|
||||
(load "lib/persist/concurrency.sx")
|
||||
(load "lib/persist/subscribe.sx")
|
||||
(load "lib/persist/api.sx")
|
||||
(epoch 2)
|
||||
|
||||
@@ -4,9 +4,10 @@
|
||||
"log": {"pass": 9, "fail": 0},
|
||||
"kv": {"pass": 13, "fail": 0},
|
||||
"project": {"pass": 9, "fail": 0},
|
||||
"subscribe": {"pass": 9, "fail": 0}
|
||||
"subscribe": {"pass": 9, "fail": 0},
|
||||
"concurrency": {"pass": 8, "fail": 0}
|
||||
},
|
||||
"total_pass": 46,
|
||||
"total_pass": 54,
|
||||
"total_fail": 0,
|
||||
"total": 46
|
||||
"total": 54
|
||||
}
|
||||
|
||||
@@ -9,4 +9,5 @@ _Generated by `lib/persist/conformance.sh`_
|
||||
| kv | 13 | 0 | 13 |
|
||||
| project | 9 | 0 | 9 |
|
||||
| subscribe | 9 | 0 | 9 |
|
||||
| **Total** | **46** | **0** | **46** |
|
||||
| concurrency | 8 | 0 | 8 |
|
||||
| **Total** | **54** | **0** | **54** |
|
||||
|
||||
96
lib/persist/tests/concurrency.sx
Normal file
96
lib/persist/tests/concurrency.sx
Normal file
@@ -0,0 +1,96 @@
|
||||
; Phase 2 — optimistic concurrency: conflict is a real result, not a crash.
|
||||
|
||||
(persist-test
|
||||
"append-expect 0 on empty stream succeeds"
|
||||
(persist/event-seq
|
||||
(persist/append-expect
|
||||
(persist/open)
|
||||
"s"
|
||||
0
|
||||
"x"
|
||||
0
|
||||
{}))
|
||||
1)
|
||||
(persist-test
|
||||
"append-expect with correct seq succeeds"
|
||||
(let
|
||||
((b (persist/open)))
|
||||
(begin
|
||||
(persist/append b "s" "x" 0 {})
|
||||
(persist/event-seq
|
||||
(persist/append-expect b "s" 1 "x" 0 {}))))
|
||||
2)
|
||||
(persist-test
|
||||
"append-expect with stale seq returns a conflict"
|
||||
(let
|
||||
((b (persist/open)))
|
||||
(begin
|
||||
(persist/append b "s" "x" 0 {})
|
||||
(persist/append b "s" "x" 0 {})
|
||||
(persist/conflict?
|
||||
(persist/append-expect b "s" 1 "x" 0 {}))))
|
||||
true)
|
||||
(persist-test
|
||||
"a successful append is not a conflict"
|
||||
(persist/conflict?
|
||||
(persist/append-expect
|
||||
(persist/open)
|
||||
"s"
|
||||
0
|
||||
"x"
|
||||
0
|
||||
{}))
|
||||
false)
|
||||
(persist-test
|
||||
"conflict carries expected and actual"
|
||||
(let
|
||||
((b (persist/open)))
|
||||
(begin
|
||||
(persist/append b "s" "x" 0 {})
|
||||
(persist/append b "s" "x" 0 {})
|
||||
(let
|
||||
((r (persist/append-expect b "s" 0 "x" 0 {})))
|
||||
(list (persist/conflict-expected r) (persist/conflict-actual r)))))
|
||||
(list 0 2))
|
||||
(persist-test
|
||||
"a conflicting append does not write"
|
||||
(let
|
||||
((b (persist/open)))
|
||||
(begin
|
||||
(persist/append b "s" "x" 0 {})
|
||||
(persist/append-expect b "s" 0 "x" 0 {})
|
||||
(persist/count b "s")))
|
||||
1)
|
||||
(persist-test
|
||||
"two writers: first wins, second conflicts"
|
||||
(let
|
||||
((b (persist/open)))
|
||||
(let
|
||||
((seen (persist/last-seq b "s")))
|
||||
(begin
|
||||
(persist/append-expect b "s" seen "x" 0 {:who "A"})
|
||||
(persist/conflict?
|
||||
(persist/append-expect b "s" seen "x" 0 {:who "B"})))))
|
||||
true)
|
||||
(persist-test
|
||||
"retry after conflict succeeds"
|
||||
(let
|
||||
((b (persist/open)))
|
||||
(let
|
||||
((seen (persist/last-seq b "s")))
|
||||
(begin
|
||||
(persist/append-expect b "s" seen "x" 0 {:who "A"})
|
||||
(let
|
||||
((r (persist/append-expect b "s" seen "x" 0 {:who "B"})))
|
||||
(if
|
||||
(persist/conflict? r)
|
||||
(persist/event-seq
|
||||
(persist/append-expect
|
||||
b
|
||||
"s"
|
||||
(persist/conflict-actual r)
|
||||
"x"
|
||||
0
|
||||
{:who "B"}))
|
||||
(persist/event-seq r))))))
|
||||
2)
|
||||
Reference in New Issue
Block a user