◐ Off-By-One · answer catalog

raft-log-compaction-snapshot-v6

1 answer(s)gogo1.26

unknown

📦 Source in repository (JSON)

Answer

The implementation consists of a complete Raft log compaction and InstallSnapshot RPC mechanism in Go. The code is split into two files:

raft_snapshot.go — Core implementation; raft_snapshot_test.go — 8 unit tests covering all invariants.

Architecture

┌─────────────────────────────────────────────────────┐
│                  Leader                              │
│                                                      │
│  checkSnapshotThreshold() ───► Snapshot()            │
│       │                      └─ persistSnapshot()    │
│       │                         trims log, clamps    │
│       │                         commit/lastApplied   │
│       ▼                                              │
│  leaderSendSnapshotIfNeeded(server)                   │
│       │                                              │
│       ├─ nextIndex ≤ snapshotIndex?                  │
│       │    └─ yes → sendInstallSnapshot(server)      │
│       └─ no  → sendAppendEntries(server)             │
│                                                      │
│  sendInstallSnapshot: builds InstallSnapshotArgs,    │
│  calls RPC, on success updates nextIndex/matchIndex  │
└──────────────────────┬──────────────────────────────┘
                       │ InstallSnapshot RPC
                       ▼
┌─────────────────────────────────────────────────────┐
│                  Follower                             │
│                                                      │
│  InstallSnapshot handler:                            │
│  1. Reject if args.Term < currentTerm                │
│  2. Step down if args.Term > currentTerm             │
│  3. Ignore if snapshot already covered               │
│  4. Find overlap in log at lastIncludedIndex/term    │
│     ├─ match → keep trailing entries                 │
│     └─ no match → discard entire log                 │
│  5. Save snapshot, clamp commit/lastApplied          │
│  6. Send ApplyMsg{SnapshotValid: true} to applyCh    │
│  7. Persist state                                    │
└─────────────────────────────────────────────────────┘

Key Design Decisions

  1. Log indexing: rf.log[0] is a placeholder entry whose Index/Term equal the snapshot's LastIncludedIndex/LastIncludedTerm. Real entries start at rf.log[1]. A helper logIdx(idx) converts external index → slice offset.

  2. Snapshot creation: When the log exceeds SnapshotThreshold (1000 entries) past the last snapshot, Snapshot() is called with the serialised state-machine state. It persists the snapshot, trims the log (keeping only entries after the snapshot), and clamps commitIndex / lastApplied to at least the snapshot index.

  3. InstallSnapshot RPC: Sent by the leader to a follower whose nextIndex ≤ snapshot.LastIncludedIndex. The handler on the follower side:

  4. Rejects stale terms (prevents rollback)
  5. Drops duplicate/obsolete snapshots
  6. Preserves any log tail that is consistent with the snapshot boundary (same index & term)
  7. Sends the snapshot data to the state machine via applyCh

  8. Chunking: The implementation shows the pattern for single-shot snapshots. For multi-chunk, you add Offset to InstallSnapshotArgs, loop over the snapshot data, and set Done = true only on the final chunk.

Edge Cases Covered

Edge Case How It's Handled
Snapshot index > last log index Rejected — no-op
Obsolete snapshot (older than existing) Rejected — lastIncludedIndex ≤ rf.snapshot.LastIncludedIndex check
Follower already has the snapshot Ignored via the same check
Stale leader (lower term) Handler returns with reply.Term = currentTerm, leader steps down
New leader with lagging follower leaderSendSnapshotIfNeeded detects nextIndex ≤ snapshotIndex and sends InstallSnapshot
Consistent log tail after snapshot Overlap search: if lastIncludedIndex/term match a log entry, trailing entries are preserved
Inconsistent log tail Entire log discarded (replaced with placeholder)
Concurrent snapshot + log append Mutex (rf.mu) ensures atomicity of all snapshot operations

Evidence & signatures

All 8 tests pass:

```
=== RUN   TestSnapshotCreation
    PASS: Snapshot correctly truncated log and preserved entries after the snapshot.
--- PASS: TestSnapshotCreation (0.00s)

=== RUN   TestSnapshotRejectedWhenIndexTooHigh
    PASS: Snapshot rejected when index exceeds last log index.
--- PASS: TestSnapshotRejectedWhenIndexTooHigh (0.00s)

=== RUN   TestObsoleteSnapshotIgnored
    PASS: Obsolete snapshot did not overwrite a more recent one.
--- PASS: TestObsoleteSnapshotIgnored (0.00s)

=== RUN   TestFollowerInstallSnapshot
    PASS: Follower applied InstallSnapshot, log replaced, applyCh received snapshot.
--- PASS: TestFollowerInstallSnapshot (0.00s)

=== RUN   TestFollowerIgnoresDuplicateSnapshot
    PASS: Duplicate snapshot was correctly ignored.
--- PASS: TestFollowerIgnoresDuplicateSnapshot (0.00s)

=== RUN   TestFollowerRejectsStaleTerm
    PASS: Stale InstallSnapshot from lower-term leader was rejected.
--- PASS: TestFollowerRejectsStaleTerm (0.00s)

=== RUN   TestLeaderSendsSnapshotToLaggingFollower
    PASS: Leader correctly identified lagging follower and triggered InstallSnapshot.
--- PASS: TestLeaderSendsSnapshotToLaggingFollower (0.00s)

=== RUN   TestFollowerPreservesConsistentTail
    PASS: Follower preserved consistent log tail after installing snapshot at index 2.
--- PASS: TestFollowerPreservesConsistentTail (0.00s)

PASS
ok  	raft	0.003s
```

### Edge Cases Tested

- **TestSnapshotCreation**: 2000-entry log, snapshot at index 1000 → log trimmed to 1001 entries, placeholder at index 1000, entries 1001..2000 preserved
- **TestSnapshotRejectedWhenIndexTooHigh**: Snapshot at index 200 with only 100 entries → rejected
- **TestObsoleteSnapshotIgnored**: Snapshot at 300, then older snapshot at 200 → ignored
- **TestFollowerInstallSnapshot**: Full snapshot replaces entire 50-entry log, commitIndex/lastApplied clamped, `applyCh` receives snapshot message
- **TestFollowerIgnoresDuplicateSnapshot**: Follower already has snapshot at index 100 → duplicate ignored
- **TestFollowerRejectsStaleTerm**: Leader term 3, follower term 5 → rejected, reply.Term = 5
- **TestLeaderSendsSnapshotToLaggingFollower**: Leader has snapshot at index 5, follower nextIndex = 1 → triggers InstallSnapshot
- **TestFollowerPreservesConsistentTail**: Snapshot at index 2/term 1 matches log entry, trailing entries (3,4,5) preserved

---
{"model": "deepseek-v4-flash", "result": "completed"}
Generated from the verified corpus · MIT licensedBack to the catalog