use std::{path::Path, sync::Arc};
use ts_mooncake::store::{regex_escape, Store as Client, StoreLibrary, StoreSetup};
use crate::{
layout::Range,
store::{HostBuffer, IoResult, Store, StoreError},
};
const OBJECT_NOT_FOUND: i64 = -704;
const OBJECT_ALREADY_EXISTS: i32 = -705;
const NO_AVAILABLE_HANDLE: i32 = -200;
pub struct MooncakeStore {
client: Client,
host: HostBuffer,
}
impl MooncakeStore {
pub fn connect(
library: &Path,
setup: &StoreSetup,
host: HostBuffer,
) -> Result<Self, ts_mooncake::Error> {
let library = StoreLibrary::load(library)?;
let client = Client::connect(Arc::clone(&library), setup)?;
if host.len > 0 {
client.register_buffer(host.base, host.len)?;
}
tracing::info!(
protocol = %setup.protocol,
master = %setup.master_server_address,
segment = setup.global_segment_size,
"Mooncake Store L3 ready"
);
Ok(Self { client, host })
}
fn addresses(host: HostBuffer, ranges: &[Range]) -> (Vec<usize>, Vec<usize>) {
ranges
.iter()
.map(|&range| (host.address(range), range.size))
.unzip()
}
}
fn failed(error: ts_mooncake::Error) -> StoreError {
StoreError(error.to_string())
}
fn get_outcome(results: &[i64], sizes: &[usize]) -> IoResult {
let full = |result: i64, size: usize| u64::try_from(result).is_ok_and(|n| n == size as u64);
IoResult {
success: results
.iter()
.zip(sizes)
.map(|(&result, &size)| full(result, size))
.collect(),
retired: results
.iter()
.all(|&result| result >= 0 || result == OBJECT_NOT_FOUND),
object_failures: results
.iter()
.zip(sizes)
.map(|(&result, &size)| {
result == OBJECT_NOT_FOUND || (result >= 0 && !full(result, size))
})
.collect(),
}
}
fn put_outcome(exists: Vec<bool>, missing: &[usize], results: &[i32]) -> IoResult {
let mut success = exists;
for (&index, &result) in missing.iter().zip(results) {
success[index] = result == 0 || result == OBJECT_ALREADY_EXISTS;
}
IoResult {
object_failures: vec![false; success.len()],
success,
retired: results
.iter()
.all(|&result| [0, OBJECT_ALREADY_EXISTS, NO_AVAILABLE_HANDLE].contains(&result)),
}
}
fn namespace_pattern(prefix: &str) -> String {
format!("^{}.*", regex_escape(prefix))
}
impl Store for MooncakeStore {
fn failures_retire(&self) -> bool {
false
}
fn batch_exists(&self, keys: &[String]) -> Result<Vec<bool>, StoreError> {
if keys.is_empty() {
return Ok(Vec::new());
}
let results = self.client.batch_is_exist(keys).map_err(failed)?;
if results.iter().any(|&flag| flag != 0 && flag != 1) {
return Err(StoreError(format!(
"Mooncake existence query failed: {results:?}"
)));
}
Ok(results.into_iter().map(|flag| flag == 1).collect())
}
fn batch_get_into(
&self,
keys: &[String],
host: HostBuffer,
ranges: &[Range],
) -> Result<IoResult, StoreError> {
if keys.len() != ranges.len() {
return Err(StoreError("ragged L3 get".to_owned()));
}
if keys.is_empty() {
return Ok(IoResult {
success: Vec::new(),
retired: true,
object_failures: Vec::new(),
});
}
let (addrs, sizes) = Self::addresses(host, ranges);
let results = self
.client
.batch_get_into(keys, &addrs, &sizes)
.map_err(failed)?;
Ok(get_outcome(&results, &sizes))
}
fn batch_put_from(
&self,
keys: &[String],
host: HostBuffer,
ranges: &[Range],
) -> Result<IoResult, StoreError> {
if keys.len() != ranges.len() {
return Err(StoreError("ragged L3 put".to_owned()));
}
let exists = match self.batch_exists(keys) {
Ok(exists) => exists,
Err(error) => {
tracing::error!(%error, "Mooncake existence query before PUT failed");
return Ok(IoResult {
success: vec![false; keys.len()],
retired: true,
object_failures: vec![false; keys.len()],
});
}
};
let missing: Vec<usize> = (0..keys.len()).filter(|&i| !exists[i]).collect();
if missing.is_empty() {
return Ok(put_outcome(exists, &missing, &[]));
}
let put_keys: Vec<String> = missing.iter().map(|&i| keys[i].clone()).collect();
let put_ranges: Vec<Range> = missing.iter().map(|&i| ranges[i]).collect();
let (addrs, sizes) = Self::addresses(host, &put_ranges);
let results = self
.client
.batch_put_from(&put_keys, &addrs, &sizes)
.map_err(failed)?;
Ok(put_outcome(exists, &missing, &results))
}
fn remove_by_prefix(&self, prefix: &str) -> bool {
match self
.client
.remove_by_regex(&namespace_pattern(prefix), true)
{
Ok(removed) if removed >= 0 => true,
Ok(status) => {
tracing::error!(status, prefix, "clearing the Mooncake L3 namespace failed");
false
}
Err(error) => {
tracing::error!(%error, prefix, "clearing the Mooncake L3 namespace failed");
false
}
}
}
fn close(&self) {
if self.host.len > 0 {
if let Err(error) = self.client.unregister_buffer(self.host.base) {
tracing::warn!(%error, "deregistering the Host pool from the store failed");
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn existing_objects_are_not_rewritten_and_rejections_retire() {
let outcome = put_outcome(vec![true, false, false], &[1, 2], &[0, NO_AVAILABLE_HANDLE]);
assert_eq!(outcome.success, [true, true, false]);
assert!(outcome.retired);
assert_eq!(outcome.object_failures, [false, false, false]);
let outcome = put_outcome(vec![false], &[0], &[OBJECT_ALREADY_EXISTS]);
assert_eq!(outcome.success, [true]);
assert!(outcome.retired);
}
#[test]
fn a_failed_put_does_not_retire() {
for error in [-1, -800] {
let outcome = put_outcome(vec![true, false, false], &[1, 2], &[0, error]);
assert_eq!(outcome.success, [true, true, false]);
assert!(!outcome.retired);
}
}
#[test]
fn a_get_miss_retires_as_an_object_failure() {
let outcome = get_outcome(&[OBJECT_NOT_FOUND, 8], &[8, 8]);
assert_eq!(outcome.success, [false, true]);
assert!(outcome.retired);
assert_eq!(outcome.object_failures, [true, false]);
let outcome = get_outcome(&[OBJECT_NOT_FOUND, -800], &[8, 8]);
assert!(!outcome.retired);
assert_eq!(outcome.object_failures, [true, false]);
}
#[test]
fn a_short_get_is_an_invalid_object() {
let outcome = get_outcome(&[4, 8], &[8, 8]);
assert_eq!(outcome.success, [false, true]);
assert!(outcome.retired);
assert_eq!(outcome.object_failures, [true, false]);
let outcome = get_outcome(&[0, -1], &[8, 8]);
assert_eq!(outcome.success, [false, false]);
assert!(!outcome.retired);
assert_eq!(outcome.object_failures, [true, false]);
}
#[test]
fn a_namespace_clear_matches_only_its_prefix() {
assert_eq!(namespace_pattern("model.v1_"), r"^model\.v1_.*");
assert_eq!(namespace_pattern("tsl3-ab12"), r"^tsl3\-ab12.*");
}
}