Skip to content

test(epaxos): resident-state conflict benchmarks - #8

Closed
metaphorics wants to merge 0 commit into
mainfrom
test/resident-benchmarks
Closed

test(epaxos): resident-state conflict benchmarks#8
metaphorics wants to merge 0 commit into
mainfrom
test/resident-benchmarks

Conversation

@metaphorics

@metaphorics metaphorics commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Summary

BenchmarkComputeAttrsResident1k/100k, StepPreAcceptGrown, RetireExecuted on pre-grown nodes.

Summary by Sourcery

Introduce a per-lane conflict engine and executed-instance retirement with async record-load support, tighten validation and error handling across EPaxos and KV helpers, and add benchmarks, formal/TLA coverage, and stricter lint/CI gates.

New Features:

  • Add a conflictEngine index for per-lane conflict tracking and expose VisitConflicts for low-allocation conflict inspection.
  • Introduce async record load handshake via Ready.RecordLoads and ProvideRecordLoad to recover folded instances referenced by inbound messages.
  • Add executed-instance retirement with configurable per-lane retention and resident-instance backpressure via MaxResidentInstances in Config.
  • Extend the KV timestamp-bounds API and storage helpers to support additional bounded staleness and configuration-history reconstruction semantics.

Enhancements:

  • Refactor core EPaxos record mutation paths to clone-and-apply through conflictEngine, improving invariants and reducing direct map bookkeeping.
  • Tighten validation for configuration, membership outcomes, message timing domains, and checksums, including new ErrInvalidRecord handling.
  • Make RawNode.Tick and various internal timers return errors instead of panicking, updating tests to assert on error paths.
  • Improve error reporting and robustness in faultcampaign and lifecyclecollector harnesses, including HTTP status handling and subprocess diagnostics.
  • Clarify logging and CLI behaviors in kvcheckpoint and faultcampaign tools, making output and failure modes more operator-friendly.

Build:

  • Add .golangci.yml and AGENTS.md to standardize lint configuration and local validation commands for contributors.

CI:

  • Introduce golangci-lint configuration and wire it into CI for the main module and examples/kv, enforcing stricter static checks and security linting.

Documentation:

  • Expand EPAXOS.MD and epaxos/doc.go with documentation for the conflict engine, record-load handshake, and VisitConflicts API.
  • Introduce EPaxosRetirePrefix TLA+ spec and configuration to document and model-check fold/retire prefix monotonicity.
  • Document agent and task-level goals in AGENTS.md and .agent-tasks to guide future work and verification.

Tests:

  • Add conflict_engine, record_load, retire, and visit_conflicts unit tests to validate the new conflict index, folding, and record-load semantics.
  • Extend protocol, recovery, TOQ, DST, hard-state, fuzz, and bootstrap tests to cover new branches, error paths, and boundary conditions.
  • Enhance examples/kv backup, checkpoint, and storage tests to exercise new validation, manifest, and error-handling logic.
  • Add refinementtrace, faultcampaign, lifecyclecollector, and TLA runner tests to check new invariants and model coverage.

Chores:

  • Rename and tidy various test helpers, constants, and struct fields for clarity and consistency without changing semantics.

@sourcery-ai

sourcery-ai Bot commented Jul 14, 2026

Copy link
Copy Markdown

Reviewer's Guide

Replace map-based conflict indexing with a per-lane conflictEngine, introduce folded-history record loading and executed-instance retirement, wire them into EPaxos core, and add resident-state benchmarks plus lint/test hardening.

Sequence diagram for folded record load handshake via Ready.RecordLoads

sequenceDiagram
    actor Embedding
    participant RawNode
    participant Storage

    Embedding->>RawNode: Step(message)
    RawNode->>RawNode: needsRecordLoad(ref)
    alt [needsRecordLoad returns true]
        RawNode->>RawNode: deferRecordLoad(ref, message)
        loop poll
            Embedding->>RawNode: HasReady()
            RawNode-->>Embedding: HasReady()
        end
        Embedding->>Storage: [load durable record for ref]
        Storage-->>Embedding: RecordLoadResult
        Embedding->>RawNode: ProvideRecordLoad(result)
        RawNode->>RawNode: setInstanceRecord(inst, result.Record)
        RawNode->>RawNode: maybeRefoldLoaded(ref)
        RawNode->>RawNode: Step(deferred messages)
    else [needsRecordLoad returns false]
        RawNode->>RawNode: handlePrepare/handleEvidence
    end
Loading

File-Level Changes

Change Details Files
Introduce conflictEngine to manage per-lane conflict/loading state and replace legacy conflict maps.
  • Add conflictEngine/laneTree/postingSet structures for per-lane conflict indexing and key/global views.
  • Replace RawNode.conflicts/allConflicts/globalConflicts maps with conflictEngine field and update all callsites (computeAttrsAt, tryPreAcceptConflict, VisitConflicts, etc.).
  • Track resident instances, folded-through watermarks, and retired sequence breakpoints to answer prefix-max queries without scanning all instances.
  • Update TOQ and other tests to assert against conflictEngine APIs (engine.keyMax/globalMax/residentCount) instead of removed conflictIndex maps.
epaxos/conflict_engine.go
epaxos/node.go
epaxos/conflict_engine_test.go
epaxos/retire.go
epaxos/retire_test.go
epaxos/revisited_test.go
epaxos/conflict_ordering_test.go
epaxos/sparse_progress.go
epaxos/sim_test.go
epaxos/performance_benchmark_test.go
epaxos/toq_test.go
epaxos/protocol_coverage_test.go
epaxos/remaining_test.go
epaxos/internal_test.go
Add folded-record load handshake and Ready.RecordLoads, plus resident backpressure and runtime stats.
  • Introduce Ready.RecordLoads and RecordLoadResult, with validation and cloning support.
  • Add RawNode.needsRecordLoad/deferRecordLoad/ProvideRecordLoad/maybeRefoldLoaded to defer inbound messages for folded refs and replay them after asynchronous record load.
  • Enforce MaxDeferredRecordLoads config bounds and ErrDeferredRecordLoadFull/ErrUnrequestedRecordLoad/ErrInvalidRecord errors; plumb through Step/Advance/RuntimeStats.
  • Add MaxResidentInstances guard in Propose/ProposeConfChange using engine.residentCount and expose PayloadStubInstances/FoldedInstances stats.
epaxos/node.go
epaxos/types.go
epaxos/message.go
epaxos/record_load_test.go
epaxos/hard_state_ready_test.go
epaxos/performance_benchmark_test.go
Implement executed-instance retirement and executedTracker exact-prefix compaction.
  • Add RetainExecutedPerLane config and validation, and store it on RawNode.
  • Extend executedTracker with through map and forgetExactThrough to drop exact entries that are covered by folded prefixes.
  • Add RawNode.retireExecuted, called from Advance, to fold executed instances beyond the per-lane retention tail and advance conflictEngine fold watermarks.
  • Add tests to ensure folded executed prefixes remain logically present (contains/prefix), and that retention interacts correctly with fold watermarks.
epaxos/retire.go
epaxos/sparse_progress.go
epaxos/retire_test.go
epaxos/exhaustion_test.go
Tighten message/record validation, parsing, and bootstrap/codec checks for safety and correctness.
  • Add uvarint8 helpers for message/bootstrap type/reject/status parsing and enforce upper bounds to reject oversized encodings.
  • Strengthen validateStoredInstanceRecord and validateConfChangeResult/validateMembershipResult to treat unspecified outcomes as invalid and simplify special-case conf-change command validation.
  • Harden DecodeBootstrapMessage/DecodeMessage against oversized type/field values and overrun of shared evidence arenas.
  • Tighten EPaxos record/timing encoding/decoding (bounds on status/command kind, file size checks, negative size handling) and fix timestamp-bounds edge cases in kv.TimestampBounds.
epaxos/node.go
epaxos/codec.go
epaxos/bootstrap.go
epaxos/bootstrap_fence_test.go
epaxos/codec_evidence_scratch_test.go
epaxos/message.go
examples/kv/epaxos_storage.go
examples/kv/backup.go
examples/kv/kv.go
examples/kv/epaxos_record_timing_codec_test.go
Expand tests, benchmarks, and CI/lint harness to cover new behaviors and enforce style/security checks.
  • Add BenchmarkComputeAttrsResident1k/100k, BenchmarkStepPreAcceptGrown, and BenchmarkRetireExecuted to measure conflictEngine and retirement costs on pre-grown nodes.
  • Adjust existing tests to propagate Tick errors, handle new Ready.RecordLoads fields, and mark exhaustive or unsafe operations with explicit //nolint pragmas where appropriate.
  • Introduce EPaxosRetirePrefix TLA+ model/spec and wire it into tla_model_check_runner to prove fold-prefix monotonicity for deps/seq.
  • Add golangci-lint config and CI steps (including examples/kv) plus agent instructions and task goals to enforce R9 conventions.
epaxos/performance_benchmark_test.go
epaxos/recovery_test.go
epaxos/remaining_test.go
epaxos/faultsim_harness_test.go
epaxos/fault_campaign_test.go
epaxos/faultsim_trace_test.go
epaxos/faultsim_oracle_test.go
epaxos/stress_test.go
.github/workflows/ci.yml
.golangci.yml
AGENTS.md
.agent-tasks/epaxos-conflict-gc/GOALS.md

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

@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, and left some high level feedback:

  • The maxResidentInstances guard is duplicated in both Propose and ProposeConfChange; consider factoring this into a small helper (e.g. checkResidentCapacity) so future callers don’t have to remember to reapply the same resident-count check.
  • The conflict engine’s advanceFold (and related calls) panic when invariants are violated (e.g. non-contiguous folds); since this is library code, consider returning an error (or at least guarding at call sites and surfacing a typed error) instead of panicking so embeddings can handle corruption or misuse more gracefully.
  • In deferRecordLoad you sort and deduplicate Ready.RecordLoads for every new ref; you could defer this work to the point where the Ready batch is frozen (or only normalize when the slice grows beyond 1 element) to reduce per-message overhead on high-load paths.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The `maxResidentInstances` guard is duplicated in both `Propose` and `ProposeConfChange`; consider factoring this into a small helper (e.g. `checkResidentCapacity`) so future callers don’t have to remember to reapply the same resident-count check.
- The conflict engine’s `advanceFold` (and related calls) panic when invariants are violated (e.g. non-contiguous folds); since this is library code, consider returning an error (or at least guarding at call sites and surfacing a typed error) instead of panicking so embeddings can handle corruption or misuse more gracefully.
- In `deferRecordLoad` you sort and deduplicate `Ready.RecordLoads` for every new ref; you could defer this work to the point where the Ready batch is frozen (or only normalize when the slice grows beyond 1 element) to reduce per-message overhead on high-load paths.

## Individual Comments

### Comment 1
<location path="epaxos/node.go" line_range="4733-4666" />
<code_context>
+	})
+}
+
 func (n *RawNode) computeAttrsAt(cmd Command, exclude InstanceRef, processAt uint64, timedPreAccept bool) Attributes {
 	conf := n.confFor(exclude.Conf)
 	deps := make([]InstanceNum, len(conf.Voters))
</code_context>
<issue_to_address>
**question (bug_risk):** Timed PreAccept now uses conflict-key–scoped walks instead of the previous broad lane scan; this changes dependency selection semantics and may need a deliberate review.

The old `timedPreAccept` path walked all resident instances and added eligible conflicts regardless of `ConflictKeys`. With the new `conflictEngine` integration, non-global commands now depend on `keyLaneSet`/`walkKeyDesc` keyed by `cmd.ConflictKeys`, and global commands on `maxEligibleAny`/`walkDesc`. This means TOQ-timed proposals now only depend on what the engine deems conflicting by key/global scope. Please confirm that all command types using timed PreAccept have complete `ConflictKeys` coverage, and that we don’t have workloads where partial/missing key tagging previously relied on the broader scan for safety.
</issue_to_address>

### Comment 2
<location path="epaxos/node.go" line_range="4653-4662" />
<code_context>
+// VisitConflicts yields resident in-flight instances that conflict with cmd.
+// Folded history is not enumerated. yield returns false to stop early.
+// The walk is designed to avoid heap allocation in the steady state.
+func (n *RawNode) VisitConflicts(cmd Command, yield func(InstanceRef, Status) bool) {
+	if n == nil || yield == nil || cmd.Kind == CommandNoop {
+		return
+	}
+	confID := n.currentHardState.Conf.ID
+	stop := false
+	visit := func(ref InstanceRef, status Status) bool {
+		if !yield(ref, status) {
+			stop = true
+			return false
+		}
+		return true
+	}
+	if commandHasGlobalConflictScope(cmd.Kind) {
+		n.engine.lanes(confID, func(lane instanceLane) bool {
+			if stop {
+				return false
</code_context>
<issue_to_address>
**suggestion (bug_risk):** VisitConflicts may return the same conflicting instance more than once due to overlapping key/global walks.

In the non-global path, `VisitConflicts` walks key-specific postings via `keyLaneSet`/`walkKeyDesc`, then all lanes again via `globalMax`/`walkGlobalDesc`, without deduplication. A resident with both key postings and global conflict scope (or shared lanes across keys) can therefore be yielded multiple times. If callers assume each `InstanceRef` appears only once, this can break embedding logic. Please either document that duplicates are expected or add a small `map[InstanceRef]struct{}` (when `cmd.Kind` != `CommandNoop`) to ensure each ref is yielded at most once.

Suggested implementation:

```golang
	confID := n.currentHardState.Conf.ID
	stop := false

	// seen ensures that each conflicting instance is yielded at most once,
	// even if it is discovered via overlapping walks (e.g., key-specific
	// and global conflict scopes).
	seen := make(map[InstanceRef]struct{})

	visit := func(ref InstanceRef, status Status) bool {
		if _, ok := seen[ref]; ok {
			// Already yielded this instance; skip.
			return true
		}
		seen[ref] = struct{}{}

		if !yield(ref, status) {
			stop = true
			return false
		}
		return true
	}

```

To guarantee de-duplication across all conflict walks, ensure that every place in `VisitConflicts` that currently calls `yield(ref, status)` instead calls `visit(ref, status)`. From the provided snippet this already appears to be the intended pattern, but if any direct `yield` calls remain later in the function, they should be replaced with `visit` so the `seen` map can filter duplicates.
</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/node.go
}
return true
}
if commandHasGlobalConflictScope(cmd.Kind) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

question (bug_risk): Timed PreAccept now uses conflict-key–scoped walks instead of the previous broad lane scan; this changes dependency selection semantics and may need a deliberate review.

The old timedPreAccept path walked all resident instances and added eligible conflicts regardless of ConflictKeys. With the new conflictEngine integration, non-global commands now depend on keyLaneSet/walkKeyDesc keyed by cmd.ConflictKeys, and global commands on maxEligibleAny/walkDesc. This means TOQ-timed proposals now only depend on what the engine deems conflicting by key/global scope. Please confirm that all command types using timed PreAccept have complete ConflictKeys coverage, and that we don’t have workloads where partial/missing key tagging previously relied on the broader scan for safety.

Comment thread epaxos/node.go
Comment on lines +4653 to +4662
func (n *RawNode) VisitConflicts(cmd Command, yield func(InstanceRef, Status) bool) {
if n == nil || yield == nil || cmd.Kind == CommandNoop {
return
}
confID := n.currentHardState.Conf.ID
stop := false
visit := func(ref InstanceRef, status Status) bool {
if !yield(ref, status) {
stop = true
return false

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 (bug_risk): VisitConflicts may return the same conflicting instance more than once due to overlapping key/global walks.

In the non-global path, VisitConflicts walks key-specific postings via keyLaneSet/walkKeyDesc, then all lanes again via globalMax/walkGlobalDesc, without deduplication. A resident with both key postings and global conflict scope (or shared lanes across keys) can therefore be yielded multiple times. If callers assume each InstanceRef appears only once, this can break embedding logic. Please either document that duplicates are expected or add a small map[InstanceRef]struct{} (when cmd.Kind != CommandNoop) to ensure each ref is yielded at most once.

Suggested implementation:

	confID := n.currentHardState.Conf.ID
	stop := false

	// seen ensures that each conflicting instance is yielded at most once,
	// even if it is discovered via overlapping walks (e.g., key-specific
	// and global conflict scopes).
	seen := make(map[InstanceRef]struct{})

	visit := func(ref InstanceRef, status Status) bool {
		if _, ok := seen[ref]; ok {
			// Already yielded this instance; skip.
			return true
		}
		seen[ref] = struct{}{}

		if !yield(ref, status) {
			stop = true
			return false
		}
		return true
	}

To guarantee de-duplication across all conflict walks, ensure that every place in VisitConflicts that currently calls yield(ref, status) instead calls visit(ref, status). From the provided snippet this already appears to be the intended pattern, but if any direct yield calls remain later in the function, they should be replaced with visit so the seen map can filter duplicates.

@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(\text{all instances})$ conflict machinery with a per-lane conflict engine, introduces a two-tier executed-instance retirement mechanism to bound resident memory, and implements an asynchronous record-load handshake to recover folded records. Feedback on these changes highlights a critical bug in ProvideRecordLoad where a mismatch between the loaded record's Ref and the requested res.Ref could cause an infinite loop of record loads. Additionally, optimization opportunities were identified to avoid global map iterations in retireExecuted and to eliminate redundant heap allocations from unnecessary record cloning during apply and fold operations.

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/node.go
Comment on lines +4585 to +4591
rec := res.Record
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.

critical

If the loaded record's Ref does not match the requested res.Ref, installing it under rec.Ref while clearing res.Ref from pendingRecordLoads will cause the replayed messages for res.Ref to trigger another record load request. This can lead to an infinite loop of record loads.\n\nWe should strictly validate that the loaded record's Ref matches res.Ref (or populate it if it is zero) and return ErrInvalidRecord if they mismatch.

Suggested change
rec := res.Record
if rec.Ref != res.Ref && !res.Ref.IsZero() {
// allow Ref on result to define
if rec.Ref.IsZero() {
rec.Ref = res.Ref
}
}
rec := res.Record
if rec.Ref.IsZero() {
rec.Ref = res.Ref
}
if rec.Ref != res.Ref {
return ErrInvalidRecord
}

Comment thread epaxos/retire.go
Comment on lines +11 to +59
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++
}
contagious := folded
for next := folded + 1; next <= target; next++ {
if !n.engine.canAdvanceFold(lane, next) {
break
}
contagious = next
}
if contagious > folded {
n.engine.advanceFold(lane, contagious)
n.executed.forgetExactThrough(lane, contagious)
}
}

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 iterates over all resident instances in n.instances on every call, which scales as $O(\text{resident instances})$ and can become a significant performance bottleneck as the resident set grows. Additionally, calling Clone() on each retired record introduces unnecessary heap allocations.\n\nWe can optimize this by iterating over the active lanes in n.engine.laneIndex and checking only the sequential range of instances [folded + 1, target] for each lane. This avoids the global map iteration entirely and allows us to safely pass inst.rec directly to foldRecord without cloning.

	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 || inst.rec.Status != StatusExecuted || !n.executed.contains(ref) {
				continue
			}
			n.engine.foldRecord(inst.rec)
			delete(n.instances, ref)
			n.foldedInstances++
		}
		contagious := folded
		for next := folded + 1; next <= target; next++ {
			if !n.engine.canAdvanceFold(lane, next) {
				break
			}
			contagious = next
		}
		if contagious > folded {
			n.engine.advanceFold(lane, contagious)
			n.executed.forgetExactThrough(lane, contagious)
		}
	}

Comment thread epaxos/node.go
Comment on lines +4641 to +4642
previous := inst.rec.Clone()
n.engine.apply(&previous, rec)

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

Calling inst.rec.Clone() here is unnecessary because n.engine.apply only reads from the previous record and does not modify or retain it. Passing &inst.rec directly avoids redundant heap allocations of slice fields.

Suggested change
previous := inst.rec.Clone()
n.engine.apply(&previous, rec)
n.engine.apply(&inst.rec, rec)

Comment thread epaxos/node.go
Comment on lines +4623 to +4624
rec := inst.rec.Clone()
n.engine.foldRecord(rec)

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

Calling inst.rec.Clone() here is unnecessary because n.engine.foldRecord only reads from the record and does not modify or retain it. Passing inst.rec directly avoids redundant heap allocations.

Suggested change
rec := inst.rec.Clone()
n.engine.foldRecord(rec)
n.engine.foldRecord(inst.rec)

@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: 2ac9a225e5

ℹ️ 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 in Ready.Empty

When a folded-ref Prepare/Evidence is deferred, the only work made visible to the embedding can be this new RecordLoads slice. Ready.Empty still ignores it, so HasReady/ReadyInto treat the node as idle and return an empty Ready instead of the load request, leaving the message stuck in pendingRecordLoads with no way for the application to call ProvideRecordLoad.

Useful? React with 👍 / 👎.

Comment thread epaxos/node.go
Comment on lines +4549 to +4550
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.

P2 Badge Merge deferred RecordLoads after the frozen Ready

When deferRecordLoad runs while a Ready is already frozen, readyTarget() returns nextReady, so this appends the request to n.nextReady.RecordLoads. The later mergeNextReady path merges records/messages/committed work but never moves RecordLoads, so after the current Ready is advanced the load request disappears while the corresponding messages remain deferred in pendingRecordLoads.

Useful? React with 👍 / 👎.

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.

P2 Badge Reject mismatched record-load results

If storage responds Found=true for request res.Ref but returns a record whose non-zero Record.Ref is different, this branch falls through: the checksum is validated for that other record, it is installed, and the wait for res.Ref is deleted before replaying messages for the original folded ref. That silently loses or re-defers the requested load instead of reporting an invalid result.

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 Compare RecordLoads when advancing Ready

For Ready batches that include RecordLoads alongside other work, Advance only checks the count and never verifies that the acknowledged refs match n.frozenReady.RecordLoads, unlike the equality checks for records/messages/committed entries below. A stale or out-of-order ack with the same length can consume the load request for a different folded ref, leaving the actual deferred messages waiting for a load the embedding was never asked to perform.

Useful? React with 👍 / 👎.

Comment thread epaxos/retire.go
}
rec := it.rec.Clone()
n.engine.foldRecord(rec)
delete(n.instances, it.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 Route value messages for folded refs through record loads

After this deletes an executed instance from n.instances, delayed value-carrying messages for the same folded ref (PreAccept, Accept, TryPreAccept, or Commit) are treated as unknown because only the Prepare/Evidence handlers call needsRecordLoad. In a long partition after the retention tail moves, such a retry can install and persist a pre-executed/committed record over the durable executed record, resurrecting or downgrading an already applied instance.

Useful? React with 👍 / 👎.

Comment thread epaxos/node.go
n.frozenReady.Messages = consumeReadyPrefix(n.frozenReady.Messages, len(rd.Messages))
n.frozenReady.BootstrapMessages = consumeReadyPrefix(n.frozenReady.BootstrapMessages, len(rd.BootstrapMessages))
n.frozenReady.Committed = consumeReadyPrefix(n.frozenReady.Committed, len(rd.Committed))
n.frozenReady.RecordLoads = consumeReadyPrefix(n.frozenReady.RecordLoads, len(rd.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 Service RecordLoads before advancing Ready

The new load requests are consumed here as soon as the caller advances the Ready, but the repository's Ready drivers I checked (for example examples/kv/cmd/kvnode/main.go's drainLocked) still only apply records, send messages, and then call Advance without loading these refs or calling ProvideRecordLoad. Any mixed Ready containing both normal work and RecordLoads will therefore drop the load request while the deferred messages remain stuck.

Useful? React with 👍 / 👎.

@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 force-pushed the test/resident-benchmarks branch from c260e96 to 4b34960 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.

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.

1 participant