// 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 in-memory tail of the log that has not yet been written to `Storage`
/// (etcd's `unstable`). It holds newly appended entries and, optionally, a
/// snapshot waiting to be applied, until they are handed to a `Ready` and their
/// writes are confirmed. `entries[i]` sits at absolute log index `i + offset`.
///
/// `offset_in_progress` (exclusive, `>= offset`) marks how far the entries have
/// begun being written; `snapshot_in_progress` says the snapshot write has
/// begun. Following etcd, the "in progress" cursors are what let the same
/// entries be exposed once for persistence and then withheld until stabilized.
/// The snapshot is an `Option` rather than a sentinel — its absence is a real
/// type state, not an index-0 magic value.
pub(all) struct Unstable {
mut snapshot : Snapshot?
mut entries : Array[Entry]
mut offset : UInt64
mut offset_in_progress : UInt64
mut snapshot_in_progress : Bool
}
///|
/// A fresh unstable tail anchored just past the last stable entry.
pub fn Unstable::new(offset : UInt64) -> Unstable {
{
snapshot: None,
entries: [],
offset,
offset_in_progress: offset,
snapshot_in_progress: false,
}
}
///|
/// The first index the unstable region can speak to — one past the snapshot —
/// or `None` when there is no snapshot.
pub fn Unstable::maybe_first_index(self : Unstable) -> UInt64? {
self.snapshot.map(s => s.last_index + 1)
}
///|
/// The last index covered, if there is at least one entry or a snapshot.
pub fn Unstable::maybe_last_index(self : Unstable) -> UInt64? {
let l = self.entries.length()
if l != 0 {
Some(self.offset + l.to_uint64() - 1)
} else {
self.snapshot.map(s => s.last_index)
}
}
///|
/// The term of the entry at `i`, if the unstable region knows it — either from
/// an entry it holds or from the snapshot baseline.
pub fn Unstable::maybe_term(self : Unstable, i : UInt64) -> UInt64? {
if i < self.offset {
match self.snapshot {
Some(s) => if s.last_index == i { Some(s.last_term) } else { None }
None => None
}
} else {
match self.maybe_last_index() {
None => None
Some(last) =>
if i > last {
None
} else {
Some(self.entries[(i - self.offset).to_int()].term)
}
}
}
}
///|
/// The entries not already in the process of being written to storage.
pub fn Unstable::next_entries(self : Unstable) -> Array[Entry] {
let in_progress = (self.offset_in_progress - self.offset).to_int()
if self.entries.length() == in_progress {
[]
} else {
self.entries[in_progress:].to_owned()
}
}
///|
/// The snapshot to write, if one is present and not already being written.
pub fn Unstable::next_snapshot(self : Unstable) -> Snapshot? {
if self.snapshot_in_progress {
None
} else {
self.snapshot
}
}
///|
/// Mark every held entry and the snapshot as having begun their write, so they
/// are withheld from later `next_entries`/`next_snapshot` until stabilized.
pub fn Unstable::accept_in_progress(self : Unstable) -> Unit {
let n = self.entries.length()
if n > 0 {
self.offset_in_progress = self.entries[n - 1].index + 1
}
if self.snapshot is Some(_) {
self.snapshot_in_progress = true
}
}
///|
/// Discard the entries up to and including `id` now that they are durably
/// stored. Ignored if the entry is missing, matched only the snapshot baseline,
/// or the term no longer matches (the unstable tail was replaced meanwhile).
pub fn Unstable::stable_to(self : Unstable, id : EntryId) -> Unit {
match self.maybe_term(id.index) {
None => return
Some(gt) => {
if id.index < self.offset {
return
}
if gt != id.term {
return
}
let num = (id.index + 1 - self.offset).to_int()
self.entries = self.entries[num:].to_owned()
self.offset = id.index + 1
self.offset_in_progress = u64_max(self.offset_in_progress, self.offset)
}
}
}
///|
/// Drop the snapshot once it has been written to storage.
pub fn Unstable::stable_snap_to(self : Unstable, i : UInt64) -> Unit {
if self.snapshot is Some(s) && s.last_index == i {
self.snapshot = None
self.snapshot_in_progress = false
}
}
///|
/// Replace the unstable tail with a snapshot baseline: the log restarts just
/// past `s`, with no in-memory entries.
pub fn Unstable::restore(self : Unstable, s : Snapshot) -> Unit {
self.offset = s.last_index + 1
self.offset_in_progress = self.offset
self.entries = []
self.snapshot = Some(s)
self.snapshot_in_progress = false
}
///|
/// Splice `ents` onto the tail: append directly when they follow the last held
/// entry, replace the whole tail when they start at or before `offset`, or
/// truncate the divergent suffix and append otherwise. Only in-progress entries
/// before the truncation point stay in progress.
pub fn Unstable::truncate_and_append(
self : Unstable,
ents : Array[Entry],
) -> Unit {
let from_index = ents[0].index
if from_index == self.offset + self.entries.length().to_uint64() {
for e in ents {
self.entries.push(e)
}
} else if from_index <= self.offset {
self.entries = ents
self.offset = from_index
self.offset_in_progress = self.offset
} else {
let merged = self.slice(self.offset, from_index)
for e in ents {
merged.push(e)
}
self.entries = merged
self.offset_in_progress = u64_min(self.offset_in_progress, from_index)
}
}
///|
/// The held entries with indices in `[lo, hi)`. The whole range must lie within
/// the unstable region, otherwise this aborts (etcd panics).
pub fn Unstable::slice(
self : Unstable,
lo : UInt64,
hi : UInt64,
) -> Array[Entry] {
self.must_check_out_of_bounds(lo, hi)
self.entries[(lo - self.offset).to_int():(hi - self.offset).to_int()].to_owned()
}
///|
/// Guard: `offset <= lo <= hi <= offset + len(entries)`.
fn Unstable::must_check_out_of_bounds(
self : Unstable,
lo : UInt64,
hi : UInt64,
) -> Unit {
if lo > hi {
abort("invalid unstable.slice: lo > hi")
}
let upper = self.offset + self.entries.length().to_uint64()
if lo < self.offset || hi > upper {
abort("unstable.slice out of bound")
}
}