| 22 |
22 |
|
use super::db::{get_sync_state, set_sync_state};
|
| 23 |
23 |
|
use super::schema::{ConflictStrategy, SyncSchema};
|
| 24 |
24 |
|
use super::stash;
|
| 25 |
|
- |
use crate::conflict::{Resolution, detect_conflicts, resolve_lww_at};
|
|
25 |
+ |
use crate::conflict::{Resolution, change_order, detect_conflicts, resolve_lww_at};
|
| 26 |
26 |
|
use crate::error::Result;
|
| 27 |
27 |
|
use crate::ids::DeviceId;
|
| 28 |
28 |
|
use crate::types::{ChangeEntry, ChangeOp, Hlc, PulledChange};
|
| 164 |
164 |
|
/// advance the clock past the remote HLCs, stamp local pending edits, split into
|
| 165 |
165 |
|
/// clean vs conflicting against local pending, HLC-gate the clean set against the
|
| 166 |
166 |
|
/// committed ledger, resolve conflicts by `resolve_lww`, and collapse to one
|
| 167 |
|
- |
/// entry per row (highest HLC wins, operation-agnostic).
|
|
167 |
+ |
/// entry per row. The conflict step and the collapse step both decide a winner,
|
|
168 |
+ |
/// and both ask [`change_order`], so they cannot disagree.
|
| 168 |
169 |
|
///
|
| 169 |
170 |
|
/// Every discard along that path is stashed under `scope` first (see
|
| 170 |
171 |
|
/// [`super::stash`]). Three of them exist and they are easy to miscount: the
|
| 334 |
335 |
|
Ok(out)
|
| 335 |
336 |
|
}
|
| 336 |
337 |
|
|
| 337 |
|
- |
/// Collapse to one entry per `(table, row_id)`, keeping the highest HLC,
|
| 338 |
|
- |
/// operation-agnostic, so a newer delete beats an older edit and vice versa, and
|
| 339 |
|
- |
/// every device converges on the same winner. First-seen order is preserved
|
| 340 |
|
- |
/// (apply re-orders by schema anyway).
|
|
338 |
+ |
/// Collapse to one entry per `(table, row_id)`, keeping the winner under
|
|
339 |
+ |
/// [`change_order`], operation-agnostic, so a newer delete beats an older edit
|
|
340 |
+ |
/// and vice versa, and every device converges on the same winner. First-seen
|
|
341 |
+ |
/// order is preserved (apply re-orders by schema anyway).
|
|
342 |
+ |
///
|
|
343 |
+ |
/// The winner rule is [`change_order`]'s and not this function's, which is the
|
|
344 |
+ |
/// point: the conflict resolver answers the same question and the two must not
|
|
345 |
+ |
/// be able to disagree.
|
| 341 |
346 |
|
fn collapse_max_hlc(entries: Vec<ChangeEntry>) -> Vec<ChangeEntry> {
|
| 342 |
347 |
|
let mut best: HashMap<(String, String), usize> = HashMap::new();
|
| 343 |
348 |
|
let mut kept: Vec<ChangeEntry> = Vec::new();
|
| 344 |
349 |
|
for e in entries {
|
| 345 |
350 |
|
let key = (e.table.clone(), e.row_id.clone());
|
| 346 |
351 |
|
match best.get(&key) {
|
| 347 |
|
- |
Some(&i) if kept[i].hlc >= e.hlc => {}
|
|
352 |
+ |
Some(&i) if change_order(&kept[i], &e).is_ge() => {}
|
| 348 |
353 |
|
Some(&i) => kept[i] = e,
|
| 349 |
354 |
|
None => {
|
| 350 |
355 |
|
best.insert(key, kept.len());
|
| 492 |
497 |
|
assert_eq!(labels(&out), ["r2-new", "r1-only"]);
|
| 493 |
498 |
|
}
|
| 494 |
499 |
|
|
|
500 |
+ |
/// The case the shared order exists for. Two changes for one row at an
|
|
501 |
+ |
/// exact HLC tie: the winner has to be the same on every device, and the
|
|
502 |
+ |
/// only thing every device agrees on is the payload bytes. Arrival order is
|
|
503 |
+ |
/// not that thing, so the two orders must agree here.
|
|
504 |
+ |
#[test]
|
|
505 |
+ |
fn collapse_breaks_an_exact_hlc_tie_on_payload_not_arrival_order() {
|
|
506 |
+ |
let a = entry("r1", ChangeOp::Update, 100, 1, "aaa");
|
|
507 |
+ |
let b = entry("r1", ChangeOp::Update, 100, 1, "bbb");
|
|
508 |
+ |
assert_eq!(a.hlc, b.hlc, "the tie is the premise of this test");
|
|
509 |
+ |
|
|
510 |
+ |
let forwards = collapse_max_hlc(vec![a.clone(), b.clone()]);
|
|
511 |
+ |
let backwards = collapse_max_hlc(vec![b, a]);
|
|
512 |
+ |
assert_eq!(
|
|
513 |
+ |
labels(&forwards),
|
|
514 |
+ |
labels(&backwards),
|
|
515 |
+ |
"two devices disagreed at an exact tie because they saw the batch in different orders"
|
|
516 |
+ |
);
|
|
517 |
+ |
assert_eq!(labels(&forwards), ["bbb"], "higher payload bytes win");
|
|
518 |
+ |
}
|
|
519 |
+ |
|
| 495 |
520 |
|
fn note_name(conn: &Connection, id: &str) -> Option<String> {
|
| 496 |
521 |
|
conn.query_row("SELECT name FROM note WHERE id = ?1", [id], |r| r.get(0))
|
| 497 |
522 |
|
.optional()
|