Skip to content

Commit f4efd90

Browse files
authored
Merge pull request #1 from gosuda/chore/strict-lints
chore: strict golangci-lint bootstrap and dual-module CI
2 parents c90cfd6 + 492d315 commit f4efd90

71 files changed

Lines changed: 1076 additions & 535 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# EPaxos conflict engine and executed-instance GC
2+
3+
## Goal
4+
5+
Replace the $O(\text{all instances})$ conflict and attrs machinery with a per-lane conflict engine, add two-tier executed-instance retirement with an asynchronous record-load handshake, expose `VisitConflicts`, and enforce strict linting.
6+
7+
## Success criteria
8+
9+
- R1: Attribute computation and TryPreAccept conflict checks avoid resident-instance scans.
10+
- R2: The conflict index remains coherent for every record mutation and removal.
11+
- R3: Embeddings receive a minimal, zero-allocation public conflict query API.
12+
- R4: Executed instances retire automatically with configurable per-lane retention.
13+
- R5: Payload-drop and fold reclamation bound resident memory, with proposal backpressure.
14+
- R6: Wire format and durable storage remain unchanged; durable compaction remains separate.
15+
- R7: Folded-record recovery uses a deterministic asynchronous Ready handshake.
16+
- R8: Property tests, invariants, TLA evidence, and resident-state benchmarks prove behavior.
17+
- R9: Strict golangci-lint, CI enforcement, and task-goal scaffolding are in place.
18+
- R10: The work is delivered as atomic GitHub issues and PRs targeting `main`.
19+
20+
## Verifiable-goals scaffolding
21+
22+
- [Conflict engine tests](../../epaxos/conflict_engine_test.go)
23+
- [Retirement tests](../../epaxos/retire_test.go)
24+
25+
Remove this task-goal file after the final implementation unit merges.

.github/workflows/ci.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,5 +52,16 @@ jobs:
5252
chmod +x /tmp/lein
5353
echo /tmp >> "$GITHUB_PATH"
5454
55+
- name: Run golangci-lint
56+
uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9
57+
with:
58+
version: v2.12.2
59+
60+
- name: Run golangci-lint (examples/kv)
61+
uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9
62+
with:
63+
version: v2.12.2
64+
working-directory: examples/kv
65+
5566
- name: Run repository verification gates
5667
run: tests/ci.sh

.golangci.yml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
version: "2"
2+
3+
run:
4+
go: "1.26"
5+
6+
linters:
7+
# govet: DEFAULT analyzer set only. Do NOT set enable-all (fieldalignment/shadow noise).
8+
# All of errcheck, errorlint, exhaustive, gocritic, gosec, govet, ineffassign, revive,
9+
# staticcheck, unused, wastedassign stay enabled — R9 correctness/style gate.
10+
enable:
11+
- errcheck
12+
- errorlint
13+
- exhaustive
14+
- gocritic
15+
- gosec
16+
- govet
17+
- ineffassign
18+
- revive
19+
- staticcheck
20+
- unused
21+
- wastedassign
22+
settings:
23+
exhaustive:
24+
default-signifies-exhaustive: false
25+
staticcheck:
26+
checks:
27+
- all

AGENTS.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Agent Instructions
2+
3+
Run these commands before completing relevant changes:
4+
5+
```sh
6+
go test -race -count=1 ./...
7+
go vet ./...
8+
golangci-lint run ./...
9+
(cd examples/kv && golangci-lint run ./...)
10+
tests/ci.sh
11+
```
12+
13+
Per-task goals live in `.agent-tasks/<task-id>/GOALS.md`.
14+
Richer project conventions are init's job.

epaxos/bootstrap.go

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ func validateBootstrapFrontier(f BootstrapFrontier, base ConfState) error {
130130
if coverage > InstanceNum(maxBootstrapSparseRefs-totalCoverage) {
131131
return ErrBootstrapBounds
132132
}
133-
totalCoverage += int(coverage)
133+
totalCoverage += int(coverage) //nolint:gosec // G115: conversion is bounded by protocol or test-fixture limits.
134134
for j, instance := range lane.Sparse {
135135
if instance == 0 || instance <= lane.CompactedExecutedThrough || instance > lane.ObservedThrough ||
136136
(j > 0 && lane.Sparse[j-1] >= instance) {
@@ -340,6 +340,7 @@ func (c ReadyCertificate) Clone() ReadyCertificate {
340340
// BootstrapPhase is the durable phase of a voter plan.
341341
type BootstrapPhase uint8
342342

343+
// BootstrapPhase values describe voter-plan progress.
343344
const (
344345
BootstrapPhaseUnspecified BootstrapPhase = iota
345346
BootstrapPhasePreparing
@@ -356,6 +357,7 @@ const (
356357
// BootstrapOutcome is a terminal replicated membership-control outcome.
357358
type BootstrapOutcome uint8
358359

360+
// BootstrapOutcome values describe terminal bootstrap results.
359361
const (
360362
BootstrapOutcomeUnspecified BootstrapOutcome = iota
361363
BootstrapOutcomeActivated
@@ -424,6 +426,7 @@ func (r BootstrapRecord) Clone() BootstrapRecord {
424426
// LocalVoterStatus is the durable local admission state.
425427
type LocalVoterStatus uint8
426428

429+
// LocalVoterStatus values describe local admission eligibility.
427430
const (
428431
LocalVoterStatusUnspecified LocalVoterStatus = iota
429432
LocalVoterStatusStaged
@@ -523,6 +526,7 @@ type BootstrapStatusSnapshot struct {
523526
// BootstrapExit selects one reserved terminal control ref for recovery.
524527
type BootstrapExit uint8
525528

529+
// BootstrapExit values select reserved terminal controls.
526530
const (
527531
BootstrapExitActivate BootstrapExit = iota + 1
528532
BootstrapExitAbort
@@ -531,6 +535,7 @@ const (
531535
// BootstrapMessageType identifies an authenticated out-of-band bootstrap message.
532536
type BootstrapMessageType uint8
533537

538+
// BootstrapMessageType values identify bootstrap control messages.
534539
const (
535540
BootstrapMsgFenceRequest BootstrapMessageType = iota + 1
536541
BootstrapMsgFenceAck
@@ -1405,6 +1410,8 @@ func validateBootstrapRecord(record BootstrapRecord) error {
14051410
if record.Outcome != BootstrapOutcomeAborted || record.TerminalRef != record.Plan.Reservations.Abort {
14061411
return ErrBootstrapControl
14071412
}
1413+
case BootstrapPhaseUnspecified, BootstrapPhasePreparing, BootstrapPhasePrepared, BootstrapPhaseLocalFenced, BootstrapPhaseFenced, BootstrapPhaseCertified, BootstrapPhaseTargetReady, BootstrapPhaseFinalizing:
1414+
fallthrough
14081415
default:
14091416
if record.Outcome != BootstrapOutcomeUnspecified || !record.TerminalRef.IsZero() {
14101417
return ErrBootstrapControl
@@ -1455,6 +1462,8 @@ func validateMembershipResult(record InstanceRecord) error {
14551462
if !confStateIsZero(result.Successor) || record.ConfChangeResult.Outcome == ConfChangeApplied {
14561463
return fmt.Errorf("%w: rejected membership result has successor", ErrInvalidConfig)
14571464
}
1465+
case BootstrapOutcomeUnspecified:
1466+
fallthrough
14581467
default:
14591468
return fmt.Errorf("%w: unknown membership outcome", ErrInvalidConfig)
14601469
}
@@ -1523,7 +1532,7 @@ func DecodeBootstrapMessage(src []byte, message *BootstrapMessage) error {
15231532
if p.uvarint() != bootstrapWireVersion {
15241533
return ErrInvalidBootstrapMessage
15251534
}
1526-
message.Type = BootstrapMessageType(p.uvarint())
1535+
message.Type = BootstrapMessageType(p.uvarint8())
15271536
p.fixed((*[32]byte)(&message.Cluster))
15281537
p.fixed((*[32]byte)(&message.Plan))
15291538
message.From = ReplicaID(p.uvarint())
@@ -1657,6 +1666,15 @@ func (p *bootstrapParser) uvarint() uint64 {
16571666
return value
16581667
}
16591668

1669+
func (p *bootstrapParser) uvarint8() uint8 {
1670+
v := p.uvarint()
1671+
if v > uint64(^uint8(0)) {
1672+
p.err = true
1673+
return 0
1674+
}
1675+
return uint8(v)
1676+
}
1677+
16601678
func (p *bootstrapParser) fixed(dst *[32]byte) {
16611679
if p.err || len(p.b) < len(dst) {
16621680
p.err = true
@@ -1666,9 +1684,9 @@ func (p *bootstrapParser) fixed(dst *[32]byte) {
16661684
p.b = p.b[len(dst):]
16671685
}
16681686

1669-
func (p *bootstrapParser) bytes(max int) []byte {
1687+
func (p *bootstrapParser) bytes(maxLen int) []byte {
16701688
length := p.uvarint()
1671-
if p.err || length > uint64(max) || length > uint64(len(p.b)) {
1689+
if p.err || length > uint64(maxLen) || length > uint64(len(p.b)) { //nolint:gosec // G115: conversion is bounded by protocol or test-fixture limits.
16721690
p.err = true
16731691
return nil
16741692
}
@@ -2823,6 +2841,7 @@ func (n *RawNode) admitWhileFenced(message Message) error {
28232841
if message.Command.Kind == CommandUser && len(message.Command.Payload) == 0 {
28242842
return nil
28252843
}
2844+
case MsgPreAccept, MsgAccept, MsgCommit, MsgTryPreAccept, MsgTryPreAcceptResp, MsgEvidence, MsgEvidenceResp:
28262845
}
28272846
wire, err := decodeMembershipCommand(message.Command)
28282847
if err != nil || wire.Operation != operation || wire.Plan.Request.Plan != state.record.Plan.Request.Plan {
@@ -2858,6 +2877,7 @@ func (n *RawNode) admitWhileFenced(message Message) error {
28582877
if inst == nil {
28592878
return ErrBootstrapContradiction
28602879
}
2880+
case MsgPreAcceptResp, MsgAcceptResp, MsgPrepareResp, MsgTryPreAccept, MsgTryPreAcceptResp, MsgEvidence, MsgEvidenceResp:
28612881
}
28622882
return nil
28632883
}

epaxos/bootstrap_core_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@ func newBootstrapTestFixture(t *testing.T, voters int, local ReplicaID) bootstra
2121
for i := range identities {
2222
identities[i] = VoterIdentity{Replica: ReplicaID(i + 1), Incarnation: 1}
2323
}
24-
target := VoterIdentity{Replica: ReplicaID(voters + 1), Incarnation: 1}
24+
target := VoterIdentity{Replica: ReplicaID(voters + 1), Incarnation: 1} //nolint:gosec // G115: conversion is bounded by protocol or test-fixture limits.
2525
cluster := ClusterID{1, 2, 3}
26-
planID := BootstrapID{9, byte(voters), byte(local)}
26+
planID := BootstrapID{9, byte(voters), byte(local)} //nolint:gosec // G115: conversion is bounded by protocol or test-fixture limits.
2727
store := NewMemoryStorage()
2828
store.Hard = HardState{Conf: conf.Clone()}
2929
store.ConfigHistory = []ConfigHistoryEntry{{Conf: conf.Clone()}}

epaxos/bootstrap_fence_test.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package epaxos
22

33
import (
4+
"encoding/binary"
45
"bytes"
56
"errors"
67
"reflect"
@@ -435,3 +436,45 @@ func TestBootstrapEnvelopeAndChunkValidationIsCanonicalBoundedAndIdempotent(t *t
435436
t.Fatalf("same-index total conflict err=%v", err)
436437
}
437438
}
439+
440+
441+
func TestDecodeBootstrapMessageRejectsOversizedType(t *testing.T) {
442+
f := newBootstrapTestFixture(t, 1, 1)
443+
plan := prepareBootstrapPlan(t, f)
444+
message, err := BuildBootstrapMessage(BootstrapMessage{
445+
Type: BootstrapMsgReadyQuery, Cluster: f.cluster, Plan: plan.Request.Plan,
446+
From: 1, FromIncarnation: 1, To: 1, BaseID: plan.Request.Base.ID,
447+
BaseDigest: plan.RequestDigest, Payload: []byte("{}"),
448+
})
449+
if err != nil {
450+
t.Fatal(err)
451+
}
452+
encoded, err := EncodeBootstrapMessage(nil, message)
453+
if err != nil {
454+
t.Fatal(err)
455+
}
456+
// Replace the type uvarint (immediately after magic+version) with 257 so it
457+
// would truncate to BootstrapMsgFenceRequest without range checking.
458+
frame := append([]byte(nil), encoded[:len(bootstrapWireMagic)]...)
459+
// re-encode version then oversized type then rest after original version+type
460+
// Parse original after magic: version uvarint then type uvarint.
461+
rest := encoded[len(bootstrapWireMagic):]
462+
_, nVer := binary.Uvarint(rest)
463+
if nVer <= 0 {
464+
t.Fatal("version uvarint")
465+
}
466+
_, nType := binary.Uvarint(rest[nVer:])
467+
if nType <= 0 {
468+
t.Fatal("type uvarint")
469+
}
470+
frame = append(frame, rest[:nVer]...)
471+
frame = binary.AppendUvarint(frame, 257)
472+
frame = append(frame, rest[nVer+nType:]...)
473+
var got BootstrapMessage
474+
if err := DecodeBootstrapMessage(frame, &got); !errors.Is(err, ErrInvalidBootstrapMessage) {
475+
t.Fatalf("oversized bootstrap type err=%v, want ErrInvalidBootstrapMessage; got=%#v", err, got)
476+
}
477+
if got.Type != 0 || got.From != 0 || len(got.Payload) != 0 {
478+
t.Fatalf("failed decode left residue: %#v", got)
479+
}
480+
}

epaxos/bootstrap_recovery_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -772,7 +772,7 @@ func TestDueRecoveryTimersShareBoundedFairDriveBudget(t *testing.T) {
772772

773773
func TestLiveOldQuorumAfterFencerCrashesCanFinalizeButCannotChooseOrdinaryWork(t *testing.T) {
774774
for voters := 3; voters <= 6; voters++ {
775-
t.Run(string(rune('0'+voters)), func(t *testing.T) {
775+
t.Run(string(rune('0'+voters)), func(t *testing.T) { //nolint:gosec // G115: conversion is bounded by protocol or test-fixture limits.
776776
fixture, plan := standaloneBootstrapPlan(t, voters)
777777
frontier := standaloneFrontier(plan, 0)
778778
quorum := slowQuorumSize(voters)

epaxos/branch_test.go

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -110,13 +110,21 @@ func TestDirectProtocolBranches(t *testing.T) {
110110
t.Fatal(err)
111111
}
112112
inst := rn.instances[ref2]
113-
rn.handlePreAcceptResp(Message{Type: MsgPreAcceptResp, From: 2, To: 1, Ref: ref2, Ballot: Ballot{Number: 5, Replica: 2}, Reject: true, RejectHint: Ballot{Number: 5, Replica: 2}, Deps: rn.q.deps()})
113+
if err := rn.handlePreAcceptResp(Message{Type: MsgPreAcceptResp, From: 2, To: 1, Ref: ref2, Ballot: Ballot{Number: 5, Replica: 2}, Reject: true, RejectHint: Ballot{Number: 5, Replica: 2}, Deps: rn.q.deps()}); err != nil {
114+
panic(err)
115+
}
114116
if inst.phase != phasePrepare {
115117
t.Fatal("reject did not start prepare")
116118
}
117-
rn.handlePrepareResp(Message{Type: MsgPrepareResp, From: 2, To: 1, Ref: ref2, RecordStatus: StatusAccepted, Ballot: Ballot{Number: 6, Replica: 2}, RecordBallot: Ballot{Number: 6, Replica: 2}, Seq: 3, Deps: rn.q.deps(), Command: inst.rec.Command})
118-
rn.handlePrepareResp(Message{Type: MsgPrepareResp, From: 2, To: 1, Ref: ref2, RecordStatus: StatusAccepted, Ballot: Ballot{Number: 6, Replica: 2}, RecordBallot: Ballot{Number: 6, Replica: 2}, Seq: 3, Deps: rn.q.deps(), Command: inst.rec.Command})
119-
rn.handlePrepareResp(Message{Type: MsgPrepareResp, From: 3, To: 1, Ref: ref2, RecordStatus: StatusCommitted, Ballot: Ballot{Number: 7, Replica: 3}, RecordBallot: Ballot{Number: 7, Replica: 3}, Seq: 4, Deps: rn.q.deps(), Command: inst.rec.Command})
119+
if err := rn.handlePrepareResp(Message{Type: MsgPrepareResp, From: 2, To: 1, Ref: ref2, RecordStatus: StatusAccepted, Ballot: Ballot{Number: 6, Replica: 2}, RecordBallot: Ballot{Number: 6, Replica: 2}, Seq: 3, Deps: rn.q.deps(), Command: inst.rec.Command}); err != nil {
120+
panic(err)
121+
}
122+
if err := rn.handlePrepareResp(Message{Type: MsgPrepareResp, From: 2, To: 1, Ref: ref2, RecordStatus: StatusAccepted, Ballot: Ballot{Number: 6, Replica: 2}, RecordBallot: Ballot{Number: 6, Replica: 2}, Seq: 3, Deps: rn.q.deps(), Command: inst.rec.Command}); err != nil {
123+
panic(err)
124+
}
125+
if err := rn.handlePrepareResp(Message{Type: MsgPrepareResp, From: 3, To: 1, Ref: ref2, RecordStatus: StatusCommitted, Ballot: Ballot{Number: 7, Replica: 3}, RecordBallot: Ballot{Number: 7, Replica: 3}, Seq: 4, Deps: rn.q.deps(), Command: inst.rec.Command}); err != nil {
126+
panic(err)
127+
}
120128
rn.startAccept(inst, inst.rec.Attributes())
121129
rn.commit(inst, inst.rec.Attributes())
122130
}
@@ -132,13 +140,19 @@ func TestAcceptResponseAndConfigBranches(t *testing.T) {
132140
}
133141
inst := rn.instances[ref]
134142
rn.startAccept(inst, inst.rec.Attributes())
135-
rn.handleAcceptResp(Message{Type: MsgAcceptResp, From: 2, To: 1, Ref: ref, Ballot: Ballot{Number: 9, Replica: 2}, Reject: true, RejectHint: Ballot{Number: 9, Replica: 2}, Deps: rn.q.deps()})
143+
if err := rn.handleAcceptResp(Message{Type: MsgAcceptResp, From: 2, To: 1, Ref: ref, Ballot: Ballot{Number: 9, Replica: 2}, Reject: true, RejectHint: Ballot{Number: 9, Replica: 2}, Deps: rn.q.deps()}); err != nil {
144+
panic(err)
145+
}
136146
inst.phase = phaseAccept
137147
inst.rec.Status = StatusAccepted
138148
inst.rec.RecordBallot = inst.rec.Ballot
139149
inst.accOK = 0
140-
rn.handleAcceptResp(Message{Type: MsgAcceptResp, From: 2, To: 1, Ref: ref, Ballot: inst.rec.Ballot, RecordBallot: inst.rec.Ballot, RecordStatus: StatusAccepted, Seq: inst.rec.Seq, Deps: rn.q.deps()})
141-
rn.handleAcceptResp(Message{Type: MsgAcceptResp, From: 2, To: 1, Ref: ref, Ballot: inst.rec.Ballot, RecordBallot: inst.rec.Ballot, RecordStatus: StatusAccepted, Seq: inst.rec.Seq, Deps: rn.q.deps()})
150+
if err := rn.handleAcceptResp(Message{Type: MsgAcceptResp, From: 2, To: 1, Ref: ref, Ballot: inst.rec.Ballot, RecordBallot: inst.rec.Ballot, RecordStatus: StatusAccepted, Seq: inst.rec.Seq, Deps: rn.q.deps()}); err != nil {
151+
panic(err)
152+
}
153+
if err := rn.handleAcceptResp(Message{Type: MsgAcceptResp, From: 2, To: 1, Ref: ref, Ballot: inst.rec.Ballot, RecordBallot: inst.rec.Ballot, RecordStatus: StatusAccepted, Seq: inst.rec.Seq, Deps: rn.q.deps()}); err != nil {
154+
panic(err)
155+
}
142156
rn.enqueueRecord(InstanceRecord{Ref: InstanceRef{Replica: 1, Instance: 99, Conf: 1}, Deps: rn.q.deps(), Command: Command{Kind: CommandNoop}})
143157
rn.enqueueMessage(Message{Type: MsgPrepareResp, From: 1, To: 2, Ref: ref, Ballot: Ballot{Number: 1, Replica: 2}, Deps: rn.q.deps()})
144158
if _, err := FastQuorum(0); err == nil {
@@ -183,7 +197,9 @@ func TestRestartAcceptedForeignInstanceSchedulesPrepareRecovery(t *testing.T) {
183197
}
184198
advanceOK(t, rn, initial)
185199
for tick := uint64(1); tick < 4; tick++ {
186-
rn.Tick()
200+
if err := rn.Tick(); err != nil {
201+
panic(err)
202+
}
187203
tickReady := rn.Ready()
188204
wantTick := HardState{Conf: wantInitial.Conf, Tick: tick}
189205
if !tickReady.HardState.Equal(wantTick) || !tickReady.MustSync ||
@@ -196,7 +212,9 @@ func TestRestartAcceptedForeignInstanceSchedulesPrepareRecovery(t *testing.T) {
196212
advanceOK(t, rn, tickReady)
197213
}
198214

199-
rn.Tick()
215+
if err := rn.Tick(); err != nil {
216+
panic(err)
217+
}
200218
if inst.phase != phasePrepare {
201219
t.Fatalf("foreign accepted recovery phase = %d, want prepare after recovery deadline", inst.phase)
202220
}

0 commit comments

Comments
 (0)