use std::{
collections::{BTreeMap, HashMap, HashSet},
io::{Read, Write},
sync::{
atomic::{AtomicBool, Ordering},
Arc, Mutex,
},
};
pub(crate) const SENTINEL: &str = "@hl-checkpoint-stream";
const ABI: u32 = 1;
const MAGIC_REQUEST: u32 = 0x484b_4351;
const MAGIC_REPLY: u32 = 0x484b_4353;
const NAME_MAX: usize = 512;
const PAYLOAD_MAX: usize = 4 * 1024 * 1024;
const REQUEST_BYTES: usize = 48;
const REPLY_BYTES: usize = 32;
const STATUS_OK: i32 = 0;
const STATUS_ERROR: i32 = -1;
const STATUS_ALREADY: i32 = 1;
const OP_OBJECT_BEGIN: u32 = 1;
const OP_OBJECT_WRITE: u32 = 2;
const OP_OBJECT_WRITE_AT: u32 = 3;
const OP_OBJECT_TELL: u32 = 4;
const OP_OBJECT_FINISH: u32 = 5;
const OP_OBJECT_ABORT: u32 = 6;
const OP_GROUP_BEGIN: u32 = 7;
const OP_GROUP_COMMIT: u32 = 8;
const OP_GROUP_ABORT: u32 = 9;
const OP_CLAIM: u32 = 10;
const OP_UNCLAIM: u32 = 11;
const OP_COMMIT: u32 = 12;
const OP_GROUP_PRESENT: u32 = 13;
const OP_GROUP_COUNT: u32 = 14;
const OP_DIGEST: u32 = 15;
const OP_SOURCE_LIST: u32 = 16;
const OP_SOURCE_SIZE: u32 = 17;
const OP_SOURCE_READ: u32 = 18;
const HASH_BASIS: u64 = 14_695_981_039_346_656_037;
const HASH_PRIME: u64 = 1_099_511_628_211;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct StoreError {
pub message: String,
}
impl StoreError {
pub fn new(message: impl Into<String>) -> Self {
Self {
message: message.into(),
}
}
}
impl std::fmt::Display for StoreError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str(&self.message)
}
}
impl std::error::Error for StoreError {}
pub trait CheckpointStore: Send + Sync {
fn put(&self, name: &str, data: &[u8]) -> Result<(), StoreError>;
fn get(&self, name: &str) -> Result<Vec<u8>, StoreError>;
fn list(&self) -> Result<Vec<String>, StoreError>;
fn commit(&self, manifest: &[u8]) -> Result<(), StoreError> {
self.put("MANIFEST", manifest)
}
}
#[derive(Debug, Default)]
pub struct MemoryStore {
objects: Mutex<BTreeMap<String, Vec<u8>>>,
}
impl MemoryStore {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn objects(&self) -> BTreeMap<String, Vec<u8>> {
self.objects.lock().expect("memory store lock").clone()
}
#[must_use]
pub fn committed(&self) -> bool {
self.objects
.lock()
.expect("memory store lock")
.contains_key("MANIFEST")
}
#[must_use]
pub fn bytes(&self) -> usize {
self.objects
.lock()
.expect("memory store lock")
.values()
.map(Vec::len)
.sum()
}
}
impl CheckpointStore for MemoryStore {
fn put(&self, name: &str, data: &[u8]) -> Result<(), StoreError> {
self.objects
.lock()
.map_err(|_| StoreError::new("memory store lock is poisoned"))?
.insert(name.to_owned(), data.to_vec());
Ok(())
}
fn get(&self, name: &str) -> Result<Vec<u8>, StoreError> {
self.objects
.lock()
.map_err(|_| StoreError::new("memory store lock is poisoned"))?
.get(name)
.cloned()
.ok_or_else(|| StoreError::new(format!("no such object: {name}")))
}
fn list(&self) -> Result<Vec<String>, StoreError> {
Ok(self
.objects
.lock()
.map_err(|_| StoreError::new("memory store lock is poisoned"))?
.keys()
.cloned()
.collect())
}
}
fn hash_bytes(mut hash: u64, data: &[u8]) -> u64 {
for byte in data {
hash ^= u64::from(*byte);
hash = hash.wrapping_mul(HASH_PRIME);
}
hash
}
fn object_hash(name: &str, data: &[u8]) -> u64 {
let mut hash = hash_bytes(HASH_BASIS, name.as_bytes());
hash = hash_bytes(hash, &[0]);
hash = hash_bytes(hash, &(data.len() as u64).to_ne_bytes());
hash_bytes(hash, data)
}
fn image_digest(objects: &BTreeMap<String, (u64, u64)>) -> (u64, u64, u64) {
let mut hash = HASH_BASIS;
let mut bytes = 0_u64;
for (name, (object, size)) in objects {
hash = hash_bytes(hash, name.as_bytes());
hash = hash_bytes(hash, &[0]);
hash = hash_bytes(hash, &object.to_ne_bytes());
bytes += *size;
}
(hash, objects.len() as u64, bytes)
}
fn digested(name: &str) -> bool {
name != "MANIFEST" && name != "RECOVERY.jsonl" && !name.starts_with(".RECOVERY.jsonl.tmp.")
}
#[derive(Debug)]
struct Object {
name: String,
bytes: Vec<u8>,
}
#[derive(Default)]
struct State {
open: HashMap<(u64, u64), Object>,
staged: HashMap<String, Vec<Object>>,
committed_groups: HashSet<String>,
claims: HashSet<String>,
digest: BTreeMap<String, (u64, u64)>,
failure: Option<String>,
}
pub(crate) struct SinkServer {
store: Arc<dyn CheckpointStore>,
state: Mutex<State>,
committed: AtomicBool,
running: AtomicBool,
}
impl std::fmt::Debug for SinkServer {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str("SinkServer")
}
}
impl SinkServer {
pub(crate) fn new(store: Arc<dyn CheckpointStore>) -> Self {
Self {
store,
state: Mutex::new(State::default()),
committed: AtomicBool::new(false),
running: AtomicBool::new(true),
}
}
pub(crate) fn committed(&self) -> bool {
self.committed.load(Ordering::SeqCst)
}
pub(crate) fn stop(&self) {
self.running.store(false, Ordering::SeqCst);
}
pub(crate) fn failure(&self) -> Option<String> {
self.state
.lock()
.ok()
.and_then(|state| state.failure.clone())
}
fn record_failure(&self, message: String) {
if let Ok(mut state) = self.state.lock() {
if state.failure.is_none() {
state.failure = Some(message);
}
}
}
fn publish(&self, object: &Object) -> Result<(), StoreError> {
self.store.put(&object.name, &object.bytes)?;
if digested(&object.name) {
if let Ok(mut state) = self.state.lock() {
state.digest.insert(
object.name.clone(),
(
object_hash(&object.name, &object.bytes),
object.bytes.len() as u64,
),
);
}
}
Ok(())
}
fn stored_digest(&self) -> Result<(u64, u64, u64), StoreError> {
let mut objects = BTreeMap::new();
for name in self.store.list()? {
if !digested(&name) {
continue;
}
let bytes = self.store.get(&name)?;
objects.insert(
name.clone(),
(object_hash(&name, &bytes), bytes.len() as u64),
);
}
Ok(image_digest(&objects))
}
fn serve(self: &Arc<Self>, channel: &mut std::os::unix::net::UnixStream, id: u64) {
loop {
let mut header = [0_u8; REQUEST_BYTES];
match channel.read_exact(&mut header) {
Ok(()) => {}
Err(_) => return, }
let Some(request) = Request::decode(&header) else {
self.record_failure("checkpoint channel framing is invalid".into());
return;
};
let mut name = vec![0_u8; request.name_size];
if channel.read_exact(&mut name).is_err() {
return;
}
let name = String::from_utf8_lossy(name.split_last().map_or(&[][..], |(_, rest)| rest))
.into_owned();
let mut payload = Vec::new();
if request.carries_payload() {
payload = vec![0_u8; usize::try_from(request.length).unwrap_or(0)];
if channel.read_exact(&mut payload).is_err() {
return;
}
}
let reply = self.dispatch(id, &request, &name, &payload);
if reply.write(channel).is_err() {
return;
}
}
}
#[allow(clippy::too_many_lines)]
fn dispatch(&self, id: u64, request: &Request, name: &str, payload: &[u8]) -> Reply {
let key = (id, request.stream);
match request.op {
OP_OBJECT_BEGIN => {
if name.len() > NAME_MAX {
return Reply::error();
}
let Ok(mut state) = self.state.lock() else {
return Reply::error();
};
state.open.insert(
key,
Object {
name: name.to_owned(),
bytes: Vec::new(),
},
);
Reply::ok()
}
OP_OBJECT_WRITE | OP_OBJECT_WRITE_AT => {
let Ok(mut state) = self.state.lock() else {
return Reply::error();
};
let Some(object) = state.open.get_mut(&key) else {
return Reply::error();
};
if request.op == OP_OBJECT_WRITE {
object.bytes.extend_from_slice(payload);
} else {
let offset = usize::try_from(request.offset).unwrap_or(usize::MAX);
let end = offset + payload.len();
if object.bytes.len() < end {
object.bytes.resize(end, 0);
}
object.bytes[offset..end].copy_from_slice(payload);
}
Reply::ok()
}
OP_OBJECT_TELL => {
let Ok(state) = self.state.lock() else {
return Reply::error();
};
state.open.get(&key).map_or_else(Reply::error, |object| {
Reply::value(object.bytes.len() as u64)
})
}
OP_OBJECT_FINISH => {
let object = {
let Ok(mut state) = self.state.lock() else {
return Reply::error();
};
match state.open.remove(&key) {
Some(object) => object,
None => return Reply::error(),
}
};
let group = object
.name
.split_once('/')
.map(|(group, _)| group.to_owned());
if let Some(group) = group {
let Ok(mut state) = self.state.lock() else {
return Reply::error();
};
if state.staged.contains_key(&group) {
state.staged.entry(group).or_default().push(object);
return Reply::ok();
}
}
match self.publish(&object) {
Ok(()) => Reply::ok(),
Err(error) => {
self.record_failure(format!("store rejected {}: {error}", object.name));
Reply::error()
}
}
}
OP_OBJECT_ABORT => {
if let Ok(mut state) = self.state.lock() {
state.open.remove(&key);
}
Reply::ok()
}
OP_GROUP_BEGIN => {
let Ok(mut state) = self.state.lock() else {
return Reply::error();
};
state.staged.insert(name.to_owned(), Vec::new());
Reply::ok()
}
OP_GROUP_COMMIT => {
let staged = {
let Ok(mut state) = self.state.lock() else {
return Reply::error();
};
state.staged.remove(name).unwrap_or_default()
};
for object in &staged {
if let Err(error) = self.publish(object) {
self.record_failure(format!("store rejected {}: {error}", object.name));
return Reply::error();
}
}
if let Ok(mut state) = self.state.lock() {
state.committed_groups.insert(name.to_owned());
}
Reply::ok()
}
OP_GROUP_ABORT => {
if let Ok(mut state) = self.state.lock() {
state.staged.remove(name);
}
Reply::ok()
}
OP_CLAIM => {
let Ok(mut state) = self.state.lock() else {
return Reply::error();
};
if state.claims.insert(name.to_owned()) {
Reply::ok()
} else {
Reply::status(STATUS_ALREADY)
}
}
OP_UNCLAIM => {
if let Ok(mut state) = self.state.lock() {
state.claims.remove(name);
}
Reply::ok()
}
OP_GROUP_PRESENT => {
let Ok(state) = self.state.lock() else {
return Reply::error();
};
Reply::value(u64::from(state.committed_groups.contains(name)))
}
OP_GROUP_COUNT => {
let Ok(state) = self.state.lock() else {
return Reply::error();
};
Reply::value(
state
.committed_groups
.iter()
.filter(|group| group.starts_with(name))
.count() as u64,
)
}
OP_DIGEST => {
let digest = {
let Ok(state) = self.state.lock() else {
return Reply::error();
};
if state.digest.is_empty() {
None
} else {
Some(image_digest(&state.digest))
}
};
let digest = match digest {
Some(digest) => digest,
None => match self.stored_digest() {
Ok(digest) => digest,
Err(_) => return Reply::error(),
},
};
let mut bytes = Vec::with_capacity(24);
bytes.extend_from_slice(&digest.0.to_ne_bytes());
bytes.extend_from_slice(&digest.1.to_ne_bytes());
bytes.extend_from_slice(&digest.2.to_ne_bytes());
Reply::payload(bytes)
}
OP_COMMIT => match self.store.commit(payload) {
Ok(()) => {
self.committed.store(true, Ordering::SeqCst);
Reply::ok()
}
Err(error) => {
self.record_failure(format!("store rejected the manifest: {error}"));
Reply::error()
}
},
OP_SOURCE_LIST => {
let Ok(names) = self.store.list() else {
return Reply::error();
};
let mut seen = Vec::new();
for full in names {
let entry = full.split_once('/').map_or(full.as_str(), |(head, _)| head);
if entry.starts_with(name) && !seen.iter().any(|held| held == entry) {
seen.push(entry.to_owned());
}
}
let mut bytes = Vec::new();
for entry in &seen {
bytes.extend_from_slice(entry.as_bytes());
bytes.push(0);
}
let count = seen.len() as u64;
Reply {
status: STATUS_OK,
value: count,
payload: bytes,
}
}
OP_SOURCE_SIZE => match self.store.get(name) {
Ok(bytes) => Reply::value(bytes.len() as u64),
Err(_) => Reply::status(STATUS_ALREADY), },
OP_SOURCE_READ => {
let Ok(bytes) = self.store.get(name) else {
return Reply::error();
};
let offset = usize::try_from(request.offset).unwrap_or(usize::MAX);
if offset >= bytes.len() {
return Reply::payload(Vec::new());
}
let length = usize::try_from(request.length)
.unwrap_or(0)
.min(PAYLOAD_MAX);
let end = offset.saturating_add(length).min(bytes.len());
Reply::payload(bytes[offset..end].to_vec())
}
_ => Reply::error(),
}
}
}
pub(crate) fn serve(
server: &Arc<SinkServer>,
broker: std::os::unix::net::UnixDatagram,
) -> std::thread::JoinHandle<()> {
let server = Arc::clone(server);
std::thread::spawn(move || {
let mut workers = Vec::new();
while server.running.load(Ordering::SeqCst) {
let Some(mut channel) =
crate::ffi::broker_accept(&broker, std::time::Duration::from_millis(50))
else {
continue;
};
let worker = Arc::clone(&server);
let id = workers.len() as u64 + 1;
workers.push(std::thread::spawn(move || {
worker.serve(&mut channel, id);
}));
}
for worker in workers {
let _ = worker.join();
}
})
}
#[derive(Debug)]
struct Request {
op: u32,
stream: u64,
offset: u64,
length: u64,
name_size: usize,
}
impl Request {
fn decode(bytes: &[u8; REQUEST_BYTES]) -> Option<Self> {
let word =
|at: usize| u32::from_ne_bytes(bytes[at..at + 4].try_into().ok().unwrap_or([0; 4]));
let long =
|at: usize| u64::from_ne_bytes(bytes[at..at + 8].try_into().ok().unwrap_or([0; 8]));
if word(0) != MAGIC_REQUEST || word(4) != ABI {
return None;
}
let name_size = word(40) as usize;
let length = long(32);
if name_size > NAME_MAX || length > PAYLOAD_MAX as u64 {
return None;
}
Some(Self {
op: word(8),
stream: long(16),
offset: long(24),
length,
name_size,
})
}
const fn carries_payload(&self) -> bool {
self.length != 0 && self.op != OP_SOURCE_READ
}
}
#[derive(Debug)]
struct Reply {
status: i32,
value: u64,
payload: Vec<u8>,
}
impl Reply {
const fn status(status: i32) -> Self {
Self {
status,
value: 0,
payload: Vec::new(),
}
}
const fn ok() -> Self {
Self::status(STATUS_OK)
}
const fn error() -> Self {
Self::status(STATUS_ERROR)
}
const fn value(value: u64) -> Self {
Self {
status: STATUS_OK,
value,
payload: Vec::new(),
}
}
const fn payload(payload: Vec<u8>) -> Self {
Self {
status: STATUS_OK,
value: 0,
payload,
}
}
fn write(&self, channel: &mut std::os::unix::net::UnixStream) -> std::io::Result<()> {
let mut header = [0_u8; REPLY_BYTES];
header[0..4].copy_from_slice(&MAGIC_REPLY.to_ne_bytes());
header[4..8].copy_from_slice(&ABI.to_ne_bytes());
header[8..12].copy_from_slice(&self.status.to_ne_bytes());
header[16..24].copy_from_slice(&self.value.to_ne_bytes());
header[24..32].copy_from_slice(&(self.payload.len() as u64).to_ne_bytes());
channel.write_all(&header)?;
if !self.payload.is_empty() {
channel.write_all(&self.payload)?;
}
channel.flush()
}
}
#[cfg(test)]
mod tests {
use super::{image_digest, object_hash, CheckpointStore, MemoryStore};
use std::collections::BTreeMap;
#[test]
fn the_image_digest_is_order_independent_in_its_input() {
let mut forward = BTreeMap::new();
forward.insert("a".to_owned(), (object_hash("a", b"one"), 3));
forward.insert("b".to_owned(), (object_hash("b", b"two"), 3));
let mut backward = BTreeMap::new();
backward.insert("b".to_owned(), (object_hash("b", b"two"), 3));
backward.insert("a".to_owned(), (object_hash("a", b"one"), 3));
assert_eq!(image_digest(&forward), image_digest(&backward));
assert_eq!(image_digest(&forward).1, 2);
assert_eq!(image_digest(&forward).2, 6);
}
#[test]
fn a_memory_store_round_trips_objects() {
let store = MemoryStore::new();
store.put("proc.1/pages", b"payload").expect("put");
assert_eq!(store.get("proc.1/pages").expect("get"), b"payload");
assert_eq!(store.list().expect("list"), vec!["proc.1/pages".to_owned()]);
assert!(!store.committed());
store.commit(b"manifest").expect("commit");
assert!(store.committed());
}
}