max / synckit
- Co-Authored-By
- Claude Opus 5 (1M context) <noreply@anthropic.com>
1 file changed,
+49 insertions,
-3 deletions
| @@ -79,7 +79,7 @@ | |||
| 79 | 79 | /// conflict. Mint a real clock with [`Hlc::tick`]/[`Hlc::observe`]; the only | |
| 80 | 80 | /// sanctioned nil-node clock is the explicit legacy floor ([`hlc_legacy_floor`]), | |
| 81 | 81 | /// used solely for pre-HLC entries that arrive with no embedded clock. | |
| 82 | - | #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] | |
| 82 | + | #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] | |
| 83 | 83 | pub struct Hlc { | |
| 84 | 84 | /// Wall-clock component, milliseconds since the Unix epoch. | |
| 85 | 85 | pub wall_ms: i64, | |
| @@ -90,6 +90,29 @@ | |||
| 90 | 90 | pub node: DeviceId, | |
| 91 | 91 | } | |
| 92 | 92 | ||
| 93 | + | /// The ordering is written out rather than derived, because it is the rule that | |
| 94 | + | /// decides which of two changes for a row wins, and every device has to compute | |
| 95 | + | /// it identically. A derived `Ord` would encode that rule in the declaration | |
| 96 | + | /// order of the fields, where reordering them for readability would silently | |
| 97 | + | /// change which change survives a sync. Spelling it out costs nothing and pins | |
| 98 | + | /// it: wall clock first, then the within-millisecond counter, then the minting | |
| 99 | + | /// device, which is what makes the value globally unique and the comparison | |
| 100 | + | /// total. | |
| 101 | + | impl Ord for Hlc { | |
| 102 | + | fn cmp(&self, other: &Self) -> std::cmp::Ordering { | |
| 103 | + | self.wall_ms | |
| 104 | + | .cmp(&other.wall_ms) | |
| 105 | + | .then_with(|| self.counter.cmp(&other.counter)) | |
| 106 | + | .then_with(|| self.node.as_uuid().cmp(&other.node.as_uuid())) | |
| 107 | + | } | |
| 108 | + | } | |
| 109 | + | ||
| 110 | + | impl PartialOrd for Hlc { | |
| 111 | + | fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> { | |
| 112 | + | Some(self.cmp(other)) | |
| 113 | + | } | |
| 114 | + | } | |
| 115 | + | ||
| 93 | 116 | impl Hlc { | |
| 94 | 117 | /// The zero HLC for `node`. Used as the initial clock state and as the floor | |
| 95 | 118 | /// for legacy entries that predate HLC support. | |
| @@ -770,8 +793,8 @@ | |||
| 770 | 793 | }) | |
| 771 | 794 | } | |
| 772 | 795 | ||
| 773 | - | /// The parts that encode causality. The derived `Ord` also breaks ties | |
| 774 | - | /// on `node`, which is a convergence device rather than a clock reading, | |
| 796 | + | /// The parts that encode causality. `Hlc`'s `Ord` also breaks ties on | |
| 797 | + | /// `node`, which is a convergence device rather than a clock reading, | |
| 775 | 798 | /// so a monotonicity claim is about this pair. | |
| 776 | 799 | fn reading(h: &Hlc) -> (i64, u32) { | |
| 777 | 800 | (h.wall_ms, h.counter) | |
| @@ -854,6 +877,29 @@ | |||
| 854 | 877 | } | |
| 855 | 878 | } | |
| 856 | 879 | ||
| 880 | + | /// Build an HLC from its three components, shortest form for the ordering | |
| 881 | + | /// tests below. | |
| 882 | + | fn hlc(wall_ms: i64, counter: u32, node: u8) -> Hlc { | |
| 883 | + | let mut bytes = [0u8; 16]; | |
| 884 | + | bytes[15] = node; | |
| 885 | + | Hlc { | |
| 886 | + | wall_ms, | |
| 887 | + | counter, | |
| 888 | + | node: DeviceId::new(Uuid::from_bytes(bytes)), | |
| 889 | + | } | |
| 890 | + | } | |
| 891 | + | ||
| 892 | + | /// The nil node is the legacy floor, and the floor has to lose. A pre-HLC | |
| 893 | + | /// entry deserializes onto it, so if it ever won a comparison a legacy | |
| 894 | + | /// entry would overwrite a real edit. | |
| 895 | + | #[test] | |
| 896 | + | fn legacy_floor_loses_to_every_real_clock() { | |
| 897 | + | let floor = hlc_legacy_floor(); | |
| 898 | + | assert!(floor < hlc(0, 0, 1)); | |
| 899 | + | assert!(floor < hlc(0, 1, 0)); | |
| 900 | + | assert!(floor < hlc(1, 0, 0)); | |
| 901 | + | } | |
| 902 | + | ||
| 857 | 903 | #[test] | |
| 858 | 904 | fn change_op_serde_roundtrip() { | |
| 859 | 905 | for (variant, expected_str) in [ |