use std::cmp::Ordering;
use std::collections::BinaryHeap;
use std::hash::{Hash, Hasher};
use std::sync::Mutex;
use std::thread;
use xxhash_rust::xxh3::Xxh3;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CommitEntry {
pub commit_seq: u64,
pub page_numbers: Vec<u32>,
pub timestamp_ns: u64,
}
#[derive(Debug, Default)]
pub struct MicaShard {
entries: Mutex<Vec<CommitEntry>>,
shard_seq: std::sync::atomic::AtomicU64,
}
impl MicaShard {
fn new() -> Self {
Self::default()
}
#[must_use]
pub fn shard_seq(&self) -> u64 {
self.shard_seq.load(std::sync::atomic::Ordering::Relaxed)
}
#[must_use]
pub fn len(&self) -> usize {
self.entries.lock().expect("MicaShard mutex poisoned").len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.len() == 0
}
}
#[derive(Debug)]
pub struct MicaCommitLog {
shards: Vec<MicaShard>,
num_shards: usize,
}
impl MicaCommitLog {
#[must_use]
pub fn new(num_shards: usize) -> Self {
let num_shards = num_shards.max(1);
let mut shards = Vec::with_capacity(num_shards);
for _ in 0..num_shards {
shards.push(MicaShard::new());
}
Self { shards, num_shards }
}
#[must_use]
pub fn shard_count(&self) -> usize {
self.num_shards
}
pub fn append(&self, commit_seq: u64, page_numbers: Vec<u32>) {
let idx = self.shard_for_current_thread();
let entry = CommitEntry {
commit_seq,
page_numbers,
timestamp_ns: now_ns(),
};
let shard = &self.shards[idx];
{
let mut guard = shard.entries.lock().expect("MicaShard mutex poisoned");
guard.push(entry);
}
shard
.shard_seq
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
}
#[must_use]
pub fn merge_ordered(&self) -> Vec<CommitEntry> {
let mut shard_runs: Vec<Vec<CommitEntry>> = Vec::with_capacity(self.num_shards);
let mut total = 0usize;
for shard in &self.shards {
let guard = shard.entries.lock().expect("MicaShard mutex poisoned");
let mut run = guard.clone();
drop(guard);
run.sort_by_key(|entry| entry.commit_seq);
total += run.len();
shard_runs.push(run);
}
if total == 0 {
return Vec::new();
}
let mut heap: BinaryHeap<HeapHead> = BinaryHeap::with_capacity(shard_runs.len());
for (shard_idx, run) in shard_runs.iter().enumerate() {
if let Some(entry) = run.first() {
heap.push(HeapHead {
commit_seq: entry.commit_seq,
shard_idx,
pos: 0,
});
}
}
let mut out = Vec::with_capacity(total);
while let Some(head) = heap.pop() {
let HeapHead { shard_idx, pos, .. } = head;
let run = &shard_runs[shard_idx];
out.push(run[pos].clone());
let next_pos = pos + 1;
if next_pos < run.len() {
heap.push(HeapHead {
commit_seq: run[next_pos].commit_seq,
shard_idx,
pos: next_pos,
});
}
}
out
}
#[must_use]
pub fn total_len(&self) -> usize {
self.shards.iter().map(MicaShard::len).sum()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.shards.iter().all(MicaShard::is_empty)
}
fn shard_for_current_thread(&self) -> usize {
let tid = thread::current().id();
let mut hasher = Xxh3::new();
tid.hash(&mut hasher);
let h = hasher.finish();
let mixed = splitmix64(h);
(mixed as usize) % self.num_shards
}
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
struct HeapHead {
commit_seq: u64,
shard_idx: usize,
pos: usize,
}
impl Ord for HeapHead {
fn cmp(&self, other: &Self) -> Ordering {
other
.commit_seq
.cmp(&self.commit_seq)
.then_with(|| other.shard_idx.cmp(&self.shard_idx))
}
}
impl PartialOrd for HeapHead {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
#[inline]
fn splitmix64(mut x: u64) -> u64 {
x = x.wrapping_add(0x9E37_79B9_7F4A_7C15);
let mut z = x;
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
z ^ (z >> 31)
}
fn now_ns() -> u64 {
use std::time::{SystemTime, UNIX_EPOCH};
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_or(0, |dur| {
let secs = dur.as_secs();
let nanos = u64::from(dur.subsec_nanos());
secs.saturating_mul(1_000_000_000).saturating_add(nanos)
})
}
#[cfg(test)]
mod tests {
use super::{CommitEntry, MicaCommitLog};
use std::sync::Arc;
use std::thread;
#[test]
fn single_thread_100_entries_merge_ordered() {
let log = MicaCommitLog::new(4);
for seq in 0..100 {
log.append(seq, vec![seq as u32, (seq + 1) as u32]);
}
let merged = log.merge_ordered();
assert_eq!(merged.len(), 100);
for (i, entry) in merged.iter().enumerate() {
assert_eq!(entry.commit_seq, i as u64);
assert_eq!(entry.page_numbers, vec![i as u32, (i as u32) + 1]);
}
for pair in merged.windows(2) {
assert!(pair[0].commit_seq < pair[1].commit_seq);
}
}
#[test]
fn multithreaded_4x100_entries_merge_ordered() {
let log = Arc::new(MicaCommitLog::new(4));
let threads: Vec<_> = (0..4)
.map(|t| {
let log = Arc::clone(&log);
thread::spawn(move || {
let base = (t as u64) * 100;
for i in 0..100 {
log.append(base + i, vec![(base + i) as u32]);
}
})
})
.collect();
for h in threads {
h.join().expect("worker thread panicked");
}
let merged: Vec<CommitEntry> = log.merge_ordered();
assert_eq!(merged.len(), 400);
for (i, entry) in merged.iter().enumerate() {
assert_eq!(entry.commit_seq, i as u64, "out-of-order at index {i}");
}
for pair in merged.windows(2) {
assert!(
pair[0].commit_seq < pair[1].commit_seq,
"merge produced non-monotone output"
);
}
}
#[test]
fn empty_log_merge_ordered_is_empty() {
let log = MicaCommitLog::new(8);
assert_eq!(log.shard_count(), 8);
assert!(log.is_empty());
let merged = log.merge_ordered();
assert!(merged.is_empty());
}
#[test]
fn zero_shards_is_clamped_to_one() {
let log = MicaCommitLog::new(0);
assert_eq!(log.shard_count(), 1);
log.append(42, vec![7]);
let merged = log.merge_ordered();
assert_eq!(merged.len(), 1);
assert_eq!(merged[0].commit_seq, 42);
assert_eq!(merged[0].page_numbers, vec![7]);
}
#[test]
fn ties_on_commit_seq_are_broken_deterministically() {
let log = MicaCommitLog::new(2);
log.append(5, vec![1]);
log.append(5, vec![2]);
log.append(5, vec![3]);
let merged = log.merge_ordered();
assert_eq!(merged.len(), 3);
for entry in &merged {
assert_eq!(entry.commit_seq, 5);
}
let merged2 = log.merge_ordered();
assert_eq!(
merged
.iter()
.map(|e| e.page_numbers.clone())
.collect::<Vec<_>>(),
merged2
.iter()
.map(|e| e.page_numbers.clone())
.collect::<Vec<_>>(),
);
}
}