///|
fn invalid_segment_plan(message : String) -> SegmentPlan {
{ segments: [], total_records: 0, total_bytes: 0, valid: false, message }
}
///|
fn append_segment(
segments : Array[SegmentMeta],
id : UInt,
first_sequence : UInt,
last_sequence : UInt,
record_count : Int,
byte_size : Int,
) -> Unit {
segments.push({ id, first_sequence, last_sequence, record_count, byte_size })
}
///|
/// Plans segment rotation without splitting a record. A single oversized record
/// receives its own segment so that forward progress remains deterministic.
pub fn plan_segments(
records : Array[JournalRecord],
max_bytes : Int,
max_records : Int,
first_id? : UInt = 1U,
) -> SegmentPlan {
if max_bytes < JOURNAL_HEADER_SIZE {
return invalid_segment_plan("max bytes is smaller than one record header")
}
if max_records <= 0 {
return invalid_segment_plan("max records must be positive")
}
let segments : Array[SegmentMeta] = []
let mut id = first_id
let mut first_sequence = 0U
let mut last_sequence = 0U
let mut segment_records = 0
let mut segment_bytes = 0
let mut total_bytes = 0
for record in records {
if record.sequence == 0U ||
(last_sequence > 0U && record.sequence != last_sequence + 1U) {
return invalid_segment_plan("record sequence is not contiguous")
}
let size = record.encoded_size()
if segment_records > 0 &&
(segment_records >= max_records || segment_bytes + size > max_bytes) {
append_segment(
segments, id, first_sequence, last_sequence, segment_records, segment_bytes,
)
id += 1U
first_sequence = 0U
segment_records = 0
segment_bytes = 0
}
if segment_records == 0 {
first_sequence = record.sequence
}
last_sequence = record.sequence
segment_records += 1
segment_bytes += size
total_bytes += size
}
if segment_records > 0 {
append_segment(
segments, id, first_sequence, last_sequence, segment_records, segment_bytes,
)
}
{
segments,
total_records: records.length(),
total_bytes,
valid: true,
message: "segment rotation plan is valid",
}
}
///|
/// Validates IDs, sequence continuity, counts, and non-empty segment metadata.
pub fn validate_segment_chain(segments : Array[SegmentMeta]) -> Bool {
let mut previous_id = 0U
let mut previous_sequence = 0U
for index, segment in segments {
if segment.record_count <= 0 ||
segment.byte_size < JOURNAL_HEADER_SIZE ||
segment.first_sequence == 0U ||
segment.last_sequence < segment.first_sequence {
return false
}
if index > 0 &&
(
segment.id != previous_id + 1U ||
segment.first_sequence != previous_sequence + 1U
) {
return false
}
previous_id = segment.id
previous_sequence = segment.last_sequence
}
true
}
///|
/// Returns the first segment that may contain records newer than a checkpoint.
/// A return value equal to segment count means no segment needs replay.
pub fn recovery_segment_index(
segments : Array[SegmentMeta],
checkpoint_sequence : UInt,
) -> Int {
for index, segment in segments {
if segment.last_sequence > checkpoint_sequence {
return index
}
}
segments.length()
}