use petgraph::graphmap::DiGraphMap;
use petgraph::visit::{IntoNodeIdentifiers, Topo};
use std::collections::{HashMap, HashSet};
use std::{fmt::Debug, marker::PhantomData};
use crate::graph::{concurrent_bubbles, split_bubble};
use crate::group::crdt::{GroupCrdtInnerError, apply_remove_unsafe};
use crate::group::{AuthorityGraphs, GroupAction, GroupCrdtInnerState, GroupMember, apply_action};
use crate::traits::{Conditions, IdentityHandle, Operation, OperationId, Resolver};
#[derive(Clone, Debug, Default)]
pub struct StrongRemove<ID, OP, M, C> {
_phantom: PhantomData<(ID, OP, M, C)>,
}
impl<ID, OP, M, C> Resolver<ID, OP, M, C> for StrongRemove<ID, OP, M, C>
where
ID: IdentityHandle,
OP: OperationId,
M: Clone + Operation<ID, OP, C>,
C: Conditions,
{
type State = GroupCrdtInnerState<ID, OP, M, C>;
type Error = GroupCrdtInnerError<OP>;
fn rebuild_required(y: &Self::State, operation: &M) -> Result<bool, Self::Error> {
let dependencies = operation.dependencies().into_iter().collect();
Ok(y.heads() != dependencies)
}
fn process(mut y: Self::State) -> Result<Self::State, Self::Error> {
y.ignore = HashSet::default();
y.mutual_removes = HashSet::default();
let bubbles = concurrent_bubbles(&y.graph);
let mut processed_bubbles = Vec::new();
let mut topo = Topo::new(&y.graph);
while let Some(operation_id) = topo.next(&y.graph) {
let bubble = bubbles.iter().find(|bubble| bubble.contains(&operation_id));
let Some(bubble) = bubble else {
y = StrongRemove::apply_operation(y, operation_id);
continue;
};
if processed_bubbles.iter().any(|b| b == &bubble) {
continue;
};
y = StrongRemove::process_bubble(y, bubble);
processed_bubbles.push(bubble);
}
Ok(y)
}
}
impl<ID, OP, M, C> StrongRemove<ID, OP, M, C>
where
ID: IdentityHandle,
OP: OperationId,
M: Clone + Operation<ID, OP, C>,
C: Conditions,
{
fn process_bubble(
mut y: GroupCrdtInnerState<ID, OP, M, C>,
bubble: &HashSet<OP>,
) -> GroupCrdtInnerState<ID, OP, M, C> {
let bubble_graph = {
let non_bubble_operations: Vec<_> = y
.graph
.node_identifiers()
.filter(|n| !bubble.contains(n))
.collect();
let mut bubble_graph = y.graph.clone();
for node_id in non_bubble_operations {
bubble_graph.remove_node(node_id);
}
bubble_graph
};
let mut authority_graphs = Self::build_authority_graphs(&y.operations, &bubble_graph);
y = Self::compute_filter(y, bubble, &mut authority_graphs);
let mut topo = Topo::new(&bubble_graph);
while let Some(operation_id) = topo.next(&bubble_graph) {
y = Self::apply_operation(y, operation_id);
}
y
}
fn is_mutual_remove(operation: &M, authority_graphs: &mut AuthorityGraphs<ID, OP>) -> bool {
let removed = removed_or_demoted_manager(operation);
let added = added_or_promoted_manager(operation);
if removed.is_none() && added.is_none() {
return false;
}
let group_id = operation.group_id();
authority_graphs.is_cycle(&group_id, &operation.id())
}
fn is_readd(group_id: ID, removed: ID, operation: &M) -> bool {
if group_id != operation.group_id() {
return false;
}
let GroupAction::Add { member: added, .. } = &operation.action() else {
return false;
};
added.id() == removed
}
fn is_removed(group_id: ID, removed: ID, operation: &M) -> bool {
if group_id != operation.group_id() {
return false;
}
operation.author() == removed
}
fn compute_filter(
mut y: GroupCrdtInnerState<ID, OP, M, C>,
bubble: &HashSet<OP>,
authority_graphs: &mut AuthorityGraphs<ID, OP>,
) -> GroupCrdtInnerState<ID, OP, M, C> {
let mut filter = HashSet::new();
let mut mutual_removes = HashSet::new();
for operation_id in bubble {
let operation = y
.operations
.get(operation_id)
.expect("all operations present in map");
let Some(removed) = removed_or_demoted_manager(operation) else {
continue;
};
if Self::is_mutual_remove(operation, authority_graphs) {
mutual_removes.insert(*operation_id);
}
let group_id = operation.group_id();
let (mut concurrent, ..) = split_bubble(&y.graph, bubble, *operation_id);
concurrent.retain(|id| {
let concurrent_operation =
y.operations.get(id).expect("all operations present in map");
let is_readd = Self::is_readd(group_id, removed, concurrent_operation);
let is_removed = Self::is_removed(group_id, removed, concurrent_operation);
is_removed || is_readd
});
filter.extend(concurrent.iter());
}
y.ignore = filter;
y.mutual_removes = mutual_removes;
y
}
fn build_authority_graphs(
operations: &HashMap<OP, M>,
bubble_graph: &DiGraphMap<OP, ()>,
) -> AuthorityGraphs<ID, OP> {
let mut authority_graphs = AuthorityGraphs::new(bubble_graph.clone());
for id in bubble_graph.nodes() {
let operation = operations.get(&id).expect("all operations present in map");
let author = operation.author();
let group_id = operation.group_id();
if let Some(removed) = removed_or_demoted_manager(operation) {
authority_graphs.add_removal(group_id, author, removed, id);
};
if let Some(added) = added_or_promoted_manager(operation) {
authority_graphs.add_delegation(group_id, author, added, id);
};
}
authority_graphs
}
fn apply_operation(
mut y: GroupCrdtInnerState<ID, OP, M, C>,
operation_id: OP,
) -> GroupCrdtInnerState<ID, OP, M, C> {
let operation = y
.operations
.get(&operation_id)
.expect("all processed operations exist");
let dependencies = HashSet::from_iter(operation.dependencies().clone());
let mut groups_y = y
.state_at(&dependencies)
.expect("all state objects to exist");
groups_y = if !y.mutual_removes.contains(&operation_id) {
apply_action(
groups_y,
operation.group_id(),
operation.id(),
operation.author(),
&operation.action(),
&y.ignore,
)
.state()
.to_owned()
} else {
let Some(removed) = removed_or_demoted_manager(operation) else {
unreachable!();
};
apply_remove_unsafe(
groups_y,
operation.group_id(),
GroupMember::Individual(removed),
)
};
y.states.insert(operation.id(), groups_y);
y
}
}
fn removed_or_demoted_manager<ID, OP, M, C>(operation: &M) -> Option<ID>
where
ID: IdentityHandle,
OP: OperationId,
M: Clone + Operation<ID, OP, C>,
C: Conditions,
{
let action = operation.action();
if let GroupAction::Remove { member: removed } = action {
return Some(removed.id());
}
if let GroupAction::Demote {
member: demoted,
access,
} = action
&& !access.is_manage()
{
return Some(demoted.id());
}
None
}
fn added_or_promoted_manager<ID, OP, M, C>(operation: &M) -> Option<ID>
where
ID: IdentityHandle,
OP: OperationId,
M: Clone + Operation<ID, OP, C>,
C: Conditions,
{
let action = operation.action();
if let GroupAction::Add {
member: added,
access,
} = &action
&& access.is_manage()
{
return Some(added.id());
}
if let GroupAction::Promote {
member: promoted,
access,
} = action
&& access.is_manage()
{
return Some(promoted.id());
}
None
}
#[cfg(test)]
mod tests {
use crate::Access;
use crate::group::GroupMember;
use crate::test_utils::{
MemberId, TestGroupState, add_member, assert_members, create_group, demote_member,
remove_member, sync,
};
use super::*;
const G0: char = '0';
const G1: char = '1';
const ALICE: char = 'A';
const BOB: char = 'B';
const CLAIRE: char = 'C';
const DAVE: char = 'D';
const EVE: char = 'E';
const FRANK: char = 'F';
const GRACE: char = 'G';
#[test]
fn mutual_removal_filter() {
let y = TestGroupState::new();
let op0 = create_group(
ALICE,
0,
G1,
vec![
(GroupMember::Individual(ALICE), Access::manage()),
(GroupMember::Individual(BOB), Access::manage()),
(GroupMember::Individual(CLAIRE), Access::manage()),
],
vec![],
);
let op1 = remove_member(ALICE, 1, G1, GroupMember::Individual(BOB), vec![op0.id()]);
let op2 = remove_member(BOB, 2, G1, GroupMember::Individual(ALICE), vec![op0.id()]);
let expected = vec![(CLAIRE, Access::manage())];
let y_i = sync(y, &[op0, op1, op2]);
assert_members(&y_i, G1, &expected);
assert_eq!(y_i.inner.ignore.len(), 2);
assert_eq!(y_i.inner.mutual_removes.len(), 2);
}
#[test]
fn mutual_remove_cycles_detected() {
let y = TestGroupState::new();
let op0 = create_group(
ALICE,
0,
G1,
vec![
(GroupMember::Individual(ALICE), Access::manage()),
(GroupMember::Individual(BOB), Access::manage()),
(GroupMember::Individual(CLAIRE), Access::manage()),
],
vec![],
);
let op1 = remove_member(ALICE, 1, G1, GroupMember::Individual(BOB), vec![op0.id()]);
let op2 = remove_member(BOB, 2, G1, GroupMember::Individual(CLAIRE), vec![op0.id()]);
let op3 = remove_member(
CLAIRE,
3,
G1,
GroupMember::Individual(ALICE),
vec![op0.id()],
);
let expected: Vec<(MemberId, Access)> = vec![];
let y_i = sync(y, &[op0, op1, op2, op3]);
assert_members(&y_i, G1, &expected);
assert_eq!(y_i.inner.ignore.len(), 3);
assert_eq!(y_i.inner.mutual_removes.len(), 3);
}
#[test]
fn mutual_remove_cycle_with_delegation() {
let y = TestGroupState::new();
let op0 = create_group(
ALICE,
0,
G1,
vec![
(GroupMember::Individual(ALICE), Access::manage()),
(GroupMember::Individual(BOB), Access::manage()),
(GroupMember::Individual(CLAIRE), Access::manage()),
],
vec![],
);
let op1 = remove_member(ALICE, 1, G1, GroupMember::Individual(BOB), vec![op0.id()]);
let op2 = remove_member(BOB, 2, G1, GroupMember::Individual(CLAIRE), vec![op0.id()]);
let op3 = add_member(
CLAIRE,
3,
G1,
GroupMember::Individual(DAVE),
Access::manage(),
vec![op0.id()],
);
let op4 = remove_member(DAVE, 4, G1, GroupMember::Individual(ALICE), vec![op3.id()]);
let expected: Vec<(MemberId, Access)> = vec![];
let y_i = sync(y, &[op0, op1, op2, op3, op4]);
assert_members(&y_i, G1, &expected);
assert_eq!(y_i.inner.ignore.len(), 3);
assert_eq!(y_i.inner.mutual_removes.len(), 3);
}
#[test]
fn demote_remove_filter() {
let y = TestGroupState::new();
let op0 = create_group(
ALICE,
0,
G1,
vec![
(GroupMember::Individual(ALICE), Access::manage()),
(GroupMember::Individual(BOB), Access::manage()),
(GroupMember::Individual(CLAIRE), Access::manage()),
],
vec![],
);
let op1 = demote_member(
ALICE,
1,
G1,
GroupMember::Individual(BOB),
Access::write(),
vec![op0.id()],
);
let op2 = remove_member(BOB, 2, G1, GroupMember::Individual(CLAIRE), vec![op0.id()]);
let expected = vec![
(ALICE, Access::manage()),
(BOB, Access::write()),
(CLAIRE, Access::manage()),
];
let y_final = sync(y, &[op0, op1, op2]);
assert_members(&y_final, G1, &expected);
}
#[test]
fn demote_add_filter() {
let y = TestGroupState::new();
let op0 = create_group(
ALICE,
0,
G1,
vec![
(GroupMember::Individual(ALICE), Access::manage()),
(GroupMember::Individual(BOB), Access::manage()),
(GroupMember::Individual(CLAIRE), Access::manage()),
],
vec![],
);
let op1 = demote_member(
ALICE,
1,
G1,
GroupMember::Individual(BOB),
Access::write(),
vec![op0.id()],
);
let op2 = add_member(
BOB,
2,
G1,
GroupMember::Individual(DAVE),
Access::read(),
vec![op0.id()],
);
let expected = vec![
(ALICE, Access::manage()),
(BOB, Access::write()),
(CLAIRE, Access::manage()),
];
let y_i = sync(y, &[op0, op1, op2]);
assert_members(&y_i, G1, &expected);
}
#[test]
fn remove_dependencies_filter() {
let y = TestGroupState::new();
let op0 = create_group(
ALICE,
0,
G1,
vec![
(GroupMember::Individual(ALICE), Access::manage()),
(GroupMember::Individual(BOB), Access::manage()),
],
vec![],
);
let op1 = remove_member(ALICE, 1, G1, GroupMember::Individual(BOB), vec![op0.id()]);
let op2 = add_member(
BOB,
2,
G1,
GroupMember::Individual(CLAIRE),
Access::manage(),
vec![op0.id()],
);
let op3 = add_member(
CLAIRE,
3,
G1,
GroupMember::Individual(DAVE),
Access::read(),
vec![op2.id()],
);
let expected_members = vec![(ALICE, Access::manage())];
let y_i = sync(y, &[op0, op1, op2, op3]);
assert_members(&y_i, G1, &expected_members);
}
#[test]
fn remove_readd_dependencies_filter() {
let y = TestGroupState::new();
let op0 = create_group(
ALICE,
0,
G1,
vec![
(GroupMember::Individual(ALICE), Access::manage()),
(GroupMember::Individual(BOB), Access::manage()),
(GroupMember::Individual(CLAIRE), Access::manage()),
],
vec![],
);
let op1 = remove_member(ALICE, 1, G1, GroupMember::Individual(BOB), vec![op0.id()]);
let op2 = add_member(
ALICE,
2,
G1,
GroupMember::Individual(BOB),
Access::manage(),
vec![op1.id()],
);
let op3 = add_member(
BOB,
3,
G1,
GroupMember::Individual(DAVE),
Access::read(),
vec![op0.id()],
);
let op4 = add_member(
BOB,
4,
G1,
GroupMember::Individual(EVE),
Access::read(),
vec![op2.id(), op3.id()],
);
let expected = vec![
(ALICE, Access::manage()),
(BOB, Access::manage()),
(CLAIRE, Access::manage()),
(EVE, Access::read()),
];
let y_final = sync(y, &[op0, op1, op2, op3, op4]);
assert_members(&y_final, G1, &expected);
}
#[test]
fn two_bubbles() {
let y = TestGroupState::new();
let op0 = create_group(
ALICE,
0,
G0,
vec![
(GroupMember::Individual(ALICE), Access::manage()),
(GroupMember::Individual(BOB), Access::manage()),
],
vec![],
);
let op1 = remove_member(ALICE, 1, G0, GroupMember::Individual(BOB), vec![op0.id()]);
let op2 = add_member(
BOB,
2,
G0,
GroupMember::Individual(CLAIRE),
Access::read(),
vec![op0.id()],
);
let op3 = add_member(
ALICE,
3,
G0,
GroupMember::Individual(DAVE),
Access::manage(),
vec![op1.id(), op2.id()],
);
let op4 = add_member(
DAVE,
4,
G0,
GroupMember::Individual(EVE),
Access::read(),
vec![op3.id()],
);
let op5 = add_member(
ALICE,
5,
G0,
GroupMember::Individual(FRANK),
Access::manage(),
vec![op4.id()],
);
let op6 = add_member(
FRANK,
6,
G0,
GroupMember::Individual(GRACE),
Access::read(),
vec![op5.id()],
);
let op7 = remove_member(DAVE, 7, G0, GroupMember::Individual(ALICE), vec![op4.id()]);
let expected_members = vec![(DAVE, Access::manage()), (EVE, Access::read())];
let y_i = sync(y, &[op0, op1, op2, op3, op4, op5, op6, op7]);
assert_members(&y_i, G0, &expected_members);
}
#[test]
fn concurrent_readds_filtered() {
let y = TestGroupState::new();
let op0 = create_group(
ALICE,
0,
G1,
vec![
(GroupMember::Individual(ALICE), Access::manage()),
(GroupMember::Individual(BOB), Access::manage()),
(GroupMember::Individual(CLAIRE), Access::manage()),
],
vec![],
);
let op1 = remove_member(ALICE, 1, G1, GroupMember::Individual(BOB), vec![op0.id()]);
let op2 = add_member(
ALICE,
2,
G1,
GroupMember::Individual(BOB),
Access::manage(),
vec![op1.id()],
);
let op3 = remove_member(CLAIRE, 3, G1, GroupMember::Individual(BOB), vec![op0.id()]);
let expected = vec![(ALICE, Access::manage()), (CLAIRE, Access::manage())];
let y_final = sync(y, &[op0, op1, op2, op3]);
assert_members(&y_final, G1, &expected);
}
#[test]
fn filter_only_concurrent_operations() {
let y = TestGroupState::new();
let op0 = create_group(
ALICE,
0,
G1,
vec![
(GroupMember::Individual(ALICE), Access::manage()),
(GroupMember::Individual(BOB), Access::manage()),
],
vec![],
);
let op1 = add_member(
BOB,
1,
G1,
GroupMember::Individual(CLAIRE),
Access::read(),
vec![op0.id()],
);
let op2 = demote_member(
ALICE,
2,
G1,
GroupMember::Individual(BOB),
Access::write(),
vec![op1.id()],
);
let op3 = add_member(
BOB,
3,
G1,
GroupMember::Individual(DAVE),
Access::read(),
vec![op1.id()],
);
let expected = vec![
(ALICE, Access::manage()),
(BOB, Access::write()),
(CLAIRE, Access::read()),
];
let y_i = sync(y, &[op0, op1, op2, op3]);
assert_members(&y_i, G1, &expected);
}
}