Skip to content

Commit 6c75b0f

Browse files
committed
Close completion audit gaps
1 parent 45efb87 commit 6c75b0f

6 files changed

Lines changed: 168 additions & 23 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ states/
2929
jepsen/target/
3030
jepsen/.lein-failures
3131
jepsen/store/
32+
kvnode-data/
3233

3334
# env file
3435
.env

MODEL_EQ_REPORT.MD

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,24 +7,24 @@ This report maps the current finite TLA+ safety model to the current Go implemen
77
| Model anchor | Implementation anchor | Evidence |
88
| --- | --- | --- |
99
| `tla/EPaxos.tla:27` TLA Init | `epaxos/node.go:98` Go node construction | Initialization sets empty instance state before transport steps; Go also restores durable state and configuration history before accepting input. |
10-
| `tla/EPaxos.tla:15` TLA SafeDeps | `epaxos/node.go:702` Go attribute computation | Both model and implementation require known conflicting commands to appear in dependency attributes. |
10+
| `tla/EPaxos.tla:15` TLA SafeDeps | `epaxos/node.go:706` Go attribute computation | Both model and implementation require known conflicting commands to appear in dependency attributes. |
1111
| `tla/EPaxos.tla:37` TLA PreAccept | `epaxos/node.go:257` Go proposal path | Local proposal computes attributes, persists pre-accepted records, indexes conflicts, and emits pre-accept messages. |
12-
| `tla/EPaxos.tla:37` TLA PreAccept receive | `epaxos/node.go:401` Go pre-accept handler | Remote pre-accept merges local conflicts with proposer attributes and stores the pre-accepted value. |
13-
| `tla/EPaxos.tla:49` TLA Accept | `epaxos/node.go:457` Go accept handler | Accept stores slow-path attributes unless the instance is already committed or promised higher. |
14-
| `tla/EPaxos.tla:58` TLA Commit | `epaxos/node.go:501` Go commit handler | Commit stores final value/attributes and invokes dependency-closed execution. |
15-
| `tla/EPaxos.tla:64` TLA Prepare | `epaxos/node.go:514` Go prepare handler | Prepare persists higher promises and returns local accepted/committed state for recovery; TLA abstracts this as the ballot-raising recovery step. |
16-
| Not represented in current TLA safety model | `epaxos/node.go:540` Go prepare response handler | Recovery response quorum handling is verified by Go tests, but the current TLA model abstracts away recovery quorum collection. |
17-
| `tla/EPaxos.tla:71` TLA Execute | `epaxos/node.go:802` Go executor | Execution collapses committed dependency SCCs, persists executed status, applies configuration commands, and emits application commands. |
18-
| `tla/EPaxos.tla:91` TLA DependencyClosure | `epaxos/node.go:900` Go component readiness | Component readiness requires outside dependencies to be executed or absent before application emission. |
19-
| `tla/EPaxos.tla:93` TLA ConflictOrder | `epaxos/node.go:949` Go dependency references | Both require conflicting executed commands to be ordered by dependencies; Go interprets dependency vectors through configuration history. |
12+
| `tla/EPaxos.tla:37` TLA PreAccept receive | `epaxos/node.go:405` Go pre-accept handler | Remote pre-accept merges local conflicts with proposer attributes and stores the pre-accepted value. |
13+
| `tla/EPaxos.tla:49` TLA Accept | `epaxos/node.go:461` Go accept handler | Accept stores slow-path attributes unless the instance is already committed or promised higher. |
14+
| `tla/EPaxos.tla:58` TLA Commit | `epaxos/node.go:505` Go commit handler | Commit stores final value/attributes and invokes dependency-closed execution. |
15+
| `tla/EPaxos.tla:64` TLA Prepare | `epaxos/node.go:518` Go prepare handler | Prepare persists higher promises and returns local accepted/committed state for recovery; TLA abstracts this as the ballot-raising recovery step. |
16+
| Not represented in current TLA safety model | `epaxos/node.go:544` Go prepare response handler | Recovery response quorum handling is verified by Go tests, but the current TLA model abstracts away recovery quorum collection. |
17+
| `tla/EPaxos.tla:71` TLA Execute | `epaxos/node.go:813` Go executor | Execution collapses committed dependency SCCs, persists executed status, applies configuration commands, and emits application commands. |
18+
| `tla/EPaxos.tla:91` TLA DependencyClosure | `epaxos/node.go:911` Go component readiness | Component readiness requires outside dependencies to be executed or absent before application emission. |
19+
| `tla/EPaxos.tla:93` TLA ConflictOrder | `epaxos/node.go:960` Go dependency references | Both require conflicting executed commands to be ordered by dependencies; Go interprets dependency vectors through configuration history. |
2020
| `tla/EPaxos.tla:78` TLA Tick | `epaxos/node.go:237` Go logical tick | Tick advances logical time and dispatches logical timers; no wall-clock API is used in the core. |
2121
| Not represented in current TLA safety model | `epaxos/checksum.go:65` Go record checksum | Durable record checksum coverage is implementation validation rather than modeled state. |
2222
| Not represented in current TLA safety model | `epaxos/checksum.go:84` Go message checksum | Transport checksum coverage is implementation validation rather than modeled state. |
2323
| Not represented in current TLA safety model | `epaxos/codec.go:40` Go decoder | Wire-decoder input tolerance is implementation validation rather than modeled state. |
2424
| Not represented in current TLA safety model | `epaxos/storage.go:9` Go storage interface | Storage virtualization is an implementation boundary used by deterministic tests rather than modeled state. |
2525
| Not represented in current TLA safety model | `epaxos/quorum.go:15` Go quorum calculation | Quorum arithmetic for cluster sizes one through seven is verified by Go tests; the current TLA constants enumerate voters directly. |
26-
| Not represented in current TLA safety model | `examples/kv/kv.go:102` Go transaction batch apply | Example KV transactions are opaque EPaxos commands at the model boundary; atomic multi-key apply is verified by Go tests rather than by the current TLA state. |
27-
| Not represented in current TLA safety model | `examples/kv/kv.go:169` Go scan implementation | Advanced scan ordering, reverse ordering, timestamp collision behavior, and Pebble durability are example storage semantics verified by Go tests rather than by the current TLA state. |
26+
| Not represented in current TLA safety model | `examples/kv/kv.go:168` Go transaction batch apply | Example KV transactions are opaque EPaxos commands at the model boundary; atomic multi-key apply is verified by Go tests rather than by the current TLA state. |
27+
| Not represented in current TLA safety model | `examples/kv/kv.go:256` Go scan implementation | Advanced scan ordering, reverse ordering, timestamp collision behavior, and Pebble durability are example storage semantics verified by Go tests rather than by the current TLA state. |
2828

2929
## Current verification evidence
3030

@@ -33,10 +33,10 @@ This report maps the current finite TLA+ safety model to the current Go implemen
3333
- Observed `go test ./...` from `examples/kv` passing.
3434
- Observed `go test -coverprofile=coverage.out ./...` from `examples/kv` plus `go tool cover -func=coverage.out` reporting 100.0% statement coverage for the example module.
3535
- Observed `go test -tags kvnode ./cmd/kvnode` from `examples/kv` passing for the tagged HTTP service package.
36-
- Observed `go test ./...` coverage including cluster destruction and restart via `TestRestartAllRawNodesRetainsExecutedAndAppliesOnlyNewCommand`, remove-voter progress via `TestRemoveVoterConfChangeAllowsLaterProgress`, storage write retry via `TestWriteErrorKeepsReadyForRetry`, partition heal convergence via `TestFiveNodePartitionHealConverges`, equal-sequence execution ordering via `TestExecutionEqualSeqTieBreaksByRef`, and inactive dependency filtering via `TestExecutionComponentsSkipInactiveDependencyRefs`.
36+
- Observed `go test ./...` coverage including deterministic ordered simulation scheduling, cluster destruction and restart via `TestRestartAllRawNodesRetainsExecutedAndAppliesOnlyNewCommand`, remove-voter progress via `TestRemoveVoterConfChangeAllowsLaterProgress`, storage write retry via `TestWriteErrorKeepsReadyForRetry`, partition heal convergence via `TestFiveNodePartitionHealConverges`, equal-sequence execution ordering via `TestExecutionEqualSeqTieBreaksByRef`, inactive dependency filtering via `TestExecutionComponentsSkipInactiveDependencyRefs`, `MaxReadyMessages` batching, and logical-time optimization fast-wait behavior.
3737
- Observed `go test ./...` coverage in `examples/kv` including transaction atomicity, malformed transaction rejection, transaction batch error propagation, transaction commit error propagation, apply-order visible-version selection, timestamp collision overwrite behavior, ApplyCommitted close/open durability, delete-tombstone scan suppression, Pebble close/open durability, and reverse scans returning the newest version for repeated keys.
3838
- Observed TLC passing with `/opt/homebrew/opt/openjdk/bin/java -cp /tmp/tla2tools.jar tlc2.TLC -config tla/EPaxos.cfg tla/EPaxos.tla`, generating 11162 states, 2002 distinct states, and no invariant violation.
39-
- Observed `lein test` from `jepsen` passing namespace and dependency loading, and observed `tests/ci.sh` running a local three-node Jepsen workload against deployed `kvnode` processes with `lein run test --no-ssh --nodes 127.0.0.1:19081,127.0.0.1:19082,127.0.0.1:19083 --time-limit 5 --concurrency 3` reporting register `:linearizable {:valid? true}` and transaction `:txn-atomic {:valid? true, :checked 27, :bad-count 0}`.
39+
- Observed `lein test` from `jepsen` passing namespace and dependency loading, and observed `tests/ci.sh` running a local three-node Jepsen workload against deployed `kvnode` processes with `lein run test --no-ssh --nodes 127.0.0.1:19081,127.0.0.1:19082,127.0.0.1:19083 --time-limit 5 --concurrency 3` reporting register `:linearizable {:valid? true}` and transaction `:txn-atomic {:valid? true, :checked 23, :bad-count 0}`.
4040
- Observed repository text audit with no disallowed external-project name, scaffolding marker, or wall-clock API matches.
4141

4242
## Open correspondence risks

epaxos/node.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -345,8 +345,12 @@ func (n *RawNode) Ready() Ready {
345345
for i := range n.pendingReady.Records {
346346
rd.Records[i] = n.pendingReady.Records[i].Clone()
347347
}
348-
rd.Messages = make([]Message, len(n.pendingReady.Messages))
349-
for i := range n.pendingReady.Messages {
348+
messageCount := len(n.pendingReady.Messages)
349+
if n.maxReadyMessages > 0 && messageCount > n.maxReadyMessages {
350+
messageCount = n.maxReadyMessages
351+
}
352+
rd.Messages = make([]Message, messageCount)
353+
for i := range rd.Messages {
350354
rd.Messages[i] = n.pendingReady.Messages[i].Clone()
351355
}
352356
rd.Committed = make([]CommittedCommand, len(n.pendingReady.Committed))

epaxos/remaining_test.go

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,133 @@ func TestRemainingReadyAndProposalBranches(t *testing.T) {
7272
zr.schedule(inst, timerAccept, 0)
7373
}
7474

75+
func TestMaxReadyMessagesCapsOnlyMessages(t *testing.T) {
76+
rn, err := NewRawNode(Config{ID: 1, Voters: makeIDs(3), MaxReadyMessages: 1})
77+
if err != nil {
78+
t.Fatal(err)
79+
}
80+
first := InstanceRef{Replica: 1, Instance: 1, Conf: 1}
81+
second := InstanceRef{Replica: 1, Instance: 2, Conf: 1}
82+
rn.pendingReady = Ready{
83+
Records: []InstanceRecord{
84+
{Ref: first, Command: Command{Payload: []byte("record-1")}},
85+
{Ref: second, Command: Command{Payload: []byte("record-2")}},
86+
},
87+
Messages: []Message{
88+
{Type: MsgPreAccept, From: 1, To: 2, Ref: first, Deps: []InstanceNum{0, 0, 0}},
89+
{Type: MsgPreAccept, From: 1, To: 3, Ref: second, Deps: []InstanceNum{0, 0, 0}},
90+
},
91+
Committed: []CommittedCommand{
92+
{Ref: first, Command: Command{Payload: []byte("commit-1")}},
93+
{Ref: second, Command: Command{Payload: []byte("commit-2")}},
94+
},
95+
MustSync: true,
96+
}
97+
98+
rd := rn.Ready()
99+
if len(rd.Messages) != 1 || rd.Messages[0].To != 2 {
100+
t.Fatalf("first ready messages = %#v, want only the first capped message", rd.Messages)
101+
}
102+
if len(rd.Records) != 2 || rd.Records[0].Ref != first || rd.Records[1].Ref != second {
103+
t.Fatalf("records were capped with messages: %#v", rd.Records)
104+
}
105+
if len(rd.Committed) != 2 || rd.Committed[0].Ref != first || rd.Committed[1].Ref != second {
106+
t.Fatalf("committed commands were capped with messages: %#v", rd.Committed)
107+
}
108+
109+
rn.Advance(rd)
110+
tail := rn.Ready()
111+
if len(tail.Records) != 0 || len(tail.Committed) != 0 {
112+
t.Fatalf("advanced records/committed reappeared with message tail: records=%#v committed=%#v", tail.Records, tail.Committed)
113+
}
114+
if len(tail.Messages) != 1 || tail.Messages[0].To != 3 || tail.Messages[0].Ref != second {
115+
t.Fatalf("message tail ready = %#v, want the unsent second message", tail.Messages)
116+
}
117+
}
118+
119+
func TestTimeOptimizationDelaysSlowAcceptUntilFastWaitTick(t *testing.T) {
120+
store := NewMemoryStorage()
121+
rn, err := NewRawNode(Config{ID: 1, Voters: makeIDs(5), Storage: store, RetryTicks: 10, TimeOptimization: true, TimeOptimizationTicks: 3})
122+
if err != nil {
123+
t.Fatal(err)
124+
}
125+
ref, err := rn.Propose(Command{ID: CommandID{Client: 7, Sequence: 1}, Payload: []byte("value"), ConflictKeys: [][]byte{[]byte("key")}})
126+
if err != nil {
127+
t.Fatal(err)
128+
}
129+
rd := rn.Ready()
130+
if len(rd.Messages) != 4 {
131+
t.Fatalf("initial preaccept messages = %#v", rd.Messages)
132+
}
133+
if err := store.ApplyReady(rd); err != nil {
134+
t.Fatal(err)
135+
}
136+
rn.Advance(rd)
137+
138+
inst := rn.instances[ref]
139+
if inst == nil {
140+
t.Fatalf("missing local instance %s", ref)
141+
}
142+
for _, from := range []ReplicaID{2, 3} {
143+
resp := Message{
144+
Type: MsgPreAcceptResp,
145+
From: from,
146+
To: 1,
147+
Ref: ref,
148+
Ballot: inst.rec.Ballot,
149+
Seq: inst.rec.Seq,
150+
Deps: append([]InstanceNum(nil), inst.rec.Deps...),
151+
RecordStatus: StatusPreAccepted,
152+
}
153+
if err := rn.Step(resp); err != nil {
154+
t.Fatalf("step preaccept response from %d: %v", from, err)
155+
}
156+
}
157+
if got, slow, fast := len(inst.preOK), rn.q.slowQuorum(), rn.q.fastQuorum(); got != slow || got >= fast {
158+
t.Fatalf("preaccept votes = %d, want slow quorum %d below fast quorum %d", got, slow, fast)
159+
}
160+
if inst.phase != phasePreAccept {
161+
t.Fatalf("phase after slow quorum preaccept responses = %d, want preaccept", inst.phase)
162+
}
163+
if rn.HasReady() {
164+
t.Fatalf("slow quorum response produced ready work before fast-wait deadline: %#v", rn.Ready())
165+
}
166+
167+
for tick := uint64(1); tick < 3; tick++ {
168+
rn.Tick()
169+
if inst.phase != phasePreAccept {
170+
t.Fatalf("phase after tick %d = %d, want preaccept before fast-wait deadline", tick, inst.phase)
171+
}
172+
if rn.HasReady() {
173+
t.Fatalf("ready work appeared after tick %d before fast-wait deadline: %#v", tick, rn.Ready())
174+
}
175+
}
176+
177+
rn.Tick()
178+
if inst.phase != phaseAccept {
179+
t.Fatalf("phase at fast-wait deadline = %d, want accept", inst.phase)
180+
}
181+
rd = rn.Ready()
182+
if len(rd.Records) != 1 || rd.Records[0].Ref != ref || rd.Records[0].Status != StatusAccepted {
183+
t.Fatalf("fast-wait ready records = %#v, want accepted record for %s", rd.Records, ref)
184+
}
185+
if len(rd.Messages) != 4 {
186+
t.Fatalf("fast-wait accept messages = %#v", rd.Messages)
187+
}
188+
seen := make(map[ReplicaID]bool, 4)
189+
for _, msg := range rd.Messages {
190+
if msg.Type != MsgAccept || msg.From != 1 || msg.Ref != ref {
191+
t.Fatalf("fast-wait message = %#v, want accept for %s from replica 1", msg, ref)
192+
}
193+
seen[msg.To] = true
194+
}
195+
for _, to := range []ReplicaID{2, 3, 4, 5} {
196+
if !seen[to] {
197+
t.Fatalf("missing accept message to replica %d in %#v", to, rd.Messages)
198+
}
199+
}
200+
}
201+
75202
func TestRemainingResponseBranches(t *testing.T) {
76203
rn, err := NewRawNode(Config{ID: 1, Voters: makeIDs(3)})
77204
if err != nil {

epaxos/sim_test.go

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"bytes"
55
"errors"
66
"fmt"
7+
"sort"
78
"testing"
89
)
910

@@ -31,10 +32,20 @@ func newSimCluster(t *testing.T, n int, opt bool) *simCluster {
3132
return s
3233
}
3334

35+
func (s *simCluster) ids() []ReplicaID {
36+
ids := make([]ReplicaID, 0, len(s.nodes))
37+
for id := range s.nodes {
38+
ids = append(ids, id)
39+
}
40+
sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] })
41+
return ids
42+
}
43+
3444
func (s *simCluster) drain() {
3545
for round := 0; round < 1000; round++ {
3646
progress := false
37-
for id, rn := range s.nodes {
47+
for _, id := range s.ids() {
48+
rn := s.nodes[id]
3849
if !rn.HasReady() {
3950
continue
4051
}
@@ -67,8 +78,8 @@ func (s *simCluster) drain() {
6778

6879
func (s *simCluster) tickAll(n int) {
6980
for i := 0; i < n; i++ {
70-
for _, rn := range s.nodes {
71-
rn.Tick()
81+
for _, id := range s.ids() {
82+
s.nodes[id].Tick()
7283
}
7384
s.drain()
7485
}
@@ -83,7 +94,7 @@ func TestClusterSizesOneThroughSevenCommit(t *testing.T) {
8394
t.Fatal(err)
8495
}
8596
s.drain()
86-
for id := range s.nodes {
97+
for _, id := range s.ids() {
8798
if got := len(s.apps[id]); got != 1 {
8899
t.Fatalf("node %d applied %d commands", id, got)
89100
}
@@ -105,7 +116,7 @@ func TestConflictingConcurrentCommandsConverge(t *testing.T) {
105116
}
106117
s.drain()
107118
want := refs(s.apps[1])
108-
for id := range s.nodes {
119+
for _, id := range s.ids() {
109120
if got := refs(s.apps[id]); fmt.Sprint(got) != fmt.Sprint(want) {
110121
t.Fatalf("node %d order %v want %v", id, got, want)
111122
}
@@ -190,7 +201,7 @@ func TestRestartAllRawNodesRetainsExecutedAndAppliesOnlyNewCommand(t *testing.T)
190201
t.Fatalf("node 1 applied %d commands before restart", got)
191202
}
192203
firstRef := s.apps[1][0].Ref
193-
for id := range s.nodes {
204+
for _, id := range s.ids() {
194205
if got := len(s.apps[id]); got != 1 {
195206
t.Fatalf("node %d applied %d commands before restart", id, got)
196207
}
@@ -213,7 +224,8 @@ func TestRestartAllRawNodesRetainsExecutedAndAppliesOnlyNewCommand(t *testing.T)
213224
t.Fatal(err)
214225
}
215226
s.drain()
216-
for id, rn := range s.nodes {
227+
for _, id := range s.ids() {
228+
rn := s.nodes[id]
217229
applied := s.apps[id]
218230
if len(applied) != 1 {
219231
t.Fatalf("node %d applied %d commands after restart: %#v", id, len(applied), applied)
@@ -483,7 +495,7 @@ func TestFiveNodePartitionHealConverges(t *testing.T) {
483495
s.drop = map[[2]ReplicaID]bool{}
484496
s.tickAll(6)
485497
want := refs(s.apps[1])
486-
for id := range s.nodes {
498+
for _, id := range s.ids() {
487499
if got := refs(s.apps[id]); fmt.Sprint(got) != fmt.Sprint(want) {
488500
t.Fatalf("node %d refs = %v, want %v", id, got, want)
489501
}

examples/kv/kv.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ type ScanOptions struct {
6767
Reverse bool
6868
}
6969

70+
// Open opens a Pebble-backed key-value store and resumes automatic version timestamps from existing records.
7071
func Open(path string) (*DB, error) {
7172
return open(path, nil)
7273
}

0 commit comments

Comments
 (0)