Skip to content

perf(epaxos): integrate conflict engine; remove O(n) index#3

Merged
metaphorics merged 14 commits into
mainfrom
perf/conflict-engine-integration
Jul 14, 2026
Merged

perf(epaxos): integrate conflict engine; remove O(n) index#3
metaphorics merged 14 commits into
mainfrom
perf/conflict-engine-integration

Conversation

@metaphorics

@metaphorics metaphorics commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Solution / proposal

Wire RawNode through setInstanceRecord; rewrite computeAttrsAt/tryPreAcceptConflict over key/global walks; prune global walks via globalMax; delete old conflict maps (residue empty).

Closes #

Summary by Sourcery

Integrate a per-lane conflict engine for EPaxos, retire the legacy conflict indexes, and tighten safety, testing, and tooling around conflict handling, timing, and checkpoints.

Enhancements:

  • Replace map-based conflict tracking with a radix-tree-based conflictEngine and adapt attribute computation and TryPreAccept conflict detection to use it.
  • Wire conflictEngine updates through all instance lifecycle transitions, including prepare/accept/commit, execution, and retirement, with fold-based aggregation of retired instances.
  • Harden bootstrap, message decoding, checkpoint, and lifecycle logic with additional validation, bounds checks, and clearer error paths, including new tests for malformed inputs.
  • Refine timing, recovery, and TOQ handling to avoid panics, enforce error returns (e.g., Tick and timers), and preserve protocol invariants under edge conditions.
  • Introduce AGENTS and task GOALS documentation to describe conflict-engine work and contributor expectations.

Build:

  • Add a shared golangci-lint configuration and run golangci-lint (including examples/kv) in CI to enforce stricter static analysis and security checks.

CI:

  • Extend the GitHub Actions CI workflow to run golangci-lint on the main module and the kv example module before existing verification gates.

Documentation:

  • Document agent runbooks and per-task goals (AGENTS.md and .agent-tasks) to guide development and verification of the conflict engine and related changes.

Tests:

  • Add comprehensive conflict_engine property tests plus targeted tests for bootstrap message decoding, codec bounds, retirement behavior, and fault/finality paths.
  • Update existing EPaxos, TOQ, recovery, DST, stress, refinement, and lifecycle tests to work with the new conflict engine, stricter Tick/timer error handling, and tightened validation.
  • Add regression tests for malformed and oversized wire input, configuration history reconstruction, and checkpoint manifest/file validation.

Closes #10

Drop govet enable-all; keep revive/gosec/exhaustive enabled for R9.
Make epaxos package lint-clean under the U1 strict config: errcheck on Tick
paths, revive renames, intentional test nolints for G115/G304/exhaustive
subsets, and leave production wire G115 checks intact.
Root golangci-lint does not load the nested module; add a second CI step
and document the local dual-module lint commands in AGENTS.md.
Mirror parser.uvarint8 for bootstrap envelopes so oversized wire types
cannot truncate into the valid BootstrapMessageType band.
Re-check depsCount against maxWireDeps before int conversion and
arena slicing so a hostile evidence frame cannot panic the node.
MkdirAll/WriteFile already use restrictive 0o750/0o600; the G115
suppressions were false and would hide future mode regressions.
Standalone per-lane sparse radix max-seq index and per-key postings with
fold summaries; property tests prove oracle equivalence and fold domination.
Wire RawNode through setInstanceRecord/installInstance, rewrite
computeAttrsAt and tryPreAcceptConflict over key/global posting walks,
and delete the old conflict maps with zero residue.
walkGlobalDesc no longer scans unrelated residents; subtrees with
globalMax==0 are skipped. Add sparse-gap visit-counter regression test.
@sourcery-ai

sourcery-ai Bot commented Jul 14, 2026

Copy link
Copy Markdown

Reviewer's Guide

Replaces the legacy O(n) conflict indexing in the EPaxos RawNode with a new per-lane conflictEngine, rewrites attribute and conflict computations to use it (including TOQ paths), introduces folding/retirement of executed instances, tightens validation, error handling, and tests, and wires stricter linting plus CI for golangci-lint.

Sequence diagram for TryPreAccept conflict detection with conflictEngine

sequenceDiagram
actor Client
participant RawNode
participant conflictEngine
participant Instances as instances[InstanceRef]

Client->>RawNode: propose / TryPreAccept(Message)
RawNode->>RawNode: tryPreAcceptConflict(m)

opt commandHasGlobalConflictScope
  RawNode->>conflictEngine: lanes(m.Ref.Conf, yield)
  loop per lane
    conflictEngine-->>RawNode: maxEligibleAny(lane)
    RawNode->>conflictEngine: walkDesc(lane, resident, yield)
    conflictEngine-->>RawNode: (instance, laneSlot)
    RawNode->>Instances: lookup InstanceRef
    RawNode->>RawNode: considerRecord(ref, instance)
  end
end

opt key-scoped conflicts
  RawNode->>conflictEngine: keyLaneSet(m.Ref.Conf, Command.ConflictKeys, yield)
  loop per lane/key
    conflictEngine-->>RawNode: keyMax(conf, key, lane)
    RawNode->>conflictEngine: walkKeyDesc(conf, key, lane, resident, yield)
    conflictEngine-->>RawNode: (instance, laneSlot)
    RawNode->>Instances: lookup InstanceRef
    RawNode->>RawNode: considerRecord(ref, instance)
  end
  RawNode->>conflictEngine: lanes(m.Ref.Conf, yield)
  loop global lanes
    conflictEngine-->>RawNode: globalMax(lane)
    RawNode->>conflictEngine: walkGlobalDesc(lane, resident, yield)
    conflictEngine-->>RawNode: (instance, laneSlot)
    RawNode->>Instances: lookup InstanceRef
    RawNode->>RawNode: considerRecord(ref, instance)
  end
end

RawNode-->>Client: conflictRef, conflictStatus, found
Loading

File-Level Changes

Change Details Files
Replace map-based conflict indexes in RawNode with a conflictEngine and route all record mutations through it.
  • Remove conflicts/allConflicts/globalConflicts maps and indexConflicts/rebuildConflictLane helpers from RawNode.
  • Introduce conflictEngine, laneTree, postingSet, and laneIndex types to track per-lane conflict slots and key/global postings with radix trees and retired breakpoints.
  • Add RawNode.setInstanceRecord and make installInstance, commit, startAccept, startTryPreAccept, TOQ handlers, and recovery paths update records via conflictEngine.apply/remove instead of direct rec mutation or indexConflicts.
  • Expose conflictEngine query helpers (maxEligibleAny, globalMax, keyMax, walkDesc, walkKeyDesc, walkGlobalDesc, keyLaneSet, lanes, prefixMaxSeq, foldRecord, advanceFold, residentCount), and adjust tests to use engine APIs instead of old conflictIndex maps.
epaxos/node.go
epaxos/conflict_engine.go
epaxos/conflict_engine_test.go
epaxos/sparse_progress.go
epaxos/conflict_ordering_test.go
epaxos/revisited_test.go
epaxos/toq_test.go
epaxos/protocol_coverage_test.go
epaxos/remaining_test.go
epaxos/branch_test.go
epaxos/performance_benchmark_test.go
Rewrite computeAttrsAt and tryPreAcceptConflict to use conflictEngine walks and prefix-max sequence calculations instead of scanning all instances or map maxima.
  • Change computeAttrsAt to early-return for no-op commands, then derive deps by walking per-key postings and global residents via conflictEngine rather than scanning n.instances and conflict maps.
  • Use conflictEngine.prefixMaxSeq with retired breakpoint compression to compute seq from deps, ensuring folded instances still influence ordering.
  • Reimplement tryPreAcceptConflict to traverse conflictEngine per lane and key/global view (including retired executed instances) using attrsDependsOn and accept-evidence guards, instead of sorting refs and scanning.
  • Teach the engine to track retiredEligibleAny and retiredGlobal and ensure retired instances participate in conflict/attr computation until folded.
epaxos/node.go
epaxos/conflict_engine.go
epaxos/conflict_engine_test.go
epaxos/sparse_progress.go
epaxos/revisited_test.go
epaxos/toq_test.go
epaxos/performance_benchmark_test.go
Integrate executed-instance retirement with conflictEngine and executedTracker, including folding and payload-drop behavior.
  • Adjust tryExecute to clone, mark StatusExecuted, apply configuration/membership effects, recompute Checksum, and call setInstanceRecord so conflictEngine state stays coherent as instances execute.
  • Extend laneIndex to track folded watermark, pendingFold postings, retiredEligibleAny/global, and retiredSeq breakpoints; add foldRecord and advanceFold to move executed instances out of resident indexes while preserving sequence bounds.
  • Update sparse_progress and retirement-related tests to work with new folding and to assert prefixMaxSeq and walk behavior across retirement, including max-InstanceNum or sparse frontiers.
  • Keep residentCount for observability and adapt tests that previously inspected len(rn.conflicts) to use engine.residentCount().
epaxos/sparse_progress.go
epaxos/node.go
epaxos/conflict_engine.go
epaxos/conflict_engine_test.go
epaxos/remaining_test.go
epaxos/sim_test.go
epaxos/pool_ownership_test.go
Tighten message/bootstrap/record validation, decoding, and error paths to be size- and type-safe, and add negative tests.
  • Change message and bootstrap codecs to read type-like enums via bounded uvarint8 (MessageType, RejectReason, Status, CommandKind, BootstrapMessageType) and enforce max deps/evidence/key lengths and arena bounds.
  • Add fuzz/negative tests for oversized wire message/Bootstrap types and invalid timing domains, and ensure DecodeMessage/DecodeBootstrapMessage return ErrInvalidMessage/ErrInvalidBootstrapMessage without panicking or leaving residue.
  • Refine Message.Validate to be more exhaustive and precise for timing-domain, reject reason, metadata combinations, and extend various switch statements with explicit default/fallthrough safe cases.
  • Harden kv epaxos record codec and checkpoint manifest parsing: bound string/file counts and sizes, reject invalid command/status kinds and timing metadata, and fix timestamp-bound classification for latest/within modes.
epaxos/codec.go
epaxos/bootstrap.go
epaxos/message.go
epaxos/codec_evidence_scratch_test.go
epaxos/bootstrap_fence_test.go
examples/kv/epaxos_storage.go
examples/kv/epaxos_record_timing_codec_test.go
epaxos/malformed_fuzz_test.go
examples/kv/backup.go
Make Tick and timers error-returning where appropriate and fix tests to handle errors instead of ignoring sentinel ErrLogicalTimeExhausted.
  • Change Tick and onTimer/schedule paths to return error in failure cases and adjust callers to panic or propagate errors; restartTimerError now uses errors.Is for ErrLogicalTimeExhausted and treats max-tick exhaustion as benign at restart.
  • Update many tests to call rn.Tick()/rn.onTimer()/startPrepare/handle*Resp via error-checked paths (failing or panic on unexpected errors) and to treat logical exhaustion as non-fatal.
  • Ensure startAccept/startTryPreAccept restarts panic on schedule error with comment that Tick prevents logical exhaustion before scheduling.
  • Adjust recovery and timing tests to expect Tick errors and handle them via t.Fatal rather than silently ignoring.
epaxos/node.go
epaxos/recovery_test.go
epaxos/remaining_test.go
epaxos/protocol_coverage_test.go
epaxos/branch_test.go
epaxos/hard_state_ready_test.go
epaxos/internal_test.go
epaxos/sim_test.go
epaxos/stress_test.go
epaxos/dst_test.go
Improve fault-injection, lifecycle, and fault-campaign harness robustness, error messages, and security posture (lint- and gosec-driven).
  • Strengthen HTTP client/server code paths in faultcampaign and lifecyclecollector (timeouts, response-body Close with error suppression, clearer wrapped errors, consistent status checking).
  • Mark benign uses of weak RNG, exec.Command, path traversal, slice indexing, and byte conversions with appropriate nolint comments (gosec, gocritic, staticcheck) where they are test-only or controlled.
  • Refine process controller and TLS verification utilities to better report authorization and metrics failures; tighten log file permissions and directory modes where appropriate.
  • Adjust backup/checkpoint/lifecycle tests to add explicit nolint annotations on deliberate file mode or path manipulations and restructure error handling for clarity.
epaxos/faultsim_harness_test.go
epaxos/faultsim_trace_test.go
epaxos/faultsim_oracle_test.go
tests/faultcampaign/campaign.go
tests/faultcampaign/process.go
tests/faultcampaign/proxy.go
tests/faultcampaign/proxy_test.go
tests/lifecyclecollector/collect.go
tests/lifecyclecollector/model.go
tests/lifecyclecollector/finalize.go
examples/kv/cmd/kvcheckpoint/main.go
examples/kv/backup.go
examples/kv/backup_test.go
Add golangci-lint to CI and configure linting, plus small style/usage fixes to make the repo lint-clean.
  • Introduce .golangci.yml with explicit enabled linters (errcheck, errorlint, exhaustive, gocritic, gosec, govet, ineffassign, revive, staticcheck, unused, wastedassign) and staticcheck settings.
  • Update GitHub Actions workflow to run golangci-lint twice (root and examples/kv) before the existing tests/ci.sh gate.
  • Apply style tweaks required by linters: use errors.Is, avoid writing to os.Stderr without checking, handle fmt.Fprintln/Fprintf errors with _ assignment, refine switch exhaustiveness with nolint:exhaustive tags, and collapse some slice appends.
  • Add AGENTS.md and task GOALS file to describe required test / lint commands and the conflict-engine task intent for automated agents.
.github/workflows/ci.yml
.golangci.yml
AGENTS.md
.agent-tasks/epaxos-conflict-gc/GOALS.md
examples/kv/cmd/kvcheckpoint/main.go
tests/faultcampaign/artifacts.go
tests/refinementtrace/cmd/refinementtrace/main.go
epaxos/faultcampaign_test.go
tests/refinementtrace/scenarios.go
epaxos/pool_ownership_test.go
tests/lifecyclecollector/collector_test.go

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 #10

@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 using radix trees and posting sets to track resident and retired instances. It integrates this engine into RawNode to optimize attribute computation and conflict checks, while also enforcing strict linting. The review comments are highly constructive and should be kept: they suggest removing unnecessary pointer indirection on the keyLanes map to reduce allocations, and optimizing descending traversals in computeAttrsAt to terminate early once the maximum conflicting instance is found.

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/conflict_engine.go

type conflictEngine struct {
laneIndex map[instanceLane]*laneIndex
byKey map[ConfID]map[string]*keyLanes

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 byKey map is defined as map[ConfID]map[string]*keyLanes, where keyLanes is itself a map type (map[instanceLane]*keyLane). In Go, maps are reference types, so using a pointer to a map (*keyLanes) introduces unnecessary indirection and an extra allocation per unique key during insertion. Changing byKey to use keyLanes directly (i.e., map[ConfID]map[string]keyLanes) would make the code more idiomatic, improve readability, and reduce allocations.

Suggested change
byKey map[ConfID]map[string]*keyLanes
byKey map[ConfID]map[string]keyLanes

Comment thread epaxos/node.go
Comment on lines +4515 to +4569
walkKey := func(lane instanceLane, key []byte, from InstanceNum) {
if from == 0 || lane.conf != exclude.Conf {
return
}
n.engine.walkKeyDesc(exclude.Conf, key, lane, from, func(instance InstanceNum, slot laneSlot) bool {
if !slot.eligible() {
return true
}
ref := InstanceRef{Conf: lane.conf, Replica: lane.replica, Instance: instance}
inst := n.instances[ref]
if inst != nil && addRef(ref, inst.rec) {
continue
if inst == nil {
return true
}
addFallbackLane(lane)
}
_ = addRef(ref, inst.rec)
// continue: need max across candidates; do not stop at first
return true
})
}
if cmd.Kind == CommandNoop {
return Attributes{Seq: seq, Deps: deps}
walkGlobal := func(lane instanceLane, from InstanceNum) {
if from == 0 || lane.conf != exclude.Conf {
return
}
n.engine.walkGlobalDesc(lane, from, func(instance InstanceNum, _ laneSlot) bool {
ref := InstanceRef{Conf: lane.conf, Replica: lane.replica, Instance: instance}
inst := n.instances[ref]
if inst == nil {
addRetired(lane, instance)
return true
}
_ = addRef(ref, inst.rec)
return true
})
}

if commandHasGlobalConflictScope(cmd.Kind) {
addIndexedLanes(n.allConflicts)
if len(n.allConflicts) == 0 {
for ref, inst := range n.instances {
if inst != nil {
addRef(ref, inst.rec)
}
}
}
} else if timedPreAccept {
for ref, inst := range n.instances {
if inst != nil {
addRef(ref, inst.rec)
n.engine.lanes(exclude.Conf, func(lane instanceLane) bool {
resident, retired := n.engine.maxEligibleAny(lane)
// Global-scope commands depend on any eligible conflict, not only global-flagged records.
if resident != 0 {
n.engine.walkDesc(lane, resident, func(instance InstanceNum, slot laneSlot) bool {
if !slot.eligible() {
return true
}
ref := InstanceRef{Conf: lane.conf, Replica: lane.replica, Instance: instance}
inst := n.instances[ref]
if inst == nil {
return true
}
_ = addRef(ref, inst.rec)
return true
})
}
}
addRetired(lane, retired)
return true
})

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

In computeAttrsAt, walkKey and walkGlobal traverse the conflict engine's postings and global lists in descending order of InstanceNum to find the maximum conflicting instance for each lane. Since the lists are traversed in descending order, the first conflicting instance encountered is guaranteed to have the largest InstanceNum for that lane. Once a dependency is successfully added via addRef (or addRetired), any subsequent instances in the same lane will have smaller instance numbers and cannot increase deps[idx]. We can optimize these walks to terminate early by returning false from the yield function as soon as a dependency is found, avoiding unnecessary traversal of the remaining elements in the list.

	walkKey := func(lane instanceLane, key []byte, from InstanceNum) {
		if from == 0 || lane.conf != exclude.Conf {
			return
		}
		n.engine.walkKeyDesc(exclude.Conf, key, lane, from, func(instance InstanceNum, slot laneSlot) bool {
			if !slot.eligible() {
				return true
			}
			ref := InstanceRef{Conf: lane.conf, Replica: lane.replica, Instance: instance}
			inst := n.instances[ref]
			if inst == nil {
				return true
			}
			return !addRef(ref, inst.rec)
		})
	}
	walkGlobal := func(lane instanceLane, from InstanceNum) {
		if from == 0 || lane.conf != exclude.Conf {
			return
		}
		n.engine.walkGlobalDesc(lane, from, func(instance InstanceNum, _ laneSlot) bool {
			ref := InstanceRef{Conf: lane.conf, Replica: lane.replica, Instance: instance}
			inst := n.instances[ref]
			if inst == nil {
				addRetired(lane, instance)
				return false
			}
			return !addRef(ref, inst.rec)
		})
	}

	if commandHasGlobalConflictScope(cmd.Kind) {
		n.engine.lanes(exclude.Conf, func(lane instanceLane) bool {
			resident, retired := n.engine.maxEligibleAny(lane)
			// Global-scope commands depend on any eligible conflict, not only global-flagged records.
			if resident != 0 {
				n.engine.walkDesc(lane, resident, func(instance InstanceNum, slot laneSlot) bool {
					if !slot.eligible() {
						return true
					}
					ref := InstanceRef{Conf: lane.conf, Replica: lane.replica, Instance: instance}
					inst := n.instances[ref]
					if inst == nil {
						return true
					}
					return !addRef(ref, inst.rec)
				})
			}
			addRetired(lane, retired)
			return true
		})

@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 1 issue

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

## Individual Comments

### Comment 1
<location path=".github/workflows/ci.yml" line_range="55-64" />
<code_context>
           chmod +x /tmp/lein
           echo /tmp >> "$GITHUB_PATH"

+      - name: Run golangci-lint
+        uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9
+        with:
+          version: v2.12.2
+
+      - name: Run golangci-lint (examples/kv)
+        uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9
+        with:
+          version: v2.12.2
+          working-directory: examples/kv
+
       - name: Run repository verification gates
</code_context>
<issue_to_address>
**issue (bug_risk):** golangci-lint configuration and CI Go version may not match supported tooling versions

`.golangci.yml` sets `run.go: "1.26"`, but that Go version does not exist. golangci-lint will try to use this to pick a toolchain, which can cause failures or unexpected defaults, especially with `golangci-lint-action@v2.12.2` that may not recognize it. Please set `run.go` to an actually supported Go version that matches the repo and CI toolchain so lint runs remain stable.
</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 .github/workflows/ci.yml
Comment on lines +55 to +64
- name: Run golangci-lint
uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9
with:
version: v2.12.2

- name: Run golangci-lint (examples/kv)
uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9
with:
version: v2.12.2
working-directory: examples/kv

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue (bug_risk): golangci-lint configuration and CI Go version may not match supported tooling versions

.golangci.yml sets run.go: "1.26", but that Go version does not exist. golangci-lint will try to use this to pick a toolchain, which can cause failures or unexpected defaults, especially with golangci-lint-action@v2.12.2 that may not recognize it. Please set run.go to an actually supported Go version that matches the repo and CI toolchain so lint runs remain stable.

@metaphorics
metaphorics merged commit 83ab238 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.

perf(epaxos): integrate conflict engine

1 participant