use aion_store::EventStore;
use aion_store::StoreError;
pub(super) trait ShardFenceSeam {
fn acquire(&self, shard: usize) -> Result<(), StoreError>;
fn publish(&self, shard: usize) -> Result<(), StoreError>;
fn is_current_owner(&self, shard: usize) -> bool;
fn extend(&self, shards: &[usize]);
}
pub(super) struct StoreFenceSeam<'a> {
pub store: &'a dyn EventStore,
}
impl ShardFenceSeam for StoreFenceSeam<'_> {
fn acquire(&self, shard: usize) -> Result<(), StoreError> {
self.store.acquire_owned_shard(shard)
}
fn publish(&self, shard: usize) -> Result<(), StoreError> {
self.store.publish_shard_owner(shard)
}
fn is_current_owner(&self, shard: usize) -> bool {
self.store.is_current_owner(shard)
}
fn extend(&self, shards: &[usize]) {
self.store.extend_owned_shards(shards);
}
}
pub(super) fn plan_adopted_shards<S: ShardFenceSeam>(
seam: &S,
shards: &[usize],
) -> Result<Vec<usize>, StoreError> {
let mut committed: Vec<usize> = Vec::with_capacity(shards.len());
for &shard in shards {
if fence_survives(seam, shard)? {
committed.push(shard);
}
}
let recoverable: Vec<usize> = committed
.into_iter()
.filter(|&shard| seam.is_current_owner(shard))
.collect();
seam.extend(&recoverable);
Ok(recoverable)
}
fn fence_survives<S: ShardFenceSeam>(seam: &S, shard: usize) -> Result<bool, StoreError> {
match seam.acquire(shard) {
Ok(()) => {}
Err(StoreError::NotOwner { .. }) => return Ok(false),
Err(error) => return Err(error),
}
match seam.publish(shard) {
Ok(()) => Ok(true),
Err(StoreError::NotOwner { .. }) => Ok(false),
Err(error) => Err(error),
}
}
#[cfg(test)]
pub(super) fn plan_adopted_shards_prefix_buggy<S: ShardFenceSeam>(
seam: &S,
shards: &[usize],
) -> Result<Vec<usize>, StoreError> {
let mut acquired: Vec<usize> = Vec::with_capacity(shards.len());
for &shard in shards {
match seam.acquire(shard) {
Ok(()) => acquired.push(shard),
Err(StoreError::NotOwner { .. }) => {}
Err(error) => return Err(error),
}
}
seam.extend(&acquired);
for &shard in &acquired {
match seam.publish(shard) {
Ok(()) | Err(StoreError::NotOwner { .. }) => {}
Err(error) => return Err(error),
}
}
Ok(acquired)
}
#[cfg(test)]
mod tests {
use std::cell::RefCell;
use std::collections::BTreeSet;
use super::{
ShardFenceSeam, StoreError, plan_adopted_shards, plan_adopted_shards_prefix_buggy,
};
#[derive(Debug, Clone, PartialEq, Eq)]
enum Call {
Acquire(usize),
Publish(usize),
IsOwner(usize),
Extend(Vec<usize>),
}
struct FakeSeam<'a> {
lose_acquire: BTreeSet<usize>,
lose_publish: BTreeSet<usize>,
lose_residual: BTreeSet<usize>,
calls: RefCell<Vec<Call>>,
executed_world: &'a RefCell<BTreeSet<usize>>,
executed_here: RefCell<usize>,
}
impl<'a> FakeSeam<'a> {
fn new(executed_world: &'a RefCell<BTreeSet<usize>>) -> Self {
Self {
lose_acquire: BTreeSet::new(),
lose_publish: BTreeSet::new(),
lose_residual: BTreeSet::new(),
calls: RefCell::new(Vec::new()),
executed_world,
executed_here: RefCell::new(0),
}
}
fn lose_acquire(mut self, shard: usize) -> Self {
self.lose_acquire.insert(shard);
self
}
fn lose_publish(mut self, shard: usize) -> Self {
self.lose_publish.insert(shard);
self
}
fn lose_residual(mut self, shard: usize) -> Self {
self.lose_residual.insert(shard);
self
}
fn recover(&self, recoverable: &[usize]) {
for &shard in recoverable {
*self.executed_here.borrow_mut() += 1;
self.executed_world.borrow_mut().insert(shard);
}
}
fn calls(&self) -> Vec<Call> {
self.calls.borrow().clone()
}
}
impl ShardFenceSeam for FakeSeam<'_> {
fn acquire(&self, shard: usize) -> Result<(), StoreError> {
self.calls.borrow_mut().push(Call::Acquire(shard));
if self.lose_acquire.contains(&shard) {
return Err(StoreError::NotOwner { shard });
}
Ok(())
}
fn publish(&self, shard: usize) -> Result<(), StoreError> {
self.calls.borrow_mut().push(Call::Publish(shard));
if self.lose_publish.contains(&shard) {
return Err(StoreError::NotOwner { shard });
}
Ok(())
}
fn is_current_owner(&self, shard: usize) -> bool {
self.calls.borrow_mut().push(Call::IsOwner(shard));
!self.lose_residual.contains(&shard)
}
fn extend(&self, shards: &[usize]) {
self.calls.borrow_mut().push(Call::Extend(shards.to_vec()));
}
}
fn first_extend_index(calls: &[Call]) -> usize {
calls
.iter()
.position(|call| matches!(call, Call::Extend(_)))
.unwrap_or(usize::MAX)
}
fn publish_index(calls: &[Call], shard: usize) -> usize {
calls
.iter()
.position(|call| matches!(call, Call::Publish(s) if *s == shard))
.unwrap_or(usize::MAX)
}
#[test]
fn publish_fence_precedes_extend_for_every_survivor() -> Result<(), StoreError> {
let world = RefCell::new(BTreeSet::new());
let seam = FakeSeam::new(&world);
let recoverable = plan_adopted_shards(&seam, &[0, 1, 2])?;
assert_eq!(recoverable, vec![0, 1, 2]);
let calls = seam.calls();
let extend_at = first_extend_index(&calls);
for shard in &recoverable {
assert!(
publish_index(&calls, *shard) < extend_at,
"shard {shard}'s publish-fence must precede the scope widening (extend)"
);
}
assert_eq!(
calls
.iter()
.filter(|c| matches!(c, Call::Extend(_)))
.count(),
1,
"extend runs exactly once"
);
assert!(calls.contains(&Call::Extend(vec![0, 1, 2])));
Ok(())
}
#[test]
fn deposed_at_publish_leaves_scope_unchanged_and_recovers_nothing() -> Result<(), StoreError> {
let world = RefCell::new(BTreeSet::new());
let seam = FakeSeam::new(&world).lose_publish(7);
let recoverable = plan_adopted_shards(&seam, &[7])?;
assert!(recoverable.is_empty(), "a deposed shard is not recoverable");
seam.recover(&recoverable);
assert_eq!(*seam.executed_here.borrow(), 0, "zero external executions");
assert!(
seam.calls().contains(&Call::Extend(vec![])),
"extend runs once over an empty survivor set — owned scope unchanged"
);
Ok(())
}
#[test]
fn deposed_at_acquire_recovers_nothing() -> Result<(), StoreError> {
let world = RefCell::new(BTreeSet::new());
let seam = FakeSeam::new(&world).lose_acquire(3);
let recoverable = plan_adopted_shards(&seam, &[3])?;
assert!(recoverable.is_empty());
assert!(
!seam.calls().iter().any(|c| matches!(c, Call::Publish(3))),
"a shard that lost acquire is never published"
);
Ok(())
}
#[test]
fn partial_win_a_fenced_b() -> Result<(), StoreError> {
let world = RefCell::new(BTreeSet::new());
let seam = FakeSeam::new(&world).lose_publish(1);
let recoverable = plan_adopted_shards(&seam, &[0, 1])?;
assert_eq!(recoverable, vec![0], "only A survives the fence");
seam.recover(&recoverable);
assert_eq!(*seam.executed_here.borrow(), 1, "A executes exactly once");
assert!(seam.calls().contains(&Call::Extend(vec![0])));
assert!(
!world.borrow().contains(&1),
"B is never executed (never recovered)"
);
Ok(())
}
#[test]
fn residual_window_depose_excludes_shard() -> Result<(), StoreError> {
let world = RefCell::new(BTreeSet::new());
let seam = FakeSeam::new(&world).lose_residual(5);
let recoverable = plan_adopted_shards(&seam, &[5])?;
assert!(
recoverable.is_empty(),
"a shard deposed in the residual window is not recovered"
);
Ok(())
}
#[test]
fn falsifiability_external_execution_is_exactly_once_under_fix() -> Result<(), StoreError> {
const SHARD: usize = 0;
let world = RefCell::new(BTreeSet::new());
let winner = FakeSeam::new(&world);
let won = plan_adopted_shards(&winner, &[SHARD])?;
winner.recover(&won);
let loser = FakeSeam::new(&world).lose_publish(SHARD);
let lost = plan_adopted_shards(&loser, &[SHARD])?;
loser.recover(&lost);
let fixed_total = *winner.executed_here.borrow() + *loser.executed_here.borrow();
assert_eq!(
fixed_total, 1,
"under the fence fix the shard's workflow executes EXACTLY once across both survivors"
);
let world_buggy = RefCell::new(BTreeSet::new());
let winner_b = FakeSeam::new(&world_buggy);
let won_b = plan_adopted_shards_prefix_buggy(&winner_b, &[SHARD])?;
winner_b.recover(&won_b);
let loser_b = FakeSeam::new(&world_buggy).lose_publish(SHARD);
let lost_b = plan_adopted_shards_prefix_buggy(&loser_b, &[SHARD])?;
loser_b.recover(&lost_b);
let buggy_total = *winner_b.executed_here.borrow() + *loser_b.executed_here.borrow();
assert_eq!(
buggy_total, 2,
"the PRE-FIX order double-executes the shard's workflow — this is the bug the fix \
closes, and proves this falsifiability test actually detects it"
);
Ok(())
}
}