use nodedb_array::types::ArrayId;
use tracing::{info, warn};
use crate::data::executor::core_loop::CoreLoop;
use crate::types::Lsn;
impl CoreLoop {
pub(in crate::data::executor) fn checkpoint_array_engines(&mut self) -> crate::Result<Lsn> {
let durable_through = self.watermark;
let ids: Vec<ArrayId> = self.array_engine.array_ids().cloned().collect();
if ids.is_empty() {
return Ok(durable_through);
}
let mut flushed = 0usize;
let mut first_error: Option<crate::Error> = None;
for id in &ids {
match self.array_engine.flush(id, durable_through.as_u64()) {
Ok(Some(_)) => flushed += 1,
Ok(None) => {}
Err(e) => {
let error = crate::Error::Storage {
engine: "array".to_string(),
detail: format!(
"array checkpoint: flush failed for tenant {} array {}: {e}",
id.tenant_id.as_u64(),
id.name
),
};
warn!(
core = self.core_id,
array = %id.name,
error = %error,
"array checkpoint flush failed for one array; continuing with \
the rest and clamping this core's checkpoint LSN"
);
if first_error.is_none() {
first_error = Some(error);
}
}
}
}
if let Some(e) = first_error {
return Err(e);
}
info!(
core = self.core_id,
arrays = ids.len(),
flushed,
durable_through_lsn = durable_through.as_u64(),
"array checkpoint flushed"
);
Ok(durable_through)
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use std::time::{Duration, Instant};
use nodedb_array::query::slice::Slice as ArraySlice;
use nodedb_array::schema::ArraySchema;
use nodedb_array::schema::ArraySchemaBuilder;
use nodedb_array::schema::attr_spec::{AttrSpec, AttrType};
use nodedb_array::schema::dim_spec::{DimSpec, DimType};
use nodedb_array::types::cell_value::value::CellValue;
use nodedb_array::types::coord::value::CoordValue;
use nodedb_array::types::domain::{Domain, DomainBound};
use nodedb_bridge::buffer::{Consumer, Producer, RingBuffer};
use nodedb_physical::physical_plan::ArrayOp;
use super::*;
use crate::bridge::dispatch::{BridgeRequest, BridgeResponse};
use crate::bridge::envelope::{PhysicalPlan, Priority, Request, Response, Status};
use crate::engine::array::wal::ArrayPutCell;
use crate::types::*;
const TID: u64 = 1;
struct Core {
core: CoreLoop,
req_tx: Producer<BridgeRequest>,
resp_rx: Consumer<BridgeResponse>,
next_id: u64,
}
impl Core {
fn open_at(dir: &std::path::Path) -> Self {
let (req_tx, req_rx) = RingBuffer::channel::<BridgeRequest>(64);
let (resp_tx, resp_rx) = RingBuffer::channel::<BridgeResponse>(64);
let core = CoreLoop::open(
0,
req_rx,
resp_tx,
dir,
Arc::new(nodedb_types::OrdinalClock::new()),
)
.expect("CoreLoop::open");
Self {
core,
req_tx,
resp_rx,
next_id: 1,
}
}
fn send(&mut self, op: ArrayOp) -> Response {
let id = self.next_id;
self.next_id += 1;
self.req_tx
.try_push(BridgeRequest {
inner: Request {
request_id: RequestId::new(id),
tenant_id: TenantId::new(TID),
database_id: DatabaseId::DEFAULT,
vshard_id: VShardId::new(0),
plan: PhysicalPlan::Array(op),
deadline: Instant::now() + Duration::from_secs(5),
priority: Priority::Normal,
trace_id: TraceId::ZERO,
consistency: ReadConsistency::Strong,
idempotency_key: None,
event_source: crate::event::EventSource::User,
user_roles: Vec::new(),
user_id: None,
statement_digest: None,
txn_id: None,
wal_lsn: None,
resolved_now_ms: None,
admission: crate::bridge::envelope::Admission::Admitted,
},
})
.expect("push request");
self.core.tick();
self.resp_rx.try_pop().expect("response").inner
}
fn open_array(&mut self, id: &ArrayId) {
let r = self.send(ArrayOp::OpenArray {
array_id: id.clone(),
schema_msgpack: zerompk::to_msgpack_vec(&schema()).expect("encode schema"),
schema_hash: SCHEMA_HASH,
prefix_bits: 8,
});
assert_eq!(r.status, Status::Ok, "open array: {r:?}");
}
fn put(&mut self, id: &ArrayId, x: i64, y: i64, v: i64, wal_lsn: u64) {
let cells = vec![ArrayPutCell {
coord: vec![CoordValue::Int64(x), CoordValue::Int64(y)],
attrs: vec![CellValue::Int64(v)],
surrogate: nodedb_types::Surrogate::ZERO,
system_from_ms: 1,
valid_from_ms: 0,
valid_until_ms: i64::MAX,
}];
let r = self.send(ArrayOp::Put {
array_id: id.clone(),
cells_msgpack: zerompk::to_msgpack_vec(&cells).expect("encode cells"),
wal_lsn,
provenance: None,
});
assert_eq!(r.status, Status::Ok, "array put: {r:?}");
}
fn slice_all(&mut self, id: &ArrayId) -> Vec<(i64, i64, i64)> {
let slice = ArraySlice {
dim_ranges: vec![None, None],
};
let r = self.send(ArrayOp::Slice {
array_id: id.clone(),
slice_msgpack: zerompk::to_msgpack_vec(&slice).expect("encode slice"),
attr_projection: vec![],
limit: 0,
cell_filter: None,
hilbert_range: None,
system_time: nodedb_types::SystemTimeScope::Current,
valid_at_ms: None,
});
assert_eq!(r.status, Status::Ok, "array slice: {r:?}");
decode_cells(r.payload.as_bytes())
}
}
const SCHEMA_HASH: u64 = 0xA55E7;
fn aid() -> ArrayId {
ArrayId::new(TenantId::new(TID), "grid")
}
fn schema() -> ArraySchema {
ArraySchemaBuilder::new("grid")
.dim(DimSpec::new(
"x",
DimType::Int64,
Domain::new(DomainBound::Int64(0), DomainBound::Int64(15)),
))
.dim(DimSpec::new(
"y",
DimType::Int64,
Domain::new(DomainBound::Int64(0), DomainBound::Int64(15)),
))
.attr(AttrSpec::new("v", AttrType::Int64, true))
.tile_extents(vec![4, 4])
.build()
.expect("build schema")
}
fn decode_cells(bytes: &[u8]) -> Vec<(i64, i64, i64)> {
use crate::data::executor::response_codec::ArraySliceResponse;
let envelope: ArraySliceResponse =
zerompk::from_msgpack(bytes).expect("slice response envelope");
let json = nodedb_types::msgpack_to_json_string(&envelope.rows_msgpack)
.expect("slice rows msgpack to json");
let rows: serde_json::Value = serde_json::from_str(&json).expect("slice rows json");
let mut out: Vec<(i64, i64, i64)> = rows
.as_array()
.expect("slice rows array")
.iter()
.map(|row| {
let coords = row["coords"].as_array().expect("coords");
let attrs = row["attrs"].as_array().expect("attrs");
(
coords[0].as_i64().expect("x"),
coords[1].as_i64().expect("y"),
attrs[0].as_i64().expect("v"),
)
})
.collect();
out.sort_unstable();
out
}
#[test]
fn checkpointed_cells_answer_a_slice_after_a_restart() {
let dir = tempfile::tempdir().expect("tempdir");
let id = aid();
let mut before = Core::open_at(dir.path());
before.open_array(&id);
before.put(&id, 1, 2, 30, 10);
before.put(&id, 9, 9, 40, 20);
assert_eq!(
before.slice_all(&id),
vec![(1, 2, 30), (9, 9, 40)],
"both cells must be live in the memtable before any flush"
);
before.core.advance_watermark(Lsn::new(20));
let reported = before
.core
.checkpoint_array_engines()
.expect("flush to a writable dir must succeed");
assert_eq!(
reported,
Lsn::new(20),
"the flush must report exactly the LSN it made durable — the manager \
deletes WAL segments below whatever this returns"
);
drop(before);
let mut after = Core::open_at(dir.path());
after.open_array(&id);
assert_eq!(
after.slice_all(&id),
vec![(1, 2, 30), (9, 9, 40)],
"every checkpointed cell must come back from its on-disk segment — \
pre-fix the checkpoint flushed nothing and both cells were gone"
);
}
#[test]
fn empty_memtable_reports_the_watermark() {
let dir = tempfile::tempdir().expect("tempdir");
let id = aid();
let mut core = Core::open_at(dir.path());
core.open_array(&id);
core.put(&id, 1, 1, 7, 5);
core.core.advance_watermark(Lsn::new(5));
core.core.checkpoint_array_engines().expect("first flush");
core.core.advance_watermark(Lsn::new(900));
assert_eq!(
core.core.checkpoint_array_engines().expect("second flush"),
Lsn::new(900),
"nothing was written since the last flush, so the array engine is \
durable through the current watermark"
);
}
#[test]
fn no_arrays_reports_the_watermark() {
let dir = tempfile::tempdir().expect("tempdir");
let mut core = Core::open_at(dir.path());
core.core.advance_watermark(Lsn::new(42));
assert_eq!(
core.core.checkpoint_array_engines().expect("flush"),
Lsn::new(42)
);
}
#[test]
fn cells_written_after_the_flush_are_still_live() {
let dir = tempfile::tempdir().expect("tempdir");
let id = aid();
let mut core = Core::open_at(dir.path());
core.open_array(&id);
core.put(&id, 1, 2, 30, 10);
core.core.advance_watermark(Lsn::new(10));
core.core.checkpoint_array_engines().expect("flush");
core.put(&id, 3, 3, 50, 11);
assert_eq!(
core.slice_all(&id),
vec![(1, 2, 30), (3, 3, 50)],
"the flushed segment and the live memtable must read as one array"
);
}
}