refactor sync to use SyncOps
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
use crate::server::SyncOp;
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
@@ -28,97 +29,23 @@ pub enum ReplicaOp {
|
||||
},
|
||||
}
|
||||
|
||||
use ReplicaOp::*;
|
||||
|
||||
impl ReplicaOp {
|
||||
// Transform takes two operations A and B that happened concurrently and produces two
|
||||
// operations A' and B' such that `apply(apply(S, A), B') = apply(apply(S, B), A')`. This
|
||||
// function is used to serialize operations in a process similar to a Git "rebase".
|
||||
//
|
||||
// *
|
||||
// / \
|
||||
// op1 / \ op2
|
||||
// / \
|
||||
// * *
|
||||
//
|
||||
// this function "completes the diamond:
|
||||
//
|
||||
// * *
|
||||
// \ /
|
||||
// op2' \ / op1'
|
||||
// \ /
|
||||
// *
|
||||
//
|
||||
// such that applying op2' after op1 has the same effect as applying op1' after op2. This
|
||||
// allows two different systems which have already applied op1 and op2, respectively, and thus
|
||||
// reached different states, to return to the same state by applying op2' and op1',
|
||||
// respectively.
|
||||
pub fn transform(
|
||||
operation1: ReplicaOp,
|
||||
operation2: ReplicaOp,
|
||||
) -> (Option<ReplicaOp>, Option<ReplicaOp>) {
|
||||
match (&operation1, &operation2) {
|
||||
// Two creations or deletions of the same uuid reach the same state, so there's no need
|
||||
// for any further operations to bring the state together.
|
||||
(&Create { uuid: uuid1 }, &Create { uuid: uuid2 }) if uuid1 == uuid2 => (None, None),
|
||||
(&Delete { uuid: uuid1 }, &Delete { uuid: uuid2 }) if uuid1 == uuid2 => (None, None),
|
||||
|
||||
// Given a create and a delete of the same task, one of the operations is invalid: the
|
||||
// create implies the task does not exist, but the delete implies it exists. Somewhat
|
||||
// arbitrarily, we prefer the Create
|
||||
(&Create { uuid: uuid1 }, &Delete { uuid: uuid2 }) if uuid1 == uuid2 => {
|
||||
(Some(operation1), None)
|
||||
}
|
||||
(&Delete { uuid: uuid1 }, &Create { uuid: uuid2 }) if uuid1 == uuid2 => {
|
||||
(None, Some(operation2))
|
||||
}
|
||||
|
||||
// And again from an Update and a Create, prefer the Update
|
||||
(&Update { uuid: uuid1, .. }, &Create { uuid: uuid2 }) if uuid1 == uuid2 => {
|
||||
(Some(operation1), None)
|
||||
}
|
||||
(&Create { uuid: uuid1 }, &Update { uuid: uuid2, .. }) if uuid1 == uuid2 => {
|
||||
(None, Some(operation2))
|
||||
}
|
||||
|
||||
// Given a delete and an update, prefer the delete
|
||||
(&Update { uuid: uuid1, .. }, &Delete { uuid: uuid2 }) if uuid1 == uuid2 => {
|
||||
(None, Some(operation2))
|
||||
}
|
||||
(&Delete { uuid: uuid1 }, &Update { uuid: uuid2, .. }) if uuid1 == uuid2 => {
|
||||
(Some(operation1), None)
|
||||
}
|
||||
|
||||
// Two updates to the same property of the same task might conflict.
|
||||
(
|
||||
&Update {
|
||||
uuid: ref uuid1,
|
||||
property: ref property1,
|
||||
value: ref value1,
|
||||
timestamp: ref timestamp1,
|
||||
},
|
||||
&Update {
|
||||
uuid: ref uuid2,
|
||||
property: ref property2,
|
||||
value: ref value2,
|
||||
timestamp: ref timestamp2,
|
||||
},
|
||||
) if uuid1 == uuid2 && property1 == property2 => {
|
||||
// if the value is the same, there's no conflict
|
||||
if value1 == value2 {
|
||||
(None, None)
|
||||
} else if timestamp1 < timestamp2 {
|
||||
// prefer the later modification
|
||||
(None, Some(operation2))
|
||||
} else {
|
||||
// prefer the later modification or, if the modifications are the same,
|
||||
// just choose one of them
|
||||
(Some(operation1), None)
|
||||
}
|
||||
}
|
||||
|
||||
// anything else is not a conflict of any sort, so return the operations unchanged
|
||||
(_, _) => (Some(operation1), Some(operation2)),
|
||||
/// Convert this operation into a [`SyncOp`].
|
||||
pub fn into_sync(self) -> SyncOp {
|
||||
match self {
|
||||
Self::Create { uuid } => SyncOp::Create { uuid },
|
||||
Self::Delete { uuid } => SyncOp::Delete { uuid },
|
||||
Self::Update {
|
||||
uuid,
|
||||
property,
|
||||
value,
|
||||
timestamp,
|
||||
} => SyncOp::Update {
|
||||
uuid,
|
||||
property,
|
||||
value,
|
||||
timestamp,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -200,4 +127,37 @@ mod test {
|
||||
assert_eq!(deser, op);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_into_sync_create() {
|
||||
let uuid = Uuid::new_v4();
|
||||
assert_eq!(Create { uuid }.into_sync(), SyncOp::Create { uuid });
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_into_sync_delete() {
|
||||
let uuid = Uuid::new_v4();
|
||||
assert_eq!(Delete { uuid }.into_sync(), SyncOp::Delete { uuid });
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_into_sync_update() {
|
||||
let uuid = Uuid::new_v4();
|
||||
let timestamp = Utc::now();
|
||||
assert_eq!(
|
||||
Update {
|
||||
uuid,
|
||||
property: "prop".into(),
|
||||
value: Some("v".into()),
|
||||
timestamp,
|
||||
}
|
||||
.into_sync(),
|
||||
SyncOp::Update {
|
||||
uuid,
|
||||
property: "prop".into(),
|
||||
value: Some("v".into()),
|
||||
timestamp,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user