use std::collections::HashMap;
use std::future::Future;
use std::sync::{Arc, Mutex};
use nodedb_array::error::ArrayError;
use nodedb_array::sync::apply::ApplyEngine;
use nodedb_array::sync::hlc::Hlc;
use nodedb_array::sync::op::ArrayOp;
use nodedb_array::sync::op_log::OpLog;
use nodedb_array::types::coord::value::CoordValue;
use nodedb_types::Namespace;
use crate::engine::array::engine::{ArrayEngineState, CellWrite};
use crate::storage::engine::StorageEngine;
use crate::sync::array::op_log_store::KvOpLogStore;
use crate::sync::array::schema_registry::SchemaRegistry;
const LAST_APPLIED_PREFIX: &str = "array.last_applied:";
fn block<F, T>(f: F) -> T
where
F: Future<Output = T>,
{
tokio::task::block_in_place(|| tokio::runtime::Handle::current().block_on(f))
}
pub struct LiteApplyEngine<S: StorageEngine> {
pub(super) storage: Arc<S>,
pub(super) array_state: Arc<tokio::sync::Mutex<ArrayEngineState>>,
pub(super) schemas: Arc<SchemaRegistry<S>>,
pub(super) op_log: Arc<KvOpLogStore<S>>,
last_applied: Mutex<HashMap<String, Hlc>>,
}
impl<S: StorageEngine> LiteApplyEngine<S> {
pub async fn new(
storage: Arc<S>,
array_state: Arc<tokio::sync::Mutex<ArrayEngineState>>,
schemas: Arc<SchemaRegistry<S>>,
op_log: Arc<KvOpLogStore<S>>,
) -> Self {
let last_applied = Self::load_last_applied(&storage).await;
Self {
storage,
array_state,
schemas,
op_log,
last_applied: Mutex::new(last_applied),
}
}
async fn load_last_applied(storage: &Arc<S>) -> HashMap<String, Hlc> {
let prefix = LAST_APPLIED_PREFIX.as_bytes();
let Ok(pairs) = storage
.scan_range(Namespace::Meta, prefix, usize::MAX)
.await
else {
return HashMap::new();
};
let mut map = HashMap::new();
for (key, value) in pairs {
if !key.starts_with(prefix) {
break;
}
let name_bytes = &key[prefix.len()..];
let Ok(name) = std::str::from_utf8(name_bytes) else {
continue;
};
if value.len() == 18 {
let bytes: [u8; 18] = match value.try_into() {
Ok(b) => b,
Err(_) => continue,
};
map.insert(name.to_owned(), Hlc::from_bytes(&bytes));
}
}
map
}
pub fn last_applied_hlc(&self, array: &str) -> Option<Hlc> {
self.last_applied.lock().ok()?.get(array).copied()
}
fn record_applied_hlc(&self, array: &str, hlc: Hlc) {
let should_persist = {
let mut map = match self.last_applied.lock() {
Ok(m) => m,
Err(_) => return,
};
let current = map.get(array).copied().unwrap_or(Hlc::ZERO);
if hlc > current {
map.insert(array.to_owned(), hlc);
true
} else {
false
}
};
if should_persist {
let key = format!("{LAST_APPLIED_PREFIX}{array}").into_bytes();
let storage = Arc::clone(&self.storage);
let bytes = hlc.to_bytes();
let _ = block(async move { storage.put(Namespace::Meta, &key, &bytes).await });
}
}
}
impl<S: StorageEngine> ApplyEngine for &LiteApplyEngine<S> {
fn schema_hlc(&self, array: &str) -> nodedb_array::error::ArrayResult<Option<Hlc>> {
Ok(self.schemas.schema_hlc(array))
}
fn already_seen(&self, array: &str, hlc: Hlc) -> nodedb_array::error::ArrayResult<bool> {
let mut iter = self.op_log.scan_range(array, hlc, hlc)?;
Ok(iter.next().is_some())
}
fn apply_put(&mut self, op: &ArrayOp) -> nodedb_array::error::ArrayResult<()> {
let system_from_ms = op.header.system_from_ms;
let attrs = op.attrs.clone().unwrap_or_default();
let array_state = Arc::clone(&self.array_state);
let storage = Arc::clone(&self.storage);
let array = op.header.array.clone();
let coord = op.coord.clone();
let valid_from_ms = op.header.valid_from_ms;
let valid_until_ms = op.header.valid_until_ms;
block(async move {
let mut state = array_state.lock().await;
state
.put_cell(
&storage,
&array,
CellWrite {
coord,
attrs,
system_from_ms,
valid_from_ms,
valid_until_ms,
},
)
.await
.map_err(|e| ArrayError::SegmentCorruption {
detail: format!("apply_put: {e}"),
})
})?;
self.op_log.append(op)?;
self.record_applied_hlc(&op.header.array, op.header.hlc);
Ok(())
}
fn apply_delete(&mut self, op: &ArrayOp) -> nodedb_array::error::ArrayResult<()> {
{
let mut state = self.array_state.blocking_lock();
state
.delete_cell(&op.header.array, op.coord.clone(), op.header.system_from_ms)
.map_err(|e| ArrayError::SegmentCorruption {
detail: format!("apply_delete: {e}"),
})?;
}
self.op_log.append(op)?;
self.record_applied_hlc(&op.header.array, op.header.hlc);
Ok(())
}
fn apply_erase(&mut self, op: &ArrayOp) -> nodedb_array::error::ArrayResult<()> {
let array_state = Arc::clone(&self.array_state);
let storage = Arc::clone(&self.storage);
let array = op.header.array.clone();
let coord = op.coord.clone();
let system_from_ms = op.header.system_from_ms;
block(async move {
let mut state = array_state.lock().await;
state
.gdpr_erase_cell(&storage, &array, coord, system_from_ms)
.await
.map_err(|e| ArrayError::SegmentCorruption {
detail: format!("apply_erase: {e}"),
})
})?;
self.op_log.append(op)?;
self.record_applied_hlc(&op.header.array, op.header.hlc);
Ok(())
}
fn invalidate_tile(
&mut self,
_array: &str,
_coord: &[CoordValue],
) -> nodedb_array::error::ArrayResult<()> {
Ok(())
}
}