use std::collections::HashMap;
use std::sync::Arc;
use zerocopy::IntoBytes;
use crate::counter::{CounterApply, ErasedCounterTarget};
use crate::error::{DbError, DbResult};
use crate::txn::participant::{ClaimKind, TxLocked, TxParticipant};
use crate::txn::{Action, Op};
use crate::unique::UniqueRow;
use crate::unique::resolve::{ClaimDecision, decide};
use crate::upsert::Upsert;
pub(crate) struct Entry {
pub id: u32,
pub rank: u8,
pub participant: Arc<dyn TxParticipant>,
pub shards: Vec<usize>,
pub lock_all: bool,
}
pub(crate) struct Write {
pub entry: usize,
pub key: Vec<u8>,
pub value: Option<Vec<u8>>,
pub derived: bool,
}
type ProjectionInput = (usize, Vec<u8>, Option<Vec<u8>>, Option<Vec<u8>>);
type EvalRank0Outcome = (Vec<Write>, Vec<ProjectionInput>);
type ClaimRowRef<'a> = (usize, &'a [u8], Option<&'a [u8]>, Option<&'a [u8]>);
type StreamRow = (usize, Vec<u8>, Option<Vec<u8>>, Option<Vec<u8>>);
type StreamLowered = Vec<Box<dyn FnOnce() + Send>>;
fn idx_for(entries: &[Entry], participant: &Arc<dyn TxParticipant>) -> Option<usize> {
entries
.iter()
.position(|e| Arc::ptr_eq(&e.participant, participant))
}
fn ensure_entry(
entries: &mut Vec<Entry>,
db: &crate::txn::TxContext<'_>,
participant: Arc<dyn TxParticipant>,
rank: u8,
key: &[u8],
) -> DbResult<usize> {
let sid = participant.shard_of(key)?;
if let Some(pos) = idx_for(entries, &participant) {
if !entries[pos].shards.contains(&sid) {
entries[pos].shards.push(sid);
}
return Ok(pos);
}
let id = db.collection_id(participant.as_ref())?;
entries.push(Entry {
id,
rank,
participant,
shards: vec![sid],
lock_all: false,
});
Ok(entries.len() - 1)
}
fn ensure_participant(
entries: &mut Vec<Entry>,
db: &crate::txn::TxContext<'_>,
participant: Arc<dyn TxParticipant>,
rank: u8,
) -> DbResult<usize> {
if let Some(pos) = idx_for(entries, &participant) {
return Ok(pos);
}
let id = db.collection_id(participant.as_ref())?;
entries.push(Entry {
id,
rank,
participant,
shards: Vec::new(),
lock_all: false,
});
Ok(entries.len() - 1)
}
fn sort_entries(entries: &mut [Entry]) {
entries.sort_by_key(|a| (a.rank, a.id));
for e in entries.iter_mut() {
e.shards.sort_unstable();
}
}
fn set_write(
writes: &mut Vec<Write>,
entry: usize,
key: Vec<u8>,
value: Option<Vec<u8>>,
derived: bool,
) {
match writes.iter_mut().find(|w| w.entry == entry && w.key == key) {
Some(existing) => {
existing.value = value;
}
None => writes.push(Write {
entry,
key,
value,
derived,
}),
}
}
pub(crate) fn abort_locked<'a>(locked: Vec<Box<dyn TxLocked + 'a>>) {
for l in locked.into_iter().rev() {
l.abort();
}
}
pub(crate) struct PlanOutcome {
pub entries: Vec<Entry>,
pub rank0_len: usize,
pub rank0_ops: Vec<Op>,
pub rank1_ops: Vec<Op>,
}
pub(crate) fn plan(
db: &crate::txn::TxContext<'_>,
ops: Vec<Op>,
claim_ops: &[crate::unique::ExplicitClaim],
) -> DbResult<PlanOutcome> {
let mut rank0_ops = Vec::new();
let mut rank1_ops = Vec::new();
for op in ops {
if db.rank_of(op.participant.as_ref()) == 0 {
rank0_ops.push(op);
} else {
rank1_ops.push(op);
}
}
let mut checked: Vec<&'static str> = Vec::new();
for op in &rank1_ops {
let name = op.participant.name();
if checked.contains(&name) {
continue;
}
checked.push(name);
if !op.participant.tx_preplan_projection_targets()?.is_empty() {
return Err(DbError::TxTargetIsAlsoASource { collection: name });
}
}
let mut entries: Vec<Entry> = Vec::new();
for op in &rank0_ops {
ensure_entry(&mut entries, db, op.participant.clone(), 0, &op.key)?;
}
let sources: Vec<_> = entries.iter().map(|e| e.participant.clone()).collect();
for src in sources {
for target in src.tx_preplan_projection_targets()? {
ensure_participant(&mut entries, db, target, 1)?;
}
for target in src.tx_preplan_stream_targets()? {
let pos = ensure_participant(&mut entries, db, target, 1)?;
entries[pos].lock_all = true;
}
}
for op in &rank1_ops {
ensure_entry(&mut entries, db, op.participant.clone(), 1, &op.key)?;
}
for ec in claim_ops {
ensure_entry(&mut entries, db, Arc::clone(&ec.participant), 1, &ec.key)?;
}
sort_entries(&mut entries);
let rank0_len = entries.iter().filter(|e| e.rank == 0).count();
Ok(PlanOutcome {
entries,
rank0_len,
rank0_ops,
rank1_ops,
})
}
pub(crate) fn eval_rank0<'a>(
entries: &'a [Entry],
locked: &mut [Box<dyn TxLocked + 'a>],
ops: Vec<Op>,
) -> DbResult<EvalRank0Outcome> {
let mut writes: Vec<Write> = Vec::new();
let mut projection_inputs = Vec::new();
let mut eval_err = None;
for op in ops {
let Some(entry_pos) = idx_for(entries, &op.participant) else {
eval_err = Some(DbError::Internal("operation lost its participant"));
break;
};
let pending = writes
.iter()
.rev()
.find(|w| w.entry == entry_pos && w.key == op.key)
.map(|w| w.value.clone());
if let Action::Insert(bytes) = &op.action {
let occupied = match pending.as_ref() {
Some(value) => value.is_some(),
None => match locked[entry_pos].contains(&op.key) {
Ok(value) => value,
Err(e) => {
eval_err = Some(e);
break;
}
},
};
if occupied {
eval_err = Some(DbError::KeyExists);
break;
}
projection_inputs.push((entry_pos, op.key.clone(), None, Some(bytes.clone())));
set_write(&mut writes, entry_pos, op.key, Some(bytes.clone()), false);
continue;
}
let current: Option<Vec<u8>> = match pending {
Some(value) => value,
None => match locked[entry_pos].read(&op.key) {
Ok(v) => v,
Err(e) => {
eval_err = Some(e);
break;
}
},
};
match op.action {
Action::Put(bytes) => {
projection_inputs.push((
entry_pos,
op.key.clone(),
current.clone(),
Some(bytes.clone()),
));
set_write(&mut writes, entry_pos, op.key, Some(bytes), false);
}
Action::Insert(_) => unreachable!("insert handled by presence-only path"),
Action::Delete => {
projection_inputs.push((entry_pos, op.key.clone(), current.clone(), None));
set_write(&mut writes, entry_pos, op.key, None, false);
}
Action::Eval(f) => match f(current.as_deref()) {
Ok(Upsert::Put(bytes)) => {
projection_inputs.push((
entry_pos,
op.key.clone(),
current.clone(),
Some(bytes.clone()),
));
set_write(&mut writes, entry_pos, op.key, Some(bytes), false);
}
Ok(Upsert::Delete) => {
projection_inputs.push((entry_pos, op.key.clone(), current.clone(), None));
set_write(&mut writes, entry_pos, op.key, None, false);
}
Ok(Upsert::Keep) => {}
Err(e) => {
eval_err = Some(e);
break;
}
},
Action::Guard(f) => {
if let Err(e) = f(current.as_deref()) {
eval_err = Some(e);
break;
}
}
}
}
eval_err.map_or(Ok((writes, projection_inputs)), Err)
}
pub(crate) struct PendingCounter {
pub entry: usize,
pub key: Vec<u8>,
pub target: Arc<dyn ErasedCounterTarget>,
pub delta: i128,
}
pub(crate) struct PendingClaim {
pub entry: usize,
pub key: Vec<u8>,
pub owner: crate::unique::UniqueOwner,
pub kind: crate::txn::participant::ClaimKind,
pub namespace: &'static str,
}
pub(crate) struct Rank0Derived {
pub extra_shards: HashMap<usize, Vec<usize>>,
pub counters: Vec<PendingCounter>,
pub claims: Vec<PendingClaim>,
pub stream_rows: Vec<StreamRow>,
}
pub(crate) fn check_derived_encryption(
participant: &Arc<dyn TxParticipant>,
log_encrypted: bool,
) -> DbResult<()> {
if participant.encrypted() && !log_encrypted {
return Err(DbError::Config(
"an encrypted collection needs TxConfig::log_encryption_key: \
the transaction log would otherwise hold its values in the clear",
));
}
Ok(())
}
pub(crate) fn project_rank0(
entries: &[Entry],
writes: &mut Vec<Write>,
inputs: Vec<ProjectionInput>,
log_encrypted: bool,
) -> DbResult<Rank0Derived> {
let mut extra_shards: HashMap<usize, Vec<usize>> = HashMap::new();
let mut counter_slots: HashMap<(usize, &'static str, Vec<u8>), usize> = HashMap::new();
let mut counters: Vec<PendingCounter> = Vec::new();
let mut claim_row_slots: HashMap<(usize, &[u8]), usize> = HashMap::new();
let mut claim_rows: Vec<ClaimRowRef<'_>> = Vec::new();
for (src, key, old, new) in &inputs {
let key: &[u8] = key.as_slice();
match claim_row_slots.entry((*src, key)) {
std::collections::hash_map::Entry::Occupied(slot) => {
claim_rows[*slot.get()].3 = new.as_deref();
}
std::collections::hash_map::Entry::Vacant(slot) => {
slot.insert(claim_rows.len());
claim_rows.push((*src, key, old.as_deref(), new.as_deref()));
}
}
}
for (src, key, old, new) in &inputs {
let src = *src;
let derived = entries[src]
.participant
.tx_project(key, old.as_deref(), new.as_deref())?;
for row in derived {
check_derived_encryption(&row.participant, log_encrypted)?;
let Some(target_pos) = idx_for(entries, &row.participant) else {
return Err(DbError::UndeclaredIndexTarget {
index: row.participant.name(),
});
};
let sid = row.participant.shard_of(&row.key)?;
extra_shards.entry(target_pos).or_default().push(sid);
set_write(writes, target_pos, row.key, row.value, true);
}
let deltas =
entries[src]
.participant
.tx_counter_deltas(key, old.as_deref(), new.as_deref())?;
for d in deltas {
check_derived_encryption(&d.participant, log_encrypted)?;
let Some(target_pos) = idx_for(entries, &d.participant) else {
return Err(DbError::UndeclaredCounterTarget {
target: d.participant.name(),
});
};
let sid = d.participant.shard_of(&d.key)?;
extra_shards.entry(target_pos).or_default().push(sid);
match counter_slots.entry((target_pos, d.target.field(), d.key.clone())) {
std::collections::hash_map::Entry::Occupied(slot) => {
let pos = *slot.get();
counters[pos].delta = counters[pos]
.delta
.checked_add(d.delta)
.ok_or(DbError::Internal("counter delta sum overflow"))?;
}
std::collections::hash_map::Entry::Vacant(slot) => {
slot.insert(counters.len());
counters.push(PendingCounter {
entry: target_pos,
key: d.key,
target: d.target,
delta: d.delta,
});
}
}
}
}
let mut claims: Vec<PendingClaim> = Vec::new();
let mut emitting_sources: Vec<bool> = Vec::with_capacity(entries.len());
for entry in entries.iter() {
emitting_sources.push(entry.participant.tx_emits()?);
}
let stream_rows = claim_rows
.iter()
.filter(|(src, ..)| emitting_sources.get(*src).copied().unwrap_or(false))
.map(|(src, key, old, new)| {
(
*src,
key.to_vec(),
old.map(<[u8]>::to_vec),
new.map(<[u8]>::to_vec),
)
})
.collect();
for (src, key, old, new) in claim_rows {
let claim_ops = entries[src].participant.tx_claims(key, old, new)?;
for op in claim_ops {
check_derived_encryption(&op.participant, log_encrypted)?;
let Some(target_pos) = idx_for(entries, &op.participant) else {
return Err(DbError::UndeclaredUniqueTarget {
namespace: op.namespace,
});
};
let sid = op.participant.shard_of(&op.key)?;
extra_shards.entry(target_pos).or_default().push(sid);
claims.push(PendingClaim {
entry: target_pos,
key: op.key,
owner: op.owner,
kind: op.kind,
namespace: op.namespace,
});
}
}
Ok(Rank0Derived {
extra_shards,
counters,
claims,
stream_rows,
})
}
pub(crate) fn lower_streams(
entries: &[Entry],
writes: &mut Vec<Write>,
rows: Vec<StreamRow>,
log_encrypted: bool,
admissions: &mut crate::queue::QueueDepthAdmissions,
) -> DbResult<StreamLowered> {
let mut after_success = Vec::new();
for (src, key, old, new) in rows {
let mut stream =
entries[src]
.participant
.tx_stream_writes(&key, old.as_deref(), new.as_deref())?;
for admission in stream.queue_depth_admissions.drain(..) {
admission.record(admissions);
}
for row in stream.writes.drain(..) {
check_derived_encryption(&row.participant, log_encrypted)?;
let target_pos =
idx_for(entries, &row.participant).ok_or(DbError::UndeclaredIndexTarget {
index: row.participant.name(),
})?;
set_write(writes, target_pos, row.key, row.value, true);
}
after_success.append(&mut stream.after_success);
}
Ok(after_success)
}
fn render_owner(owner: &crate::unique::UniqueOwner) -> String {
let reservation = if owner.is_reservation() {
" (reservation)"
} else {
""
};
format!(
"collection_tag={:#x} key={}{reservation}",
owner.collection_id(),
crate::hex::hex(owner.key_bytes())
)
}
fn decode_unique_row(bytes: &[u8]) -> DbResult<UniqueRow> {
let row: UniqueRow = zerocopy::FromBytes::read_from_bytes(bytes)
.map_err(|_| DbError::FormatMismatch("unique namespace row decode failed".into()))?;
if !row.is_well_formed() {
return Err(DbError::FormatMismatch(
"unique namespace row is corrupt: key_len exceeds its own key buffer".into(),
));
}
Ok(row)
}
pub(crate) fn pending_claims_from_explicit(
entries: &[Entry],
claim_ops: Vec<crate::unique::ExplicitClaim>,
) -> DbResult<Vec<PendingClaim>> {
let mut out = Vec::with_capacity(claim_ops.len());
for ec in claim_ops {
let Some(entry) = idx_for(entries, &ec.participant) else {
return Err(DbError::Internal(
"explicit reserve/release lost its namespace participant",
));
};
out.push(PendingClaim {
entry,
key: ec.key,
owner: ec.owner,
kind: ec.kind,
namespace: ec.namespace,
});
}
Ok(out)
}
pub(crate) fn resolve_claims<'a>(
locked: &mut [Box<dyn TxLocked + 'a>],
claims: Vec<PendingClaim>,
writes: &mut Vec<Write>,
) -> DbResult<()> {
let (releases, acquisitions): (Vec<_>, Vec<_>) = claims
.into_iter()
.partition(|c| matches!(c.kind, ClaimKind::Release));
let mut now_ms: Option<u64> = None;
for c in releases.into_iter().chain(acquisitions) {
let current: Option<Vec<u8>> = match writes
.iter()
.rev()
.find(|w| w.entry == c.entry && w.key == c.key)
{
Some(w) => w.value.clone(),
None => locked
.get_mut(c.entry)
.ok_or(DbError::Internal("unique namespace lost its lock"))?
.read(&c.key)?,
};
let row = current.as_deref().map(decode_unique_row).transpose()?;
let kind = match c.kind {
ClaimKind::Reserve {
deadline_ms: ttl_ms,
} => {
let now = *now_ms.get_or_insert_with(crate::unique::reservation_now_ms);
let deadline_ms =
now.checked_add(ttl_ms)
.ok_or(DbError::UniqueReservationInvalid {
namespace: c.namespace,
reason: "deadline overflow",
})?;
ClaimKind::Reserve { deadline_ms }
}
other => other,
};
match decide(row, &c.owner, kind) {
ClaimDecision::Write(row) => {
set_write(writes, c.entry, c.key, Some(row.as_bytes().to_vec()), true)
}
ClaimDecision::Delete => set_write(writes, c.entry, c.key, None, true),
ClaimDecision::NoOp => {}
ClaimDecision::Conflict(owner) => {
return Err(DbError::UniqueViolation {
namespace: c.namespace,
key: crate::hex::hex(&c.key),
owner: render_owner(&owner),
});
}
}
}
Ok(())
}
pub(crate) fn resolve_counters<'a>(
locked: &mut [Box<dyn TxLocked + 'a>],
counters: Vec<PendingCounter>,
writes: &mut Vec<Write>,
) -> DbResult<()> {
for c in counters {
let current: Option<Vec<u8>> = match writes
.iter()
.rev()
.find(|w| w.entry == c.entry && w.key == c.key)
{
Some(w) => w.value.clone(),
None => locked
.get_mut(c.entry)
.ok_or(DbError::Internal("counter target lost its lock"))?
.read(&c.key)?,
};
match c
.target
.resolve(current.as_deref(), c.delta)
.map_err(|e| e.with_key(&c.key))?
{
CounterApply::Put(bytes) => set_write(writes, c.entry, c.key, Some(bytes), true),
CounterApply::Delete => set_write(writes, c.entry, c.key, None, true),
CounterApply::Skip => {}
}
}
Ok(())
}
pub(crate) fn eval_rank1<'a>(
entries: &'a [Entry],
locked: &mut [Box<dyn TxLocked + 'a>],
rank0_len: usize,
ops: Vec<Op>,
writes: &mut Vec<Write>,
) -> DbResult<()> {
for op in ops {
let Some(entry_pos) = idx_for(entries, &op.participant) else {
return Err(DbError::Internal("operation lost its participant"));
};
debug_assert!(entry_pos >= rank0_len);
let locked_pos = entry_pos;
let pending = writes
.iter()
.rev()
.find(|w| w.entry == entry_pos && w.key == op.key)
.map(|w| w.value.clone());
if let Action::Insert(bytes) = &op.action {
let occupied = match pending.as_ref() {
Some(value) => value.is_some(),
None => locked[locked_pos].contains(&op.key)?,
};
if occupied {
return Err(DbError::KeyExists);
}
set_write(writes, entry_pos, op.key, Some(bytes.clone()), false);
continue;
}
let current: Option<Vec<u8>> = match pending {
Some(value) => value,
None => locked[locked_pos].read(&op.key)?,
};
match op.action {
Action::Put(bytes) => set_write(writes, entry_pos, op.key, Some(bytes), false),
Action::Insert(_) => unreachable!("insert handled by presence-only path"),
Action::Delete => set_write(writes, entry_pos, op.key, None, false),
Action::Eval(f) => match f(current.as_deref()) {
Ok(Upsert::Put(bytes)) => set_write(writes, entry_pos, op.key, Some(bytes), false),
Ok(Upsert::Delete) => set_write(writes, entry_pos, op.key, None, false),
Ok(Upsert::Keep) => {}
Err(e) => return Err(e),
},
Action::Guard(f) => f(current.as_deref())?,
}
}
Ok(())
}
pub(crate) fn admit(
db: &crate::txn::TxContext<'_>,
limits: crate::txn::TxLimits,
entries: &[Entry],
writes: &[Write],
) -> DbResult<()> {
for entry in entries {
db.collection_id(entry.participant.as_ref())?;
}
if writes.len() > limits.max_ops {
let has_derived = writes.iter().any(|w| w.derived);
return Err(DbError::Client(if has_derived {
"transaction exceeds max_ops (including derived secondary-index, counter and queue rows)"
} else {
"transaction exceeds max_ops"
}));
}
for w in writes {
let value_len = w.value.as_deref().map_or(0, <[u8]>::len);
if let Some(cap) = entries[w.entry].participant.max_entry_bytes()
&& crate::entry::entry_size(w.key.len(), value_len as u32) as usize > cap
{
return Err(DbError::Client(if w.derived {
"derived secondary-index or counter row exceeds write_buffer_size"
} else {
"transaction entry exceeds write_buffer_size"
}));
}
}
Ok(())
}
pub(crate) fn max_bytes_err(writes: &[Write]) -> DbError {
let has_derived = writes.iter().any(|w| w.derived);
DbError::Client(if has_derived {
"transaction exceeds max_bytes (including derived secondary-index and counter rows)"
} else {
"transaction exceeds max_bytes"
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::txlog::record::{OpKind, VersionTag};
use crate::txn::participant::{ClaimOp, DeferredReplay, PlannedOp, TxBackend};
use crate::unique::{UniqueOwner, UniqueRow};
struct StubNamespace;
impl TxParticipant for StubNamespace {
fn name(&self) -> &'static str {
"stub_namespace"
}
fn backend(&self) -> TxBackend {
TxBackend::Bitcask
}
fn schema_fingerprint(&self) -> u64 {
0
}
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>> {
Err(DbError::Internal("stub"))
}
}
struct StubClaimSource {
namespace: Arc<dyn TxParticipant>,
}
impl TxParticipant for StubClaimSource {
fn name(&self) -> &'static str {
"stub_claim_source"
}
fn backend(&self) -> TxBackend {
TxBackend::Bitcask
}
fn schema_fingerprint(&self) -> u64 {
0
}
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>> {
Err(DbError::Internal("stub"))
}
fn tx_claims(
&self,
_key: &[u8],
old: Option<&[u8]>,
new: Option<&[u8]>,
) -> DbResult<Vec<ClaimOp>> {
let mut ops = Vec::new();
if old != new {
let owner = UniqueOwner::new(1, b"src", 0).unwrap();
if let Some(o) = old {
ops.push(ClaimOp {
participant: Arc::clone(&self.namespace),
namespace: "stub_namespace",
key: o.to_vec(),
owner,
kind: ClaimKind::Release,
});
}
if let Some(n) = new {
ops.push(ClaimOp {
participant: Arc::clone(&self.namespace),
namespace: "stub_namespace",
key: n.to_vec(),
owner,
kind: ClaimKind::Claim,
});
}
}
Ok(ops)
}
}
fn stub_entries() -> Vec<Entry> {
let namespace: Arc<dyn TxParticipant> = Arc::new(StubNamespace);
let source: Arc<dyn TxParticipant> = Arc::new(StubClaimSource {
namespace: Arc::clone(&namespace),
});
vec![
Entry {
id: 0,
rank: 0,
participant: source,
shards: vec![0],
lock_all: false,
},
Entry {
id: 1,
rank: 1,
participant: namespace,
shards: Vec::new(),
lock_all: false,
},
]
}
#[test]
fn project_rank0_does_not_coalesce_two_rows_claiming_the_same_key() {
let entries = stub_entries();
let inputs: Vec<ProjectionInput> = vec![
(0, vec![1], None, Some(b"rail".to_vec())),
(0, vec![2], None, Some(b"rail".to_vec())),
];
let mut writes = Vec::new();
let derived = project_rank0(&entries, &mut writes, inputs, false).unwrap();
assert_eq!(
derived.claims.len(),
2,
"two source rows claiming \"rail\" must produce two claim ops, not one"
);
assert!(
derived
.claims
.iter()
.all(|c| c.kind == ClaimKind::Claim && c.key == b"rail".to_vec()),
"both ops must be claims on \"rail\": {:?}",
derived
.claims
.iter()
.map(|c| (c.kind, c.key.clone()))
.collect::<Vec<_>>()
);
assert!(
derived.extra_shards.contains_key(&1),
"the namespace entry (index 1) must be in extra_shards for the rank-1 lock set"
);
}
#[test]
fn project_rank0_collapses_one_rows_chained_writes_before_diffing() {
let entries = stub_entries();
let inputs: Vec<ProjectionInput> = vec![
(0, vec![9], None, Some(b"a".to_vec())),
(0, vec![9], Some(b"a".to_vec()), Some(b"b".to_vec())),
(0, vec![9], Some(b"b".to_vec()), Some(b"a".to_vec())),
];
let mut writes = Vec::new();
let derived = project_rank0(&entries, &mut writes, inputs, false).unwrap();
assert_eq!(
derived.claims.len(),
1,
"the row's net effect is one claim, not one op per write: {:?}",
derived
.claims
.iter()
.map(|c| (c.kind, c.key.clone()))
.collect::<Vec<_>>()
);
assert_eq!(derived.claims[0].kind, ClaimKind::Claim);
assert_eq!(derived.claims[0].key, b"a".to_vec());
assert!(
derived.claims.iter().all(|c| c.key != b"b".to_vec()),
"no op may mention \"b\" — it was only ever an intermediate value"
);
}
struct StubClaimLocked {
store: std::collections::HashMap<Vec<u8>, Vec<u8>>,
}
impl TxLocked for StubClaimLocked {
fn read(&mut self, key: &[u8]) -> DbResult<Option<Vec<u8>>> {
Ok(self.store.get(key).cloned())
}
fn contains(&mut self, _key: &[u8]) -> DbResult<bool> {
unreachable!("resolve_claims does not call contains")
}
fn reserve(&mut self, _ops: &[PlannedOp<'_>]) -> DbResult<Vec<VersionTag>> {
unreachable!("resolve_claims does not reserve")
}
fn apply_put(&mut self, _key: &[u8], _value: &[u8], _tag: VersionTag) -> DbResult<usize> {
unreachable!("resolve_claims does not apply")
}
fn apply_delete(&mut self, _key: &[u8], _tag: VersionTag) -> DbResult<usize> {
unreachable!("resolve_claims does not apply")
}
fn needs_replay(&mut self, _key: &[u8], _kind: OpKind, _tag: VersionTag) -> DbResult<bool> {
unreachable!("resolve_claims does not replay")
}
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>> {
unreachable!("resolve_claims does not finish")
}
}
fn claim_locked(rows: Vec<(&[u8], UniqueOwner)>) -> Vec<Box<dyn TxLocked>> {
let mut store = std::collections::HashMap::new();
for (key, owner) in rows {
store.insert(key.to_vec(), UniqueRow::new(owner).as_bytes().to_vec());
}
vec![Box::new(StubClaimLocked { store })]
}
fn pending_claim(
entry: usize,
key: &[u8],
owner: UniqueOwner,
kind: ClaimKind,
) -> PendingClaim {
PendingClaim {
entry,
key: key.to_vec(),
owner,
kind,
namespace: "stub_namespace",
}
}
#[test]
fn resolve_claims_applies_releases_before_claims_regardless_of_list_order() {
let old_owner = UniqueOwner::new(1, b"users:1", 0).unwrap();
let new_owner = UniqueOwner::new(1, b"users:2", 0).unwrap();
let mut locked = claim_locked(vec![(b"rail".as_slice(), old_owner)]);
let claims = vec![
pending_claim(0, b"rail", new_owner, ClaimKind::Claim),
pending_claim(0, b"rail", old_owner, ClaimKind::Release),
];
let mut writes = Vec::new();
resolve_claims(&mut locked, claims, &mut writes).unwrap();
assert_eq!(
writes.len(),
1,
"the release and the claim collapse to one write on \"rail\""
);
let value = writes[0]
.value
.as_ref()
.expect("the claim must leave the row written, not deleted");
let row: UniqueRow = zerocopy::FromBytes::read_from_bytes(value.as_slice()).unwrap();
assert_eq!(
row.owner(),
new_owner,
"the claim must win once the release is resolved first"
);
}
#[test]
fn resolve_claims_refuses_a_release_of_a_foreign_row_and_leaves_it_untouched() {
let foreign = UniqueOwner::new(1, b"users:99", 0).unwrap();
let mine = UniqueOwner::new(1, b"users:1", 0).unwrap();
let mut locked = claim_locked(vec![(b"rail".as_slice(), foreign)]);
let claims = vec![pending_claim(0, b"rail", mine, ClaimKind::Release)];
let mut writes = Vec::new();
let err = resolve_claims(&mut locked, claims, &mut writes).unwrap_err();
assert!(
matches!(
err,
DbError::UniqueViolation {
namespace: "stub_namespace",
..
}
),
"{err}"
);
assert!(writes.is_empty(), "a foreign row must not be touched");
}
#[test]
fn resolve_claims_lets_the_first_of_two_claims_on_one_key_win() {
let first = UniqueOwner::new(1, b"users:1", 0).unwrap();
let second = UniqueOwner::new(2, b"channels:1", 0).unwrap();
let mut locked = claim_locked(vec![]);
let claims = vec![
pending_claim(0, b"dup", first, ClaimKind::Claim),
pending_claim(0, b"dup", second, ClaimKind::Claim),
];
let mut writes = Vec::new();
let err = resolve_claims(&mut locked, claims, &mut writes).unwrap_err();
assert!(matches!(err, DbError::UniqueViolation { .. }), "{err}");
assert_eq!(
writes.len(),
1,
"the first claim's write must still stand even though the transaction as a whole \
is refused by `resolve_claims`'s caller — `commit()` is what discards it"
);
}
#[test]
fn resolve_claims_rejects_a_corrupt_row_instead_of_panicking() {
let mut corrupt = vec![0u8; size_of::<UniqueRow>()];
corrupt[16] = 200;
let mut store = std::collections::HashMap::new();
store.insert(b"rail".to_vec(), corrupt);
let mut locked: Vec<Box<dyn TxLocked>> = vec![Box::new(StubClaimLocked { store })];
let claimant = UniqueOwner::new(1, b"users:1", 0).unwrap();
let claims = vec![pending_claim(0, b"rail", claimant, ClaimKind::Claim)];
let mut writes = Vec::new();
let err = resolve_claims(&mut locked, claims, &mut writes).unwrap_err();
assert!(matches!(err, DbError::FormatMismatch(_)), "{err}");
assert!(writes.is_empty());
}
}