Skip to content

feat(epaxos): retire executed residents beyond retention - #5

Merged
metaphorics merged 2 commits into
mainfrom
feat/executed-instance-retirement
Jul 14, 2026
Merged

feat(epaxos): retire executed residents beyond retention#5
metaphorics merged 2 commits into
mainfrom
feat/executed-instance-retirement

Conversation

@metaphorics

@metaphorics metaphorics commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Solution / proposal

retireExecuted folds contiguous executed prefixes via engine summaries; executedTracker.contains includes through[lane]; Propose MaxResidentInstances backpressure.

Closes #

Summary by Sourcery

Replace the legacy conflict indexes with a structured per-lane conflict engine, add executed-instance retirement backed by an asynchronous record-load handshake and resident backpressure, harden decoding and tooling around safety and validation, and wire linting plus expanded tests and docs into the build and CI pipeline.

New Features:

  • Add a per-lane conflict engine to track eligible and global-scoped conflicts without scanning all resident instances
  • Introduce executed-instance retirement with per-lane retention tails and a folded-record load handshake via Ready.RecordLoads and ProvideRecordLoad
  • Expose configuration and runtime controls for record-load deferral, executed retention, and resident-instance backpressure

Bug Fixes:

  • Harden message, bootstrap, and record decoding against oversized or invalid enum values and malformed inputs
  • Tighten checkpoint and lifecycle tooling around file sizes, manifest bounds, TLS authorization, and subprocess error reporting to avoid silent or unsafe behavior
  • Ensure timing-domain, configuration-outcome, and membership-result validation rejects previously accepted but invalid combinations

Enhancements:

  • Refactor record mutation paths to centralize conflict tracking through the new conflict engine while preserving checksums and evidence semantics
  • Extend RuntimeStats and Ready cloning to account for folded instances, payload stubs, record-load misses, and RecordLoads, and improve zero-allocation behavior
  • Make Tick, timers, and recovery paths consistently return and check errors in tests, clarifying logical-time exhaustion handling
  • Improve error messages and branching in fault campaigns, refinement traces, and lifecycle collectors for clearer diagnostics and host checks
  • Relax or annotate selected unsafe/gosec usages and add scoped nolint comments where behavior is intentional

Build:

  • Add a golangci-lint configuration and wire golangci-lint runs for the main module and examples/kv into CI

CI:

  • Extend the GitHub Actions CI workflow to run golangci-lint across core and kv examples before existing repository gates

Documentation:

  • Document the record-load handshake and Ready.RecordLoads semantics in EPAXOS.MD and package epaxos docs, and add agent instructions and task goals for the conflict-GC work

Tests:

  • Add extensive conflict-engine, record-load, executed-retirement, and decoder-edge tests plus refinements to existing protocol, recovery, TOQ, and backup tests to cover new paths and stricter invariants
  • Strengthen fault-simulation, lifecycle, and refinementtrace test suites with clearer expectations, better error wrapping, and additional security-focused cases

Chores:

  • Introduce AGENTS.md and per-task goal scaffolding under .agent-tasks to guide future automated changes

Closes #12

@sourcery-ai

sourcery-ai Bot commented Jul 14, 2026

Copy link
Copy Markdown

Reviewer's Guide

Replaces the legacy conflict-index maps with a radix-tree-based conflictEngine, adds executed-instance retirement with an async record-load handshake, tightens validation and error handling across EPaxos core, tests, and KV examples, and wires new configuration knobs and metrics into the node and fault campaigns.

Sequence diagram for the new Ready.RecordLoads / ProvideRecordLoad handshake

sequenceDiagram
    actor Client
    participant RawNode
    participant Storage

    Client->>RawNode: Step(Message)
    activate RawNode
    RawNode->>RawNode: needsRecordLoad(ref)
    alt record load required
        RawNode->>RawNode: deferRecordLoad(ref, Message)
        RawNode-->>Client: error ErrMessageRejected (if queue full)
        deactivate RawNode
        Storage->>RawNode: Ready()
        note over RawNode: Ready.RecordLoads contains ref
        RawNode-->>Storage: Ready{RecordLoads}

        Storage->>Storage: load InstanceRecord from durable
        Storage->>RawNode: ProvideRecordLoad(RecordLoadResult)
        activate RawNode
        RawNode->>RawNode: validateRecordChecksum(rec)
        RawNode->>RawNode: installInstance / setInstanceRecord
        RawNode->>RawNode: Step(deferred Message)
        RawNode->>RawNode: maybeRefoldLoaded(ref)
        deactivate RawNode
    else no record load required
        RawNode->>RawNode: handlePrepare / handleEvidence
        RawNode-->>Client: Step result
    end
Loading

File-Level Changes

Change Details Files
Replace map-based conflict indexing with a lane-aware conflictEngine and adjust attribute computation and conflict checks to use it.
  • Introduce conflictEngine, laneTree, postingSet, and laneIndex structures to track per-lane eligible/global conflicts and key postings with radix trees.
  • Wire RawNode to use the new engine (residentCount, lanes, keyMax, maxEligibleAny, globalMax) instead of conflicts/allConflicts/globalConflicts and delete the old indexConflicts and rebuildConflictLane code.
  • Rewrite computeAttrs and tryPreAcceptConflict to query the engine for max per-lane/global/key conflicts and use prefixMaxSeq for sequence assignment.
  • Adjust TOQ tests, protocol coverage tests, conflict-ordering tests, and remaining tests to reference engine APIs instead of the old conflictIndex maps.
  • Add engine.verify and property-style tests (conflict_engine_test.go) to ensure radix aggregates and postings remain consistent with a model.
epaxos/conflict_engine.go
epaxos/conflict_engine_test.go
epaxos/node.go
epaxos/sparse_progress.go
epaxos/toq_test.go
epaxos/protocol_coverage_test.go
epaxos/conflict_ordering_test.go
epaxos/optimized_test.go
epaxos/remaining_test.go
Introduce executed-instance retirement and folded-record reloading via Ready.RecordLoads and ProvideRecordLoad.
  • Extend Config with RetainExecutedPerLane, MaxDeferredRecordLoads, and MaxResidentInstances and plumb them through NewRawNode with validation defaults.
  • Enhance executedTracker.contains and add forgetExactThrough to support folded-prefix membership while freeing exact entries.
  • Add RawNode.retireExecuted to fold executed instances beyond per-lane retention, advance conflictEngine fold watermarks, and trim executed exact sets; call it from Advance after enqueueing executed records.
  • Add needsRecordLoad, deferRecordLoad, ProvideRecordLoad, maybeRefoldLoaded, validateRecordChecksum, and setInstanceRecord to manage async reloading of folded records, including pending message queues, capacity backpressure, and refolding of loaded executed residents.
  • Extend Ready with RecordLoads, update HasReady/Advance/validateReadyAck/CloneInto/RuntimeStats, define RecordLoadResult type and new errors (ErrDeferredRecordLoadFull, ErrUnrequestedRecordLoad, ErrInvalidRecord, ErrResidentInstancesExceeded), and add tests for record-load and retire behavior.
  • Add Propose/ProposeConfChange backpressure based on engine.residentCount vs MaxResidentInstances, and track FoldedInstances/PayloadStubInstances metrics in RuntimeStats.
epaxos/node.go
epaxos/sparse_progress.go
epaxos/retire.go
epaxos/retire_test.go
epaxos/record_load_test.go
epaxos/types.go
epaxos/doc.go
EPAXOS.MD
Harden message/record/bootstrap codecs and validation, especially around enum ranges, lengths, and oversized varints.
  • Constrain MessageType, RejectReason, Status, CommandKind, BootstrapMessageType, BootstrapPhase, and BootstrapOutcome decoding to uint8 via new uvarint8 helpers in codec and bootstrapParser; reject oversized values as invalid.
  • Bound AcceptEvidence deps counts and slice slicing to maxWireDeps and evidenceArena length; enforce maxWireCommandPayload and maxWireConflictKey in parser.bytesBound.
  • Add tests to ensure oversized message and bootstrap types are rejected and do not leave residual state (codec_evidence_scratch_test.go, bootstrap_fence_test.go).
  • Tighten kv epaxos record decoding to validate status and command kind ranges fit in uint8 and respect timing-domain invariants; reject invalid timing-domain/processAt/TOQPending combinations.
  • Make checkpoint manifest/file parsing and collection more defensive: validate sizes non-negative and within caps, bound manifest string lengths, cap file counts, and handle negative sizes in hashing; refine error messages.
  • Improve various Validate and timing-domain switch statements with explicit fallthrough/default handling for new enum values.
epaxos/codec.go
epaxos/bootstrap.go
epaxos/bootstrap_fence_test.go
epaxos/codec_evidence_scratch_test.go
examples/kv/epaxos_storage.go
examples/kv/backup.go
examples/kv/epaxos_record_timing_codec_test.go
epaxos/message.go
epaxos/bootstrap_core_test.go
Tighten error handling, logging, and tests across recovery, timing, and harness code to propagate errors and satisfy linters.
  • Make Tick, onTimer, startPrepare, handle*Resp, and schedule calls in tests check and propagate errors instead of ignoring return values; adjust expectations accordingly.
  • Change various switch statements in tests to mark non-exhaustive subsets with //nolint:exhaustive and annotate other gosec/staticcheck suppressions where intentional (e.g., unsafe pointer comparisons, rune conversions, rand usage).
  • Improve error messages and error wrapping in faultcampaign and lifecyclecollector (e.g., more precise fmt.Errorf %w usage, separating status vs error), and ensure HTTP response bodies are closed via defers.
  • Standardize kvcheckpoint CLI printing to ignore fmt.Fprintln errors, harden report/manifest path handling (MkdirAll/WriteFile with explicit permissions, allowed path traversal), and slightly tweak some error messages for clarity.
  • Update hard_state_ready, recovery, protocol, bootstrap, DST, stress, and other tests to call Tick() and related functions expecting error returns, and adjust helper utilities accordingly.
epaxos/recovery_test.go
epaxos/remaining_test.go
epaxos/protocol_coverage_test.go
epaxos/branch_test.go
epaxos/hard_state_ready_test.go
epaxos/recovery_boundary_test.go
epaxos/dst_test.go
epaxos/stress_test.go
tests/lifecyclecollector/*.go
tests/faultcampaign/*.go
examples/kv/cmd/kvcheckpoint/main.go
examples/kv/backup_test.go
examples/kv/kv.go
tests/refinementtrace/*.go
Add golangci-lint configuration and wire lint steps into CI, adjusting codebase to satisfy new lint rules.
  • Introduce .golangci.yml enabling a focused set of linters (errcheck, errorlint, exhaustive, gocritic, gosec, govet, ineffassign, revive, staticcheck, unused, wastedassign) with tuned settings.
  • Extend GitHub Actions workflow to run golangci-lint at repo root and in examples/kv before existing ci.sh gates.
  • Sprinkle targeted //nolint annotations (gosec, gocritic, staticcheck, etc.) where code intentionally uses unsafe, OS commands, path traversal, or non-exhaustive switches.
  • Add AGENTS.md and .agent-tasks/epaxos-conflict-gc/GOALS.md to document developer/agent workflows and success criteria for the conflict-engine and GC work.
.golangci.yml
.github/workflows/ci.yml
AGENTS.md
.agent-tasks/epaxos-conflict-gc/GOALS.md
multiple *.go across epaxos/, examples/kv/, tests/

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@metaphorics

Copy link
Copy Markdown
Contributor Author

Closes #12

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 2 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="epaxos/retire_test.go" line_range="8" />
<code_context>
+	"testing"
+)
+
+func TestExecutedTrackerContainsFoldedPrefix(t *testing.T) {
+	var tr executedTracker
+	lane := instanceLane{conf: 1, replica: 1}
</code_context>
<issue_to_address>
**suggestion (testing):** Consider adding tests for retainExecutedPerLane edge cases, especially zero retention

Right now the tests only cover positive RetainExecutedPerLane. Please also add a case with RetainExecutedPerLane = 0 that verifies:
- all executed instances on a lane can be folded, and
- conflict engine foldedThrough watermark and executedTracker.contains remain consistent.
It would also be useful to cover a scenario where the number of executed instances is smaller than the retention window.

Suggested implementation:

```golang
	if !tr.contains(InstanceRef{Conf: 1, Replica: 1, Instance: 2}) {
		t.Fatal("expected contains via through after exact cleanup")
	}
}

func TestExecutedTrackerZeroRetentionFoldsAll(t *testing.T) {
	var tr executedTracker
	lane := instanceLane{conf: 1, replica: 1}

	// Simulate executing a contiguous range of instances on a lane.
	for i := InstanceNum(1); i <= 5; i++ {
		tr.add(InstanceRef{Conf: 1, Replica: 1, Instance: i})
	}

	// With RetainExecutedPerLane = 0, all executed instances on this lane
	// should be eligible to be folded, i.e. the engine would be allowed
	// to forget the entire prefix.
	//
	// We simulate that by folding the prefix completely via forgetExactThrough.
	tr.forgetExactThrough(lane, 5)

	// The foldedThrough watermark (queried via prefix) should reflect that
	// all executed instances up to 5 have been folded.
	if got := tr.prefix(lane); got != 5 {
		t.Fatalf("prefix after zero-retention fold = %d, want 5", got)
	}

	// Even though everything is folded, contains() must still report all
	// folded instances as executed, using the foldedThrough watermark.
	for i := InstanceNum(1); i <= 5; i++ {
		if !tr.contains(InstanceRef{Conf: 1, Replica: 1, Instance: i}) {
			t.Fatalf("expected contains(%d) to be true after zero-retention fold", i)
		}
	}
}

func TestExecutedTrackerRetentionWindowLargerThanExecuted(t *testing.T) {
	var tr executedTracker
	lane := instanceLane{conf: 1, replica: 1}

	// Simulate executing fewer instances than a hypothetical retention window.
	// For example, if RetainExecutedPerLane = 10 and we only executed 3
	// instances, the engine should not need to fold anything yet.
	for i := InstanceNum(1); i <= 3; i++ {
		tr.add(InstanceRef{Conf: 1, Replica: 1, Instance: i})
	}

	// No folding is applied here, to reflect that the retention window is
	// larger than the executed range.

	// All instances should be contained and the prefix should point at the
	// highest executed instance.
	if got := tr.prefix(lane); got != 3 {
		t.Fatalf("prefix with small executed set and large retention window = %d, want 3", got)
	}
	for i := InstanceNum(1); i <= 3; i++ {
		if !tr.contains(InstanceRef{Conf: 1, Replica: 1, Instance: i}) {
			t.Fatalf("expected contains(%d) to be true when retention window > executed count", i)
		}
	}
}

```

To fully exercise `retainExecutedPerLane` and the conflict engine’s `foldedThrough` watermark as mentioned in your review comment, you will likely want to:

1. Add an integration-style test (probably in this same file) that:
   - Constructs a real conflict engine instance with `RetainExecutedPerLane = 0`.
   - Marks a contiguous sequence of instances as executed through the engine’s public API.
   - Triggers whatever mechanism applies retention (e.g. a `tick`, `garbageCollect`, or similar method that folds executed instances based on `RetainExecutedPerLane`).
   - Asserts that:
     * The engine’s `foldedThrough` watermark for the lane equals the highest executed instance.
     * The internal `executedTracker` (reachable via exported accessors or test hooks) reports `contains()` correctly for folded instances.

2. Add a parallel test where `RetainExecutedPerLane` is set to a value greater than the number of executed instances (e.g. `RetainExecutedPerLane = 10` with only 3 executed instances), and verify that:
   - No folding occurs (the `foldedThrough` watermark does not advance unexpectedly).
   - All executed instances are still individually tracked (if the implementation distinguishes between folded and explicitly tracked).

You will need to adapt the above tests to the actual conflict engine type and API present in your codebase (constructor, configuration struct/fields, and the method used to drive retention).
</issue_to_address>

### Comment 2
<location path="epaxos/record_load_test.go" line_range="25" />
<code_context>
+	engine.advanceFold(lane, rec.Ref.Instance)
+}
+
+func TestRecordLoadFoundReplaysDeferred(t *testing.T) {
+	rn, err := NewRawNode(Config{ID: 1, Voters: makeIDs(3)})
+	if err != nil {
</code_context>
<issue_to_address>
**suggestion (testing):** Add tests for ProvideRecordLoad Ref handling (zero Ref override and mismatched Ref error case)

One behavior that's not covered is how ProvideRecordLoad handles the Ref field:
- If Record.Ref is zero and the result Ref is non-zero, it updates rec.Ref = res.Ref.
- If Record.Ref is non-zero and differs from res.Ref, it should return an error.
Please add tests for these two cases to lock in the expected override behavior and ensure we fail when a record would be installed under the wrong InstanceRef.

Suggested implementation:

```golang
func TestRecordLoadFoundReplaysDeferred(t *testing.T) {
	rn, err := NewRawNode(Config{ID: 1, Voters: makeIDs(3)})
	if err != nil {
		t.Fatal(err)
	}
	ref := InstanceRef{Conf: 1, Replica: 2, Instance: 5}
	rec := checkedRecord(InstanceRecord{
		Ref: ref, Status: StatusCommitted, Seq: 3, Ballot: Ballot{Replica: 2},
		Deps: rn.q.deps(), Command: Command{Payload: []byte("x"), ConflictKeys: [][]byte{[]byte("k")}},
	})
	// Mark folded without resident.
	foldTestRef(&rn.engine, rec)
}

func TestProvideRecordLoadZeroRefOverride(t *testing.T) {
	rn, err := NewRawNode(Config{ID: 1, Voters: makeIDs(3)})
	if err != nil {
		t.Fatal(err)
	}

	// Create a resident record that will be "loaded" by ProvideRecordLoad.
	ref := InstanceRef{Conf: 1, Replica: 2, Instance: 5}
	stored := checkedRecord(InstanceRecord{
		Ref:    ref,
		Status: StatusCommitted,
		Seq:    3,
		Ballot: Ballot{Replica: 2},
		Deps:   rn.q.deps(),
		Command: Command{
			Payload:      []byte("x"),
			ConflictKeys: [][]byte{[]byte("k")},
		},
	})

	// Install the resident record in the engine so ProvideRecordLoad can find it.
	foldTestRef(&rn.engine, stored)

	// Build a load request with zero Ref that should be overridden by the result Ref.
	rec := stored
	rec.Ref = InstanceRef{} // zero Ref triggers override behavior

	lane := 0 // use the default lane for this test

	loaded, err := rn.engine.ProvideRecordLoad(lane, rec)
	if err != nil {
		t.Fatalf("ProvideRecordLoad unexpected error: %v", err)
	}

	if loaded.Ref != ref {
		t.Fatalf("expected loaded.Ref to be %v, got %v", ref, loaded.Ref)
	}
	// Verify that the input record Ref was also updated to match the loaded Ref.
	if rec.Ref != ref {
		t.Fatalf("expected rec.Ref to be updated to %v, got %v", ref, rec.Ref)
	}
}

func TestProvideRecordLoadMismatchedRefError(t *testing.T) {
	rn, err := NewRawNode(Config{ID: 1, Voters: makeIDs(3)})
	if err != nil {
		t.Fatal(err)
	}

	// Create a resident record with a specific Ref.
	ref := InstanceRef{Conf: 1, Replica: 2, Instance: 5}
	stored := checkedRecord(InstanceRecord{
		Ref:    ref,
		Status: StatusCommitted,
		Seq:    3,
		Ballot: Ballot{Replica: 2},
		Deps:   rn.q.deps(),
		Command: Command{
			Payload:      []byte("x"),
			ConflictKeys: [][]byte{[]byte("k")},
		},
	})

	// Install the resident record in the engine so ProvideRecordLoad can find it.
	foldTestRef(&rn.engine, stored)

	// Build a load request whose Ref does not match the stored Ref.
	rec := stored
	rec.Ref = InstanceRef{Conf: 9, Replica: 9, Instance: 9}

	lane := 0

	_, err = rn.engine.ProvideRecordLoad(lane, rec)
	if err == nil {
		t.Fatalf("expected ProvideRecordLoad to fail for mismatched Ref, got nil error")
	}
}

```

1. These tests assume `ProvideRecordLoad` is a method with signature `func (e *engine) ProvideRecordLoad(lane int, rec InstanceRecord) (InstanceRecord, error)`. If the actual receiver type, lane type, parameter passing (by value vs pointer), or return values differ, adjust the call sites accordingly.
2. If `foldTestRef` is not sufficient to make the record discoverable by `ProvideRecordLoad`, you may need to mirror whatever setup other `ProvideRecordLoad` tests use (e.g., calling specific engine helpers to install the resident record).
3. If `InstanceRef` comparison requires a helper (e.g., `EqualInstanceRef`) instead of `!=`/`==`, replace the direct comparisons with that helper to stay consistent with existing conventions.
4. If `record_load_test.go` does not yet import any additional packages (like `reflect`) and you choose to use them for comparisons, add the imports at the top of the file.
5. If `TestRecordLoadFoundReplaysDeferred` contains additional code after the snippet you provided, move the newly added tests so they are defined at file scope (i.e., after the closing brace of that test) to keep the file compiling.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread epaxos/retire_test.go
"testing"
)

func TestExecutedTrackerContainsFoldedPrefix(t *testing.T) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Consider adding tests for retainExecutedPerLane edge cases, especially zero retention

Right now the tests only cover positive RetainExecutedPerLane. Please also add a case with RetainExecutedPerLane = 0 that verifies:

  • all executed instances on a lane can be folded, and
  • conflict engine foldedThrough watermark and executedTracker.contains remain consistent.
    It would also be useful to cover a scenario where the number of executed instances is smaller than the retention window.

Suggested implementation:

	if !tr.contains(InstanceRef{Conf: 1, Replica: 1, Instance: 2}) {
		t.Fatal("expected contains via through after exact cleanup")
	}
}

func TestExecutedTrackerZeroRetentionFoldsAll(t *testing.T) {
	var tr executedTracker
	lane := instanceLane{conf: 1, replica: 1}

	// Simulate executing a contiguous range of instances on a lane.
	for i := InstanceNum(1); i <= 5; i++ {
		tr.add(InstanceRef{Conf: 1, Replica: 1, Instance: i})
	}

	// With RetainExecutedPerLane = 0, all executed instances on this lane
	// should be eligible to be folded, i.e. the engine would be allowed
	// to forget the entire prefix.
	//
	// We simulate that by folding the prefix completely via forgetExactThrough.
	tr.forgetExactThrough(lane, 5)

	// The foldedThrough watermark (queried via prefix) should reflect that
	// all executed instances up to 5 have been folded.
	if got := tr.prefix(lane); got != 5 {
		t.Fatalf("prefix after zero-retention fold = %d, want 5", got)
	}

	// Even though everything is folded, contains() must still report all
	// folded instances as executed, using the foldedThrough watermark.
	for i := InstanceNum(1); i <= 5; i++ {
		if !tr.contains(InstanceRef{Conf: 1, Replica: 1, Instance: i}) {
			t.Fatalf("expected contains(%d) to be true after zero-retention fold", i)
		}
	}
}

func TestExecutedTrackerRetentionWindowLargerThanExecuted(t *testing.T) {
	var tr executedTracker
	lane := instanceLane{conf: 1, replica: 1}

	// Simulate executing fewer instances than a hypothetical retention window.
	// For example, if RetainExecutedPerLane = 10 and we only executed 3
	// instances, the engine should not need to fold anything yet.
	for i := InstanceNum(1); i <= 3; i++ {
		tr.add(InstanceRef{Conf: 1, Replica: 1, Instance: i})
	}

	// No folding is applied here, to reflect that the retention window is
	// larger than the executed range.

	// All instances should be contained and the prefix should point at the
	// highest executed instance.
	if got := tr.prefix(lane); got != 3 {
		t.Fatalf("prefix with small executed set and large retention window = %d, want 3", got)
	}
	for i := InstanceNum(1); i <= 3; i++ {
		if !tr.contains(InstanceRef{Conf: 1, Replica: 1, Instance: i}) {
			t.Fatalf("expected contains(%d) to be true when retention window > executed count", i)
		}
	}
}

To fully exercise retainExecutedPerLane and the conflict engine’s foldedThrough watermark as mentioned in your review comment, you will likely want to:

  1. Add an integration-style test (probably in this same file) that:

    • Constructs a real conflict engine instance with RetainExecutedPerLane = 0.
    • Marks a contiguous sequence of instances as executed through the engine’s public API.
    • Triggers whatever mechanism applies retention (e.g. a tick, garbageCollect, or similar method that folds executed instances based on RetainExecutedPerLane).
    • Asserts that:
      • The engine’s foldedThrough watermark for the lane equals the highest executed instance.
      • The internal executedTracker (reachable via exported accessors or test hooks) reports contains() correctly for folded instances.
  2. Add a parallel test where RetainExecutedPerLane is set to a value greater than the number of executed instances (e.g. RetainExecutedPerLane = 10 with only 3 executed instances), and verify that:

    • No folding occurs (the foldedThrough watermark does not advance unexpectedly).
    • All executed instances are still individually tracked (if the implementation distinguishes between folded and explicitly tracked).

You will need to adapt the above tests to the actual conflict engine type and API present in your codebase (constructor, configuration struct/fields, and the method used to drive retention).

engine.advanceFold(lane, rec.Ref.Instance)
}

func TestRecordLoadFoundReplaysDeferred(t *testing.T) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Add tests for ProvideRecordLoad Ref handling (zero Ref override and mismatched Ref error case)

One behavior that's not covered is how ProvideRecordLoad handles the Ref field:

  • If Record.Ref is zero and the result Ref is non-zero, it updates rec.Ref = res.Ref.
  • If Record.Ref is non-zero and differs from res.Ref, it should return an error.
    Please add tests for these two cases to lock in the expected override behavior and ensure we fail when a record would be installed under the wrong InstanceRef.

Suggested implementation:

func TestRecordLoadFoundReplaysDeferred(t *testing.T) {
	rn, err := NewRawNode(Config{ID: 1, Voters: makeIDs(3)})
	if err != nil {
		t.Fatal(err)
	}
	ref := InstanceRef{Conf: 1, Replica: 2, Instance: 5}
	rec := checkedRecord(InstanceRecord{
		Ref: ref, Status: StatusCommitted, Seq: 3, Ballot: Ballot{Replica: 2},
		Deps: rn.q.deps(), Command: Command{Payload: []byte("x"), ConflictKeys: [][]byte{[]byte("k")}},
	})
	// Mark folded without resident.
	foldTestRef(&rn.engine, rec)
}

func TestProvideRecordLoadZeroRefOverride(t *testing.T) {
	rn, err := NewRawNode(Config{ID: 1, Voters: makeIDs(3)})
	if err != nil {
		t.Fatal(err)
	}

	// Create a resident record that will be "loaded" by ProvideRecordLoad.
	ref := InstanceRef{Conf: 1, Replica: 2, Instance: 5}
	stored := checkedRecord(InstanceRecord{
		Ref:    ref,
		Status: StatusCommitted,
		Seq:    3,
		Ballot: Ballot{Replica: 2},
		Deps:   rn.q.deps(),
		Command: Command{
			Payload:      []byte("x"),
			ConflictKeys: [][]byte{[]byte("k")},
		},
	})

	// Install the resident record in the engine so ProvideRecordLoad can find it.
	foldTestRef(&rn.engine, stored)

	// Build a load request with zero Ref that should be overridden by the result Ref.
	rec := stored
	rec.Ref = InstanceRef{} // zero Ref triggers override behavior

	lane := 0 // use the default lane for this test

	loaded, err := rn.engine.ProvideRecordLoad(lane, rec)
	if err != nil {
		t.Fatalf("ProvideRecordLoad unexpected error: %v", err)
	}

	if loaded.Ref != ref {
		t.Fatalf("expected loaded.Ref to be %v, got %v", ref, loaded.Ref)
	}
	// Verify that the input record Ref was also updated to match the loaded Ref.
	if rec.Ref != ref {
		t.Fatalf("expected rec.Ref to be updated to %v, got %v", ref, rec.Ref)
	}
}

func TestProvideRecordLoadMismatchedRefError(t *testing.T) {
	rn, err := NewRawNode(Config{ID: 1, Voters: makeIDs(3)})
	if err != nil {
		t.Fatal(err)
	}

	// Create a resident record with a specific Ref.
	ref := InstanceRef{Conf: 1, Replica: 2, Instance: 5}
	stored := checkedRecord(InstanceRecord{
		Ref:    ref,
		Status: StatusCommitted,
		Seq:    3,
		Ballot: Ballot{Replica: 2},
		Deps:   rn.q.deps(),
		Command: Command{
			Payload:      []byte("x"),
			ConflictKeys: [][]byte{[]byte("k")},
		},
	})

	// Install the resident record in the engine so ProvideRecordLoad can find it.
	foldTestRef(&rn.engine, stored)

	// Build a load request whose Ref does not match the stored Ref.
	rec := stored
	rec.Ref = InstanceRef{Conf: 9, Replica: 9, Instance: 9}

	lane := 0

	_, err = rn.engine.ProvideRecordLoad(lane, rec)
	if err == nil {
		t.Fatalf("expected ProvideRecordLoad to fail for mismatched Ref, got nil error")
	}
}
  1. These tests assume ProvideRecordLoad is a method with signature func (e *engine) ProvideRecordLoad(lane int, rec InstanceRecord) (InstanceRecord, error). If the actual receiver type, lane type, parameter passing (by value vs pointer), or return values differ, adjust the call sites accordingly.
  2. If foldTestRef is not sufficient to make the record discoverable by ProvideRecordLoad, you may need to mirror whatever setup other ProvideRecordLoad tests use (e.g., calling specific engine helpers to install the resident record).
  3. If InstanceRef comparison requires a helper (e.g., EqualInstanceRef) instead of !=/==, replace the direct comparisons with that helper to stay consistent with existing conventions.
  4. If record_load_test.go does not yet import any additional packages (like reflect) and you choose to use them for comparisons, add the imports at the top of the file.
  5. If TestRecordLoadFoundReplaysDeferred contains additional code after the snippet you provided, move the newly added tests so they are defined at file scope (i.e., after the closing brace of that test) to keep the file compiling.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request replaces the O(all instances) conflict and attributes machinery with a per-lane conflict engine, introduces a two-tier executed-instance retirement mechanism with configurable per-lane retention, and implements an asynchronous record-load handshake for folded instances. The feedback is highly constructive and focuses on key performance and robustness improvements: optimizing the retireExecuted function to avoid O(N) map scans, validating returned record references in ProvideRecordLoad to prevent state corruption, avoiding redundant radix tree operations in conflictEngine.apply, simplifying capacity checks in deferRecordLoad, and safeguarding radixLevel against potential bit-shift overflows.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread epaxos/retire.go
Comment on lines +11 to +47
type item struct {
ref InstanceRef
rec InstanceRecord
}
byLane := make(map[instanceLane][]item)
for ref, inst := range n.instances {
if inst == nil || inst.rec.Status != StatusExecuted || !n.executed.contains(ref) {
continue
}
lane := laneFor(ref)
byLane[lane] = append(byLane[lane], item{ref: ref, rec: inst.rec})
}
for lane, items := range byLane {
executedThrough := n.executed.prefix(lane)
if executedThrough == 0 {
continue
}
var target InstanceNum
if executedThrough > retain {
target = executedThrough - retain
}
folded := n.engine.foldedThrough(lane)
if target <= folded {
continue
}
for _, it := range items {
if it.ref.Instance <= folded || it.ref.Instance > target {
continue
}
if n.instances[it.ref] == nil {
continue
}
rec := it.rec.Clone()
n.engine.foldRecord(rec)
delete(n.instances, it.ref)
n.foldedInstances++
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The current implementation of retireExecuted performs a full scan over the entire n.instances map on every Advance call. As the number of resident instances grows (up to MaxResidentInstances), this $O(N)$ scan will become a major performance bottleneck on a critical hot path. Since the executed instances to be retired are contiguous, we can optimize this to $O(1)$ amortized time by iterating over the active lanes and directly looking up the instances in the range (folded, target].

	for lane, index := range n.engine.laneIndex {
		executedThrough := n.executed.prefix(lane)
		if executedThrough == 0 {
			continue
		}
		var target InstanceNum
		if executedThrough > retain {
			target = executedThrough - retain
		}
		folded := index.folded
		if target <= folded {
			continue
		}
		for instNum := folded + 1; instNum <= target; instNum++ {
			ref := InstanceRef{Conf: lane.conf, Replica: lane.replica, Instance: instNum}
			inst := n.instances[ref]
			if inst == nil {
				continue
			}
			if inst.rec.Status != StatusExecuted || !n.executed.contains(ref) {
				continue
			}
			rec := inst.rec.Clone()
			n.engine.foldRecord(rec)
			delete(n.instances, ref)
			n.foldedInstances++
		}

Comment thread epaxos/node.go
Comment on lines +4586 to +4591
if rec.Ref != res.Ref && !res.Ref.IsZero() {
// allow Ref on result to define
if rec.Ref.IsZero() {
rec.Ref = res.Ref
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

If the record returned by the embedding has a different Ref than requested (and is not zero), the current code silently proceeds, which can cause the requested res.Ref to remain uninstalled while another instance is modified. This will lead to infinite request loops or state corruption when replaying deferred messages. We should explicitly validate that the returned record's Ref matches the requested Ref.

	if rec.Ref.IsZero() {
		rec.Ref = res.Ref
	} else if rec.Ref != res.Ref {
		return fmt.Errorf("%w: record Ref %s does not match requested Ref %s", ErrInvalidRecord, rec.Ref, res.Ref)
	}

Comment thread epaxos/conflict_engine.go
Comment on lines +561 to +577
func (e *conflictEngine) apply(prev *InstanceRecord, rec InstanceRecord) {
if prev != nil {
e.remove(prev.Ref, *prev)
}
lane := laneFor(rec.Ref)
index := e.ensureLane(lane)
_, existed := index.resident.slot(rec.Ref.Instance)
index.resident.set(rec.Ref.Instance, slotForRecord(rec))
if !existed {
e.resident++
}
if recordConflictEligible(rec) && !commandHasGlobalConflictScope(rec.Command.Kind) {
for _, key := range rec.Command.ConflictKeys {
e.ensureKeyLane(rec.Ref.Conf, key, lane).postings.insert(rec.Ref.Instance)
}
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

When updating an existing instance record (where prev != nil and prev.Ref == rec.Ref), the current implementation deletes the instance from the radix tree and postings tree, and then immediately re-inserts it. Since the conflict keys of an instance are immutable, this results in highly redundant radix tree deletions and insertions on every state transition. We can optimize this by updating the resident tree directly and only modifying the postings tree if the eligibility status actually changes.

func (e *conflictEngine) apply(prev *InstanceRecord, rec InstanceRecord) {
	lane := laneFor(rec.Ref)
	index := e.ensureLane(lane)
	if prev != nil && prev.Ref != rec.Ref {
		e.remove(prev.Ref, *prev)
		prev = nil
	}
	_, existed := index.resident.slot(rec.Ref.Instance)
	index.resident.set(rec.Ref.Instance, slotForRecord(rec))
	if !existed {
		e.resident++
	}
	prevEligible := prev != nil && recordConflictEligible(*prev) && !commandHasGlobalConflictScope(prev.Command.Kind)
	currEligible := recordConflictEligible(rec) && !commandHasGlobalConflictScope(rec.Command.Kind)
	if prevEligible && !currEligible {
		for _, key := range prev.Command.ConflictKeys {
			e.removeKeyPosting(prev.Ref.Conf, key, lane, prev.Ref.Instance)
		}
	} else if !prevEligible && currEligible {
		for _, key := range rec.Command.ConflictKeys {
			e.ensureKeyLane(rec.Ref.Conf, key, lane).postings.insert(rec.Ref.Instance)
		}
	}
}

Comment thread epaxos/node.go
Comment on lines +4538 to +4567
if n.pendingRecordLoads == nil {
n.pendingRecordLoads = make(map[InstanceRef]*recordLoadWait)
}
wait := n.pendingRecordLoads[ref]
if wait == nil {
if n.pendingRecordLoadMessages >= n.maxDeferredRecordLoads {
return fmt.Errorf("%w: %w", ErrMessageRejected, ErrDeferredRecordLoadFull)
}
wait = &recordLoadWait{}
n.pendingRecordLoads[ref] = wait
// enqueue request into next Ready batch
target := n.readyTarget()
target.RecordLoads = append(target.RecordLoads, ref)
// keep sorted unique later at Ready() freeze; also sort now for stability
sortRefs(target.RecordLoads)
// dedup adjacent
out := target.RecordLoads[:0]
var last InstanceRef
for i, r := range target.RecordLoads {
if i == 0 || r != last {
out = append(out, r)
last = r
}
}
target.RecordLoads = out
wait.requested = true
}
if n.pendingRecordLoadMessages >= n.maxDeferredRecordLoads {
return fmt.Errorf("%w: %w", ErrMessageRejected, ErrDeferredRecordLoadFull)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The capacity check n.pendingRecordLoadMessages >= n.maxDeferredRecordLoads is duplicated inside the wait == nil block and at the end of the function. We can simplify the control flow and avoid redundant map lookups/insertions by checking the capacity limit once at the very beginning of deferRecordLoad.

	if n.pendingRecordLoadMessages >= n.maxDeferredRecordLoads {
		return fmt.Errorf("%w: %w", ErrMessageRejected, ErrDeferredRecordLoadFull)
	}
	if n.pendingRecordLoads == nil {
		n.pendingRecordLoads = make(map[InstanceRef]*recordLoadWait)
	}
	wait := n.pendingRecordLoads[ref]
	if wait == nil {
		wait = &recordLoadWait{}
		n.pendingRecordLoads[ref] = wait
		// enqueue request into next Ready batch
		target := n.readyTarget()
		target.RecordLoads = append(target.RecordLoads, ref)
		// keep sorted unique later at Ready() freeze; also sort now for stability
		sortRefs(target.RecordLoads)
		// dedup adjacent
		out := target.RecordLoads[:0]
		var last InstanceRef
		for i, r := range target.RecordLoads {
			if i == 0 || r != last {
				out = append(out, r)
				last = r
			}
		}
		target.RecordLoads = out
		wait.requested = true
	}

Comment thread epaxos/conflict_engine.go
Comment on lines +75 to +82
func radixLevel(value InstanceNum) uint8 {
for level := uint8(0); level < maxRadixLevel; level++ {
if uint64(value) < uint64(1)<<(radixBits*(level+1)) {
return level
}
}
return maxRadixLevel
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The radixLevel function shifts uint64(1) by radixBits * (level + 1). If maxRadixLevel or radixBits is increased in the future such that the shift count equals or exceeds 64, this will cause undefined behavior or incorrect level computation. We should add a safety check to break early if the shift count is >= 64 to make the function robust and future-proof.

func radixLevel(value InstanceNum) uint8 {
	for level := uint8(0); level < maxRadixLevel; level++ {
		shift := radixBits * (level + 1)
		if shift >= 64 {
			break
		}
		if uint64(value) < uint64(1)<<shift {
			return level
		}
	}
	return maxRadixLevel
}

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d7241afd7d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread epaxos/types.go
MustSync bool
// RecordLoads requests durable InstanceRecords for folded refs referenced by
// inbound messages. Sorted and deduplicated; stable until Advance.
RecordLoads []InstanceRef

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Include RecordLoads when deciding Ready emptiness

Adding RecordLoads here also requires Ready.Empty to treat them as work. As written, a deferred folded-record lookup only appends RecordLoads and leaves MustSync false, so HasReady stays false, ReadyInto returns an empty batch, and Advance rejects a RecordLoads-only acknowledgement as empty. This makes the asynchronous load handshake unreachable whenever a folded ref is first requested.

Useful? React with 👍 / 👎.

Comment thread epaxos/node.go
n.pendingRecordLoads[ref] = wait
// enqueue request into next Ready batch
target := n.readyTarget()
target.RecordLoads = append(target.RecordLoads, ref)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve RecordLoads queued behind a frozen Ready

When awaitAdvance is true, readyTarget() points at nextReady, so this append can queue a load request behind the currently frozen batch. mergeNextReady never moves nextReady.RecordLoads into pendingReady, so after the frozen Ready is advanced the request disappears while pendingRecordLoads still holds the deferred messages; no subsequent Ready asks the embedding to load that record, leaving those messages stuck until the deferred-load capacity is exhausted.

Useful? React with 👍 / 👎.

Comment thread epaxos/node.go
Comment on lines +2861 to +2862
if n.needsRecordLoad(m.Ref) {
return n.deferRecordLoad(m.Ref, m)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Load folded records before value-changing handlers

This folded-record load guard is only added for Prepare/Evidence. If a delayed MsgPreAccept or MsgAccept arrives for an already executed ref at or below foldedThrough, handlePreAccept/handleAccept still see n.instances[m.Ref] == nil and install a fresh PreAccepted/Accepted record over the folded executed summary instead of loading the executed record and replying with the commit, which can regress durable status for a completed instance.

Useful? React with 👍 / 👎.

Comment thread epaxos/node.go
Comment on lines +4586 to +4590
if rec.Ref != res.Ref && !res.Ref.IsZero() {
// allow Ref on result to define
if rec.Ref.IsZero() {
rec.Ref = res.Ref
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject load results for a different record

When Found is true and res.Record.Ref is set to a different non-zero ref, this branch does not reject it; the checksum is then validated for the unrelated record, that record is installed, and the wait for res.Ref is cleared. A stale or mis-keyed storage response can therefore corrupt an unrelated instance while replaying the original deferred messages without their requested folded record; return ErrInvalidRecord unless the loaded record ref is zero or exactly matches res.Ref.

Useful? React with 👍 / 👎.

Comment thread epaxos/node.go
len(rd.Messages) > len(n.frozenReady.Messages) ||
len(rd.BootstrapMessages) > len(n.frozenReady.BootstrapMessages) ||
len(rd.Committed) > len(n.frozenReady.Committed) ||
len(rd.RecordLoads) > len(n.frozenReady.RecordLoads) ||

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Validate RecordLoads contents on Advance

This only bounds the number of acknowledged RecordLoads; unlike the other Ready slices validated below, it never compares the actual refs against n.frozenReady.RecordLoads. If an embedding reuses or mutates the Ready and advances a same-length but different load list, Advance consumes the real frozen request and the original folded messages remain waiting for a record the embedding was no longer asked to fetch.

Useful? React with 👍 / 👎.

Comment thread epaxos/node.go
Comment on lines +4624 to +4625
n.engine.foldRecord(rec)
delete(n.instances, ref)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Forget exact executed refs after refolding loaded records

When ProvideRecordLoad installs an executed folded record, installInstance adds it back to executed.exact even though executed.through already covers that ref. This refold path deletes the resident again but never removes the redundant exact entry, so repeated loads of old folded refs grow the exact map and undermine the retention bound.

Useful? React with 👍 / 👎.

Comment thread epaxos/node.go

n.enqueueExecutedRecords(rd.Committed[:ackedCommitted])
n.applyBootstrapDurability(rd.BootstrapRecords)
n.retireExecuted()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Defer folding until executed markers are durable

For user commands, enqueueExecutedRecords only places the StatusExecuted record into the next Ready batch; this new call folds and deletes the resident before that marker has been persisted and advanced. If a late Prepare/Evidence arrives in that window, the node asks storage for the folded ref and can load the still-committed durable record, then replaying the deferred message can enqueue that committed record after the pending executed marker and leave the instance regressed to committed. Retire only records whose executed marker has already been acknowledged durable.

Useful? React with 👍 / 👎.

@metaphorics
metaphorics force-pushed the feat/executed-instance-retirement branch from fc7391b to fcf1ffe Compare July 14, 2026 17:27
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@metaphorics
metaphorics merged commit 8846137 into main Jul 14, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(epaxos): executed instance retirement

1 participant