1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
// Copyright 2015 The etcd Authors
// Copyright 2026 Leo Cheng
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

///|
/// The failure modes a `RaftStorage` can report, one variant per distinguishable
/// condition so a caller can tell "this index is gone forever" apart from "this
/// index has not arrived yet" — a distinction etcd draws with distinct sentinel
/// errors and one the consensus core depends on to decide between sending a
/// snapshot and simply waiting.
pub(all) suberror StorageError {
  /// The requested index predates the last snapshot: it has been compacted away.
  Compacted
  /// The requested entry is past the end of the log: not available (yet).
  Unavailable
  /// A snapshot older than the one already stored was offered.
  SnapOutOfDate
  /// The backend needs more time to materialize the snapshot; retry later.
  SnapshotTemporarilyUnavailable
} derive(Eq)

///|
/// Read access to a node's durable log, modelled on etcd's Storage interface.
/// The consensus core reads entries, terms and the snapshot back through this
/// trait, which lets the same core run over memory, a file or a database.
///
/// The index/term accessors raise `StorageError` so a compacted index is
/// reported distinctly from an unavailable one, exactly as etcd's `Storage`
/// contract requires.
pub(open) trait RaftStorage {
  /// The persisted HardState (term, vote, commit) to resume from.
  fn initial_state(Self) -> HardState
  /// Consecutive entries in `[lo, hi)`, capped so their total encoding size does
  /// not exceed `max_size` (but at least one entry is always returned). Raises
  /// `Compacted` if `lo` is compacted, `Unavailable` if the range is empty.
  fn storage_entries(Self, UInt64, UInt64, UInt64) -> Array[Entry] raise StorageError
  /// The term of the entry at `index`, which must lie in
  /// `[first_index-1, last_index]`. Raises `Compacted`/`Unavailable` otherwise.
  fn storage_term(Self, UInt64) -> UInt64 raise StorageError
  /// The index of the first entry still available (one past the snapshot).
  fn first_index(Self) -> UInt64
  /// The index of the last entry in the log.
  fn last_index(Self) -> UInt64
  /// The most recent snapshot. Raises `SnapshotTemporarilyUnavailable` when the
  /// backend is still preparing it.
  fn storage_snapshot(Self) -> Snapshot raise StorageError
  /// Persist `entries`, extending the stable log. The port's `RaftLog` writes the
  /// confirmed unstable prefix back through this (etcd persists through the
  /// application; the port folds it into the storage abstraction), so the log can
  /// be driven over any backend, not only `MemoryStorage`.
  fn append(Self, Array[Entry]) -> Unit
}

///|
/// An in-memory `RaftStorage`. `ents[0]` is a sentinel whose index and term are
/// the snapshot baseline, so `ents[i]` always holds the entry at absolute index
/// `ents[0].index + i`. This mirrors etcd's MemoryStorage layout, which keeps
/// index arithmetic branch-free.
pub struct MemoryStorage {
  mut hard : HardState
  mut snap : Snapshot
  mut ents : Array[Entry]
  // When set, `storage_snapshot` reports the snapshot as not-yet-ready. This
  // models etcd's contract where a backend may need time to prepare a snapshot;
  // the consensus core then knows to wait rather than treat it as an error.
  mut snapshot_pending : Bool
}

///|
/// Create empty storage: an initial HardState, the empty snapshot, and a lone
/// sentinel entry at index 0.
pub fn MemoryStorage::new() -> MemoryStorage {
  {
    hard: HardState::initial(),
    snap: Snapshot::empty(),
    ents: [Entry::normal(0, 0, b"")],
    snapshot_pending: false,
  }
}

///|
/// Build storage directly over a given entry array, treating `ents[0]` as the
/// compaction sentinel. Mirrors etcd's `&MemoryStorage{ents: ents}` test setup.
pub fn MemoryStorage::from_ents(ents : Array[Entry]) -> MemoryStorage {
  {
    hard: HardState::initial(),
    snap: Snapshot::empty(),
    ents,
    snapshot_pending: false,
  }
}

///|
/// The raw entry array, sentinel included. Test-facing, to assert the exact
/// post-compaction/append layout the way etcd's storage tests do.
pub fn MemoryStorage::raw_ents(self : MemoryStorage) -> Array[Entry] {
  self.ents
}

///|
/// The absolute index of the sentinel: one below the first real entry.
fn MemoryStorage::offset(self : MemoryStorage) -> UInt64 {
  self.ents[0].index
}

///|
fn MemoryStorage::last_index_of(self : MemoryStorage) -> UInt64 {
  self.offset() + self.ents.length().to_uint64() - 1
}

///|
pub impl RaftStorage for MemoryStorage with fn first_index(self) {
  self.offset() + 1
}

///|
pub impl RaftStorage for MemoryStorage with fn last_index(self) {
  self.last_index_of()
}

///|
pub impl RaftStorage for MemoryStorage with fn storage_term(self, index) {
  let off = self.offset()
  if index < off {
    raise Compacted
  }
  if (index - off).to_int() >= self.ents.length() {
    raise Unavailable
  }
  self.ents[(index - off).to_int()].term
}

///|
pub impl RaftStorage for MemoryStorage with fn storage_entries(
  self,
  lo,
  hi,
  max_size,
) {
  let off = self.offset()
  if lo <= off {
    raise Compacted
  }
  if hi > self.last_index_of() + 1 {
    abort("entries hi is out of bound of last index")
  }
  // Only the sentinel remains: no real entries to hand back.
  if self.ents.length() == 1 {
    raise Unavailable
  }
  limit_size(self.ents[(lo - off).to_int():(hi - off).to_int()], max_size)
}

///|
pub impl RaftStorage for MemoryStorage with fn storage_snapshot(self) {
  if self.snapshot_pending {
    raise SnapshotTemporarilyUnavailable
  }
  self.snap
}

///|
pub impl RaftStorage for MemoryStorage with fn initial_state(self) {
  self.hard
}

///|
pub impl RaftStorage for MemoryStorage with fn append(self, entries) {
  MemoryStorage::append(self, entries)
}

///|
/// Report the snapshot as not-yet-ready (true) or ready (false). Lets a backend
/// signal that `storage_snapshot()` should be retried rather than treated as an
/// error.
pub fn MemoryStorage::set_snapshot_pending(
  self : MemoryStorage,
  pending : Bool,
) -> Unit {
  self.snapshot_pending = pending
}

///|
/// A best-effort, clamping read of entries with indices in `[lo, hi)`: indices
/// at or before the sentinel and past the last entry are simply skipped rather
/// than raising. Used where an approximate window is wanted without the strict
/// etcd error contract.
pub fn MemoryStorage::slice(
  self : MemoryStorage,
  lo : UInt64,
  hi : UInt64,
) -> Array[Entry] {
  let out : Array[Entry] = []
  let off = self.offset()
  let start = if lo <= off { off + 1 } else { lo }
  let last = self.last_index_of()
  let stop = if hi > last + 1 { last + 1 } else { hi }
  let mut i = start
  while i < stop {
    out.push(self.ents[(i - off).to_int()])
    i = i + 1
  }
  out
}

///|
/// Record the HardState (term, vote, commit) for the next restart.
pub fn MemoryStorage::set_hard_state(
  self : MemoryStorage,
  hs : HardState,
) -> Unit {
  self.hard = hs
}

///|
/// Append entries to the log, overwriting any conflicting suffix. Entries whose
/// indices fall at or before the sentinel are already compacted and are
/// dropped; a gap between the log and the incoming entries is a programming
/// error and aborts, matching etcd's panic.
pub fn MemoryStorage::append(
  self : MemoryStorage,
  entries : Array[Entry],
) -> Unit {
  if entries.is_empty() {
    return
  }
  let first = self.first_index()
  let ent0 = entries[0].index
  let last = ent0 + entries.length().to_uint64() - 1
  // Nothing here is newer than what we already hold.
  if last < first {
    return
  }
  // Drop the prefix the sentinel already covers.
  let start = if first > ent0 { (first - ent0).to_int() } else { 0 }
  let off = (entries[start].index - self.offset()).to_int()
  if off > self.ents.length() {
    abort("missing log entry: append leaves a gap")
  }
  while self.ents.length() > off {
    self.ents.pop() |> ignore
  }
  let mut i = start
  while i < entries.length() {
    self.ents.push(entries[i])
    i = i + 1
  }
}

///|
/// Replace the whole log with a snapshot baseline, resetting the sentinel to the
/// snapshot's index and term (Raft §7). A snapshot no newer than the one already
/// stored is rejected with `SnapOutOfDate`.
pub fn MemoryStorage::apply_snapshot(
  self : MemoryStorage,
  snapshot : Snapshot,
) -> Unit raise StorageError {
  let ms_index = self.snap.last_index
  let snap_index = snapshot.last_index
  // During bootstrap only the ConfState may be set, leaving index and term 0;
  // that case (ms_index == 0) is allowed through.
  if ms_index != 0 && ms_index >= snap_index {
    raise SnapOutOfDate
  }
  self.seed_snapshot(snapshot)
}

///|
/// Install a snapshot baseline unconditionally, resetting the sentinel to its
/// index and term. This is the write half of `apply_snapshot` without the
/// out-of-date guard, for seeding a freshly created storage whose empty baseline
/// cannot predate anything.
pub fn MemoryStorage::seed_snapshot(
  self : MemoryStorage,
  snapshot : Snapshot,
) -> Unit {
  self.snap = snapshot
  self.ents = [Entry::normal(snapshot.last_term, snapshot.last_index, b"")]
}

///|
/// Discard every entry at or before `compact_index`, moving the sentinel up to
/// that index. An index at or before the current sentinel is `Compacted`; one
/// past the last entry aborts (etcd panics), since it is a caller error.
pub fn MemoryStorage::compact(
  self : MemoryStorage,
  compact_index : UInt64,
) -> Unit raise StorageError {
  let off = self.offset()
  if compact_index <= off {
    raise Compacted
  }
  if compact_index > self.last_index_of() {
    abort("compact index is out of bound of last index")
  }
  let cut = (compact_index - off).to_int()
  let base = self.ents[cut]
  let kept : Array[Entry] = [Entry::normal(base.term, base.index, b"")]
  let mut i = cut + 1
  while i < self.ents.length() {
    kept.push(self.ents[i])
    i = i + 1
  }
  self.ents = kept
}

///|
/// Build a snapshot at `index` carrying `data`, remember it, and return it. An
/// index at or before the current snapshot is `SnapOutOfDate`; one past the last
/// entry aborts (etcd panics).
pub fn MemoryStorage::create_snapshot(
  self : MemoryStorage,
  index : UInt64,
  data : Bytes,
  conf_state? : ConfState? = None,
) -> Snapshot raise StorageError {
  if index <= self.snap.last_index {
    raise SnapOutOfDate
  }
  if index > self.last_index_of() {
    abort("snapshot index is out of bound of last index")
  }
  let off = self.offset()
  let term = self.ents[(index - off).to_int()].term
  // etcd's CreateSnapshot(i, cs, data): a supplied ConfState is recorded;
  // otherwise the snapshot keeps the membership already on record, rather than
  // silently dropping it (which would lose the cluster config on restore).
  let snap : Snapshot = {
    last_index: index,
    last_term: term,
    data,
    conf_state: conf_state.unwrap_or(self.snap.conf_state),
  }
  self.snap = snap
  snap
}