use std::sync::Arc;
use crate::error::{DbError, DbResult};
use crate::format_migration::plan::{MigrationPlan, TransformBuffers, TransformOutcome};
use crate::txlog::record::{OpKind, VersionTag};
use crate::unique::UniqueOwner;
pub(crate) struct TxDerivedWrite {
pub participant: Arc<dyn TxParticipant>,
pub key: Vec<u8>,
pub value: Option<Vec<u8>>,
}
pub(crate) struct TxStreamWrites {
pub writes: Vec<TxDerivedWrite>,
pub after_success: Vec<Box<dyn FnOnce() + Send>>,
pub queue_depth_admissions: Vec<crate::queue::QueueDepthAdmission>,
}
impl TxStreamWrites {
pub(crate) fn empty() -> Self {
Self {
writes: Vec::new(),
after_success: Vec::new(),
queue_depth_admissions: Vec::new(),
}
}
}
pub(crate) struct TxCounterDelta {
pub target: Arc<dyn crate::counter::ErasedCounterTarget>,
pub participant: Arc<dyn TxParticipant>,
pub key: Vec<u8>,
pub delta: i128,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ClaimKind {
Claim,
Release,
Reserve { deadline_ms: u64 },
}
pub(crate) struct ClaimOp {
pub participant: Arc<dyn TxParticipant>,
pub namespace: &'static str,
pub key: Vec<u8>,
pub owner: UniqueOwner,
pub kind: ClaimKind,
}
impl ClaimOp {
pub(crate) fn is_claim(&self) -> bool {
matches!(self.kind, ClaimKind::Claim)
}
pub(crate) fn is_release(&self) -> bool {
matches!(self.kind, ClaimKind::Release)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum TxBackend {
Bitcask,
Fixed,
}
pub(crate) struct PlannedOp<'a> {
pub key: &'a [u8],
pub delete: bool,
}
pub(crate) trait TxCollectionName {
fn tx_collection_name() -> &'static str {
""
}
}
impl<T: crate::CollectionMeta> TxCollectionName for T {
fn tx_collection_name() -> &'static str {
T::NAME
}
}
pub(crate) trait TxParticipant: Send + Sync {
fn name(&self) -> &'static str;
fn backend(&self) -> TxBackend;
fn schema_fingerprint(&self) -> u64;
fn encrypted(&self) -> bool;
fn max_entry_bytes(&self) -> Option<usize>;
fn shard_of(&self, key: &[u8]) -> DbResult<usize>;
fn shard_count(&self) -> usize {
1
}
fn lock_shards<'a>(&'a self, shards: &[usize]) -> DbResult<Box<dyn TxLocked + 'a>>;
fn tx_project(
&self,
key: &[u8],
old: Option<&[u8]>,
new: Option<&[u8]>,
) -> DbResult<Vec<TxDerivedWrite>> {
let _ = (key, old, new);
Ok(Vec::new())
}
fn tx_stream_writes(
&self,
key: &[u8],
old: Option<&[u8]>,
new: Option<&[u8]>,
) -> DbResult<TxStreamWrites> {
let _ = (key, old, new);
Ok(TxStreamWrites::empty())
}
fn tx_counter_deltas(
&self,
key: &[u8],
old: Option<&[u8]>,
new: Option<&[u8]>,
) -> DbResult<Vec<TxCounterDelta>> {
let _ = (key, old, new);
Ok(Vec::new())
}
fn tx_claims(
&self,
key: &[u8],
old: Option<&[u8]>,
new: Option<&[u8]>,
) -> DbResult<Vec<ClaimOp>> {
let _ = (key, old, new);
Ok(Vec::new())
}
fn tx_preplan_projection_targets(&self) -> DbResult<Vec<Arc<dyn TxParticipant>>> {
Ok(Vec::new())
}
fn tx_preplan_stream_targets(&self) -> DbResult<Vec<Arc<dyn TxParticipant>>> {
Ok(Vec::new())
}
fn tx_emits(&self) -> DbResult<bool> {
Ok(!self.tx_preplan_stream_targets()?.is_empty())
}
fn tx_prepare_streams(&self) -> DbResult<Vec<crate::queue::retention::ProducerPermit>> {
Ok(Vec::new())
}
}
pub(crate) trait TxLocked {
fn read(&mut self, key: &[u8]) -> DbResult<Option<Vec<u8>>>;
fn contains(&mut self, key: &[u8]) -> DbResult<bool>;
fn reserve(&mut self, ops: &[PlannedOp<'_>]) -> DbResult<Vec<VersionTag>>;
fn apply_put(&mut self, key: &[u8], value: &[u8], tag: VersionTag) -> DbResult<usize>;
fn apply_delete(&mut self, key: &[u8], tag: VersionTag) -> DbResult<usize>;
fn needs_replay(&mut self, key: &[u8], kind: OpKind, tag: VersionTag) -> DbResult<bool>;
fn mark_logged(&mut self);
fn abort(self: Box<Self>);
#[cfg(test)]
fn unapplied_reservations(&self) -> usize;
#[cfg(test)]
fn tx_debug_arm(&mut self, key: &[u8], tag: VersionTag) -> DbResult<()>;
fn finish(
self: Box<Self>,
fenced: &mut Vec<(usize, u64)>,
wakes: &mut crate::watch::WatchWakeBatch,
) -> DbResult<Box<dyn DeferredReplay>>;
}
pub(crate) trait DeferredReplay {
fn event_count(&self) -> usize;
fn replay_one(&mut self, local: usize);
}
impl MigrationPlan {
pub(crate) fn accepts_logged_fingerprint(&self, fingerprint: u64) -> bool {
self.format_steps
.first()
.and_then(|step| step.from_typ)
.is_some_and(|typ| typ.h() == fingerprint)
|| self
.accepted_fingerprints
.iter()
.any(|(accepted, _)| *accepted == fingerprint)
}
pub(crate) fn transform_logged<'a>(
&self,
fingerprint: u64,
key: &[u8],
value: &'a [u8],
buffers: &'a mut TransformBuffers,
) -> DbResult<TransformOutcome<'a>> {
let target = self
.format_steps
.last()
.and_then(|step| step.to_typ)
.map(armour_core::Typ::h);
let from_first = self
.format_steps
.first()
.and_then(|step| step.from_typ)
.map(armour_core::Typ::h);
let from_version = if Some(fingerprint) == target {
self.format_to_version
} else if Some(fingerprint) == from_first {
self.stored_version
} else if let Some((_, suffix_from_version)) = self
.accepted_fingerprints
.iter()
.find(|(accepted, _)| *accepted == fingerprint)
{
*suffix_from_version as u16
} else {
return Err(DbError::TxSchemaMismatch {
collection: self.collection,
tx_id: 0,
stored: target.unwrap_or(fingerprint),
logged: fingerprint,
});
};
self.transform_from_version(from_version, key, value, buffers)
}
}
#[cfg(test)]
mod tests {
use super::*;
struct StubParticipant;
struct StubLocked {
events: Vec<&'static str>,
}
struct StubDeferred {
events: Vec<&'static str>,
fired: std::rc::Rc<std::cell::RefCell<Vec<&'static str>>>,
}
impl TxParticipant for StubParticipant {
fn name(&self) -> &'static str {
"stub"
}
fn backend(&self) -> TxBackend {
TxBackend::Bitcask
}
fn schema_fingerprint(&self) -> u64 {
1
}
fn encrypted(&self) -> bool {
false
}
fn max_entry_bytes(&self) -> Option<usize> {
None
}
fn shard_of(&self, _key: &[u8]) -> DbResult<usize> {
Ok(0)
}
fn lock_shards<'a>(&'a self, _shards: &[usize]) -> DbResult<Box<dyn TxLocked + 'a>> {
Ok(Box::new(StubLocked { events: Vec::new() }))
}
}
impl TxLocked for StubLocked {
fn read(&mut self, _key: &[u8]) -> DbResult<Option<Vec<u8>>> {
Ok(None)
}
fn contains(&mut self, _key: &[u8]) -> DbResult<bool> {
Ok(false)
}
fn reserve(&mut self, ops: &[PlannedOp<'_>]) -> DbResult<Vec<VersionTag>> {
Ok((0..ops.len())
.map(|i| VersionTag::Bitcask { gsn: i as u64 })
.collect())
}
fn apply_put(&mut self, _key: &[u8], _value: &[u8], _tag: VersionTag) -> DbResult<usize> {
self.events.push("put");
Ok(1)
}
fn apply_delete(&mut self, _key: &[u8], _tag: VersionTag) -> DbResult<usize> {
self.events.push("delete");
Ok(1)
}
fn needs_replay(&mut self, _key: &[u8], _kind: OpKind, _tag: VersionTag) -> DbResult<bool> {
Ok(false)
}
fn mark_logged(&mut self) {}
fn abort(self: Box<Self>) {}
#[cfg(test)]
fn unapplied_reservations(&self) -> usize {
0
}
#[cfg(test)]
fn tx_debug_arm(&mut self, _key: &[u8], _tag: VersionTag) -> DbResult<()> {
Ok(())
}
fn finish(
self: Box<Self>,
_fenced: &mut Vec<(usize, u64)>,
_wakes: &mut crate::watch::WatchWakeBatch,
) -> DbResult<Box<dyn DeferredReplay>> {
Ok(Box::new(StubDeferred {
events: self.events,
fired: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
}))
}
}
impl DeferredReplay for StubDeferred {
fn event_count(&self) -> usize {
self.events.len()
}
fn replay_one(&mut self, local: usize) {
self.fired.borrow_mut().push(self.events[local]);
}
}
#[test]
fn tx_seam_is_object_safe_and_survives_finish() {
let p = StubParticipant;
let mut locked: Box<dyn TxLocked> = p.lock_shards(&[0]).unwrap();
let tags = locked
.reserve(&[
PlannedOp {
key: b"a",
delete: false,
},
PlannedOp {
key: b"b",
delete: true,
},
])
.unwrap();
locked.apply_put(b"a", b"v", tags[0]).unwrap();
locked.apply_delete(b"b", tags[1]).unwrap();
let mut deferred = locked
.finish(
&mut Vec::new(),
&mut crate::watch::WatchWakeBatch::default(),
)
.unwrap();
assert_eq!(deferred.event_count(), 2);
deferred.replay_one(0);
deferred.replay_one(1);
}
}