1use std::collections::HashMap;
4use std::fmt;
5use std::sync::atomic::{AtomicU32, Ordering};
6use std::sync::{Mutex, PoisonError};
7
8use super::{Capacity, Row, SqlConn, SqlError, SqlValue, TxFn, blob, count, schema};
9use crate::store::{
10 Batch, BatchOutcome, Cursor, Key, NamespaceStore, Partition, PartitionStats, Precondition,
11 ScanPage, StoreCapabilities, StoreError, StoreMaintenance, Value, Write, codec, keys,
12};
13
14pub const GET_MANY_CHUNK: usize = 64;
17
18const STATS_TTL_MS: u64 = 60_000;
20const STATS_CACHE_MAX: usize = 4096;
22
23pub(super) const GET: &str = "SELECT value FROM kv WHERE part = ?1 AND key = ?2";
24pub(super) const PUT: &str = "INSERT INTO kv (part, key, value) VALUES (?1, ?2, ?3) \
25 ON CONFLICT (part, key) DO UPDATE SET value = excluded.value";
26pub(super) const DELETE: &str = "DELETE FROM kv WHERE part = ?1 AND key = ?2";
27pub(super) const SCAN_FROM: &str = "SELECT key, value FROM kv \
28 WHERE part = ?1 AND key >= ?2 AND key < ?3 ORDER BY key LIMIT ?4";
29pub(super) const SCAN_AFTER: &str = "SELECT key, value FROM kv \
30 WHERE part = ?1 AND key > ?2 AND key < ?3 ORDER BY key LIMIT ?4";
31pub(super) const STATS: &str =
32 "SELECT COUNT(*), SUM(length(key) + length(value)) FROM kv WHERE part = ?1";
33pub const TIMER_WINDOW_START: &str = "SELECT part, key FROM kv INDEXED BY kv_timers \
35 WHERE key >= x'7700' AND key < x'7701' ORDER BY key, part LIMIT ?1";
36pub const TIMER_WINDOW_AFTER: &str = "SELECT part, key FROM kv INDEXED BY kv_timers \
40 WHERE (key, part) > (?1, ?2) AND key >= x'7700' AND key < x'7701' \
41 ORDER BY key, part LIMIT ?3";
42
43#[derive(Debug, Clone, PartialEq, Eq)]
48pub struct TimerCursor {
49 pub key: Key,
51 pub partition: Partition,
53}
54
55pub(super) const PROBE: &str = "SELECT 1";
56
57pub(super) fn get_many_sql(n: usize) -> String {
59 let marks: Vec<String> = (2..n + 2).map(|i| format!("?{i}")).collect();
60 format!(
61 "SELECT key, value FROM kv WHERE part = ?1 AND key IN ({})",
62 marks.join(", ")
63 )
64}
65
66pub struct SqlKvStore<C> {
83 conn: C,
84 schema_version: AtomicU32,
85 stats: Mutex<HashMap<Vec<u8>, (u64, PartitionStats)>>,
86 capacity: Option<Capacity>,
87}
88
89impl<C> fmt::Debug for SqlKvStore<C> {
90 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
91 f.debug_struct("SqlKvStore")
92 .field(
93 "schema_version",
94 &self.schema_version.load(Ordering::Relaxed),
95 )
96 .field("capacity", &self.capacity)
97 .finish_non_exhaustive()
98 }
99}
100
101impl<C: SqlConn> SqlKvStore<C> {
102 pub fn open(conn: C) -> Result<Self, StoreError> {
109 let version = schema::migrate(&conn)?;
110 Ok(Self::from_checked_conn(conn, version))
111 }
112
113 pub fn open_existing(conn: C) -> Result<Self, StoreError> {
119 let version = schema::require_current(&conn)?;
120 Ok(Self::from_checked_conn(conn, version))
121 }
122
123 fn from_checked_conn(conn: C, version: u32) -> Self {
124 Self {
125 conn,
126 schema_version: AtomicU32::new(version),
127 stats: Mutex::default(),
128 capacity: None,
129 }
130 }
131
132 pub fn open_with_capacity(conn: C, capacity: Capacity) -> Result<Self, StoreError> {
138 let mut store = Self::open(conn)?;
139 store.conn.set_size_limit(capacity.cap_bytes())?;
140 store.capacity = Some(capacity);
141 Ok(store)
142 }
143
144 pub fn apply_extended<F>(
150 &self,
151 p: &Partition,
152 batch: Batch,
153 extend: F,
154 ) -> Result<BatchOutcome, StoreError>
155 where
156 F: FnOnce(
157 &dyn Fn(&Key) -> Result<Option<Value>, SqlError>,
158 &mut Batch,
159 u64,
160 ) -> Result<(), SqlError>
161 + 'static,
162 {
163 batch.validate(&self.capabilities())?;
164 let part = part(p)?;
165 let adds = batch.has_put();
166 let soft_limit = self.capacity.map(|c| c.soft_limit());
167 match self.conn.transaction(Box::new(move |conn| {
168 check_and_write_extended(&conn, &part, batch, soft_limit, extend)
169 })) {
170 Ok(outcome) => Ok(outcome),
171 Err(SqlError::Full) if !adds => Err(StoreError::unavailable(
172 "database full during a delete-only batch",
173 )),
174 Err(error) => Err(error.into()),
175 }
176 }
177
178 pub fn timer_window(
187 &self,
188 after: Option<&TimerCursor>,
189 limit: u32,
190 ) -> Result<Vec<TimerCursor>, StoreError> {
191 if limit == 0 {
192 return Err(StoreError::Invalid(
193 "timer window limit must be positive".into(),
194 ));
195 }
196 let (sql, params) = match after {
197 None => (
198 TIMER_WINDOW_START,
199 vec![SqlValue::Integer(i64::from(limit))],
200 ),
201 Some(cursor) => (
202 TIMER_WINDOW_AFTER,
203 vec![
204 SqlValue::Blob(cursor.key.as_bytes().to_vec()),
205 SqlValue::Blob(cursor.partition.encode()?.to_vec()),
206 SqlValue::Integer(i64::from(limit)),
207 ],
208 ),
209 };
210 self.conn
211 .query(sql, ¶ms)?
212 .into_iter()
213 .map(|mut row| {
214 Ok(TimerCursor {
215 partition: Partition::decode(&blob(&mut row, 0)?)?,
216 key: Key::new(blob(&mut row, 1)?),
217 })
218 })
219 .collect()
220 }
221
222 #[must_use]
224 pub fn capacity(&self) -> Option<Capacity> {
225 self.capacity
226 }
227
228 #[must_use]
230 pub fn conn(&self) -> &C {
231 &self.conn
232 }
233
234 pub fn clear_stats_cache(&self) {
237 self.stats
238 .lock()
239 .unwrap_or_else(PoisonError::into_inner)
240 .clear();
241 }
242}
243
244fn part(p: &Partition) -> Result<SqlValue, StoreError> {
245 Ok(SqlValue::Blob(p.encode()?.to_vec()))
246}
247
248fn key_param(key: &Key) -> SqlValue {
249 SqlValue::Blob(key.as_bytes().to_vec())
250}
251
252fn read<C: SqlConn>(conn: &C, part: &SqlValue, key: &Key) -> Result<Option<Value>, SqlError> {
253 let mut rows = conn.query(GET, &[part.clone(), key_param(key)])?;
254 rows.first_mut()
255 .map(|row| blob(row, 0).map(Value::new))
256 .transpose()
257}
258
259fn check_and_write<C: SqlConn>(
263 conn: &C,
264 part: &SqlValue,
265 batch: Batch,
266 soft_limit: Option<u64>,
267) -> Result<BatchOutcome, SqlError> {
268 check_and_write_extended(conn, part, batch, soft_limit, |_, _, _| Ok(()))
269}
270
271fn check_and_write_extended<C: SqlConn, F>(
272 conn: &C,
273 part: &SqlValue,
274 mut batch: Batch,
275 soft_limit: Option<u64>,
276 extend: F,
277) -> Result<BatchOutcome, SqlError>
278where
279 F: FnOnce(
280 &dyn Fn(&Key) -> Result<Option<Value>, SqlError>,
281 &mut Batch,
282 u64,
283 ) -> Result<(), SqlError>,
284{
285 let now = conn.now_ms();
287 for (index, pre) in batch.preconditions.iter().enumerate() {
288 let (holds, observed) = match pre {
289 Precondition::NotAfter(deadline) if now > *deadline => {
290 return Ok(BatchOutcome::DeadlinePassed { backend_now: now });
291 }
292 Precondition::NotAfter(_) => continue,
293 Precondition::Absent(key) => {
294 let current = read(conn, part, key)?;
295 (current.is_none(), current)
296 }
297 Precondition::Present(key) => (read(conn, part, key)?.is_some(), None),
298 Precondition::Equals(key, value) => {
299 let current = read(conn, part, key)?;
300 (current.as_ref() == Some(value), current)
301 }
302 };
303 if !holds {
304 return Ok(BatchOutcome::PreconditionFailed { index, observed });
305 }
306 }
307 extend(&|key| read(conn, part, key), &mut batch, now)?;
308 batch
309 .validate(&StoreCapabilities::full())
310 .map_err(|_| SqlError::Corrupt("extended batch exceeds limits"))?;
311 if let Some(limit) = soft_limit
312 && batch.has_put()
313 && !is_relay_scan_checkpoint(&batch)
314 && !is_relay_timer_reschedule(&batch)
315 && !is_timer_retry_move(&batch)
316 && conn.size_bytes()? >= limit
317 {
318 return Err(SqlError::Full);
319 }
320 for write in batch.writes {
321 match write {
322 Write::Put(key, value) => {
323 let value = SqlValue::Blob(value.into_bytes().into());
324 conn.exec(PUT, &[part.clone(), key_param(&key), value])?
325 }
326 Write::Delete(key) => conn.exec(DELETE, &[part.clone(), key_param(&key)])?,
327 };
328 }
329 Ok(BatchOutcome::Committed)
330}
331
332fn is_relay_scan_checkpoint(batch: &Batch) -> bool {
338 let scan_key = keys::relay_scan();
339 let guarded = batch.preconditions.iter().any(|pre| {
340 matches!(pre, Precondition::Absent(key) | Precondition::Equals(key, _) if key == &scan_key)
341 });
342 let mut scan_puts = 0;
343 let only_cleanup = batch.writes.iter().all(|write| match write {
344 Write::Put(key, value) if key == &scan_key => {
345 scan_puts += 1;
346 codec::decode_relay_scan(value).is_ok()
347 }
348 Write::Delete(key) => matches!(keys::parse(key), Some(keys::ParsedKey::Relay(_))),
349 Write::Put(..) => false,
350 });
351 guarded && only_cleanup && scan_puts == 1
352}
353
354pub(super) fn is_relay_timer_reschedule(batch: &Batch) -> bool {
359 let (
360 [
361 Precondition::Equals(old_key, old_value),
362 Precondition::Absent(absent),
363 ],
364 [Write::Delete(deleted), Write::Put(new_key, new_value)],
365 ) = (batch.preconditions.as_slice(), batch.writes.as_slice())
366 else {
367 return false;
368 };
369 if old_key != deleted
370 || absent != new_key
371 || !old_value.as_bytes().is_empty()
372 || !new_value.as_bytes().is_empty()
373 {
374 return false;
375 }
376 let (
377 Some(keys::ParsedKey::Timer {
378 due_at_ms: old_due,
379 kind: old_kind,
380 reference: old_ref,
381 }),
382 Some(keys::ParsedKey::Timer {
383 due_at_ms: new_due,
384 kind: new_kind,
385 reference: new_ref,
386 }),
387 ) = (keys::parse(old_key), keys::parse(new_key))
388 else {
389 return false;
390 };
391 old_kind == crate::timers::registry::kinds::RELAY.get()
392 && new_kind == old_kind
393 && old_ref == new_ref
394 && new_due > old_due
395}
396
397pub(super) fn is_timer_retry_move(batch: &Batch) -> bool {
400 let (
401 [
402 Precondition::Equals(old_key, old_value),
403 Precondition::Absent(absent),
404 ],
405 [Write::Delete(deleted), Write::Put(new_key, new_value)],
406 ) = (batch.preconditions.as_slice(), batch.writes.as_slice())
407 else {
408 return false;
409 };
410 if old_key != deleted || absent != new_key || old_value != new_value {
411 return false;
412 }
413 let (
414 Some(keys::ParsedKey::Timer {
415 due_at_ms: old_due,
416 kind: old_kind,
417 reference: old_ref,
418 }),
419 Some(keys::ParsedKey::Timer {
420 due_at_ms: new_due,
421 kind: new_kind,
422 reference: new_ref,
423 }),
424 Some((old_original, old_attempt)),
425 Some((new_original, new_attempt)),
426 ) = (
427 keys::parse(old_key),
428 keys::parse(new_key),
429 keys::timer_retry_state(old_key),
430 keys::timer_retry_state(new_key),
431 )
432 else {
433 return false;
434 };
435 new_due > old_due
436 && new_kind == old_kind
437 && new_ref == old_ref
438 && new_original == old_original
439 && new_attempt
440 == old_attempt
441 .saturating_add(1)
442 .min(keys::MAX_TIMER_RETRY_ATTEMPT)
443}
444
445fn entry(mut row: Row) -> Result<(Key, Value), SqlError> {
446 Ok((Key::new(blob(&mut row, 0)?), Value::new(blob(&mut row, 1)?)))
447}
448
449impl<C: SqlConn> NamespaceStore for SqlKvStore<C> {
450 fn capabilities(&self) -> StoreCapabilities {
451 StoreCapabilities::full()
452 }
453
454 async fn get(&self, p: &Partition, key: &Key) -> Result<Option<Value>, StoreError> {
455 Ok(read(&self.conn, &part(p)?, key)?)
456 }
457
458 async fn get_many(
459 &self,
460 p: &Partition,
461 keys: &[Key],
462 ) -> Result<Vec<Option<Value>>, StoreError> {
463 let part = part(p)?;
464 let mut found: HashMap<Vec<u8>, Value> = HashMap::new();
465 for chunk in keys.chunks(GET_MANY_CHUNK) {
466 let params: Vec<SqlValue> = core::iter::once(part.clone())
467 .chain(chunk.iter().map(key_param))
468 .collect();
469 for row in self.conn.query(&get_many_sql(chunk.len()), ¶ms)? {
470 let (key, value) = entry(row)?;
471 found.insert(key.into_bytes().into(), value);
472 }
473 }
474 Ok(keys
475 .iter()
476 .map(|key| found.get(key.as_bytes()).cloned())
477 .collect())
478 }
479
480 async fn scan(
481 &self,
482 p: &Partition,
483 start: &Key,
484 end: &Key,
485 after: Option<&Cursor>,
486 limit: u32,
487 ) -> Result<ScanPage, StoreError> {
488 if limit == 0 {
489 return Err(StoreError::Invalid("scan limit must be at least 1".into()));
490 }
491 let (sql, lower) = match after {
492 None => (SCAN_FROM, start.as_bytes()),
493 Some(c) if start.as_bytes() <= c.as_bytes() && c.as_bytes() < end.as_bytes() => {
495 (SCAN_AFTER, c.as_bytes())
496 }
497 Some(_) => {
498 return Err(StoreError::Invalid(
499 "scan cursor outside the scanned range".into(),
500 ));
501 }
502 };
503 if lower >= end.as_bytes() {
504 return Ok(ScanPage::default());
505 }
506 let params = [
507 part(p)?,
508 SqlValue::Blob(lower.to_vec()),
509 key_param(end),
510 SqlValue::Integer(i64::from(limit) + 1),
511 ];
512 let mut entries = self
513 .conn
514 .query(sql, ¶ms)?
515 .into_iter()
516 .map(entry)
517 .collect::<Result<Vec<_>, _>>()?;
518 let want = usize::try_from(limit).unwrap_or(usize::MAX);
519 let next = (entries.len() > want).then(|| {
520 entries.truncate(want);
521 Cursor::new(entries[want - 1].0.clone().into_bytes())
522 });
523 Ok(ScanPage { entries, next })
524 }
525
526 async fn apply(&self, p: &Partition, batch: Batch) -> Result<BatchOutcome, StoreError> {
527 batch.validate(&self.capabilities())?;
528 let part = part(p)?;
529 let adds = batch.has_put();
530 let soft_limit = self.capacity.map(|c| c.soft_limit());
531 let body: TxFn<C, BatchOutcome> =
533 Box::new(move |conn: C| check_and_write(&conn, &part, batch, soft_limit));
534 match self.conn.transaction(body) {
535 Ok(outcome) => Ok(outcome),
536 Err(SqlError::Full) if !adds => Err(StoreError::unavailable(
540 "database full during a delete-only batch",
541 )),
542 Err(e) => Err(e.into()),
543 }
544 }
545
546 async fn stats(&self, p: &Partition) -> Result<PartitionStats, StoreError> {
547 let name = p.encode()?.to_vec();
548 let now = self.conn.now_ms();
549 let fresh = |at: u64| at <= now && now - at < STATS_TTL_MS;
550 {
551 let cache = self.stats.lock().unwrap_or_else(PoisonError::into_inner);
552 if let Some((at, stats)) = cache.get(&name)
553 && fresh(*at)
554 {
555 return Ok(*stats);
556 }
557 }
558 let rows = self.conn.query(STATS, &[SqlValue::Blob(name.clone())])?;
559 let row = rows
560 .first()
561 .ok_or(SqlError::Corrupt("stats returned no row"))?;
562 let stats = PartitionStats {
563 bytes: count(row, 1)?,
564 keys: Some(count(row, 0)?),
565 };
566 let mut cache = self.stats.lock().unwrap_or_else(PoisonError::into_inner);
567 if cache.len() >= STATS_CACHE_MAX {
568 cache.retain(|_, (at, _)| fresh(*at));
569 if cache.len() >= STATS_CACHE_MAX {
570 cache.clear();
571 }
572 }
573 cache.insert(name, (now, stats));
574 Ok(stats)
575 }
576
577 async fn probe(&self) -> Result<(), StoreError> {
578 self.conn.query(PROBE, &[])?;
579 Ok(())
580 }
581}
582
583impl<C: SqlConn> StoreMaintenance for SqlKvStore<C> {
584 fn layout_version(&self) -> u32 {
585 self.schema_version.load(Ordering::Relaxed)
586 }
587
588 async fn migrate(&self) -> Result<u32, StoreError> {
589 let version = schema::migrate(&self.conn)?;
590 self.schema_version.store(version, Ordering::Relaxed);
591 Ok(version)
592 }
593
594 async fn backup_to(&self, dest: &str) -> Result<(), StoreError> {
597 self.conn.backup_to(dest)
598 }
599}