Skip to content

Commit 2f9f599

Browse files
committed
Start gossip protocol: vector clocks
1 parent 5534603 commit 2f9f599

5 files changed

Lines changed: 327 additions & 1 deletion

File tree

Cargo.lock

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[workspace]
2-
members = ["api", "client", "common", "server", "xtask"]
2+
members = ["api", "client", "common", "gossip", "server", "xtask"]
33
resolver = "2"
44

55
[workspace.dependencies]

gossip/Cargo.toml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
[package]
2+
name = "sush-gossip"
3+
description = "Oxide Support Shell Gossip Protocol"
4+
version = "0.1.0"
5+
edition = "2024"
6+
publish = false
7+
8+
[dependencies]

gossip/src/clocks.rs

Lines changed: 303 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,303 @@
1+
//! Simple [vector clocks](https://en.wikipedia.org/wiki/Vector_clock)
2+
//! for the sush gossip protocol.
3+
4+
use std::cmp::{Ordering, PartialOrd};
5+
use std::ops::{Index, IndexMut};
6+
7+
/// A node index in a fixed-size message-passing (gossip) network.
8+
///
9+
/// Vector clock owners are responsible for mapping from meaningful
10+
/// identifiers to these indices.
11+
pub type NodeIndex = usize;
12+
13+
/// A Lamport (logical) clock; a counter. Should be impractical to
14+
/// overflow on realistic networks.
15+
type Clock = u64;
16+
17+
/// A list of Lamport (logical) clocks used to impose a partial order
18+
/// over events in a fixed-size message-passing (gossip) network, which
19+
/// in turn induces a notion of causality between messages.
20+
///
21+
/// Each node in the network maintains its own vector clock, and each
22+
/// message is stamped by the sender with a time (a copy of its clock).
23+
/// Whenever the `i`th node sends or receives a message, it increments
24+
/// the `i`th entry of its own clock; on receive, that clock is then
25+
/// joined (in the lattice-theoretic sense, i.e., ∨, _lub_, _sup_) via
26+
/// `max` with the message timestamp.
27+
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
28+
pub struct VectorClock<const N: usize>([Clock; N]);
29+
30+
impl<const N: usize> VectorClock<N> {
31+
pub fn new() -> Self {
32+
Self([0; N])
33+
}
34+
35+
pub fn tick(&mut self, i: NodeIndex) {
36+
self[i] += 1;
37+
}
38+
39+
pub fn join(&mut self, other: &Self) {
40+
for i in 0..N {
41+
self[i] = self[i].max(other[i]);
42+
}
43+
}
44+
45+
pub fn is_comparable_to(&self, other: &Self) -> bool {
46+
self.partial_cmp(other).is_some()
47+
}
48+
}
49+
50+
impl<const N: usize> Index<NodeIndex> for VectorClock<N> {
51+
type Output = Clock;
52+
53+
fn index(&self, index: NodeIndex) -> &Self::Output {
54+
self.0.index(index)
55+
}
56+
}
57+
58+
impl<const N: usize> IndexMut<NodeIndex> for VectorClock<N> {
59+
fn index_mut(&mut self, index: NodeIndex) -> &mut Self::Output {
60+
self.0.index_mut(index)
61+
}
62+
}
63+
64+
/// Latice ordering where `None` denotes incomparable clocks.
65+
impl<const N: usize> PartialOrd for VectorClock<N> {
66+
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
67+
use Ordering::*;
68+
69+
let mut ordering = Equal;
70+
for i in 0..N {
71+
ordering = match (ordering, self[i].cmp(&other[i])) {
72+
(Equal, ordering) => ordering,
73+
(Less | Greater, Equal) => ordering,
74+
(Less, Less) | (Greater, Greater) => ordering,
75+
(Less, Greater) | (Greater, Less) => return None,
76+
};
77+
}
78+
Some(ordering)
79+
}
80+
}
81+
82+
/// An event shared amongst `N` nodes in a causal gossip network.
83+
#[derive(Clone, Debug, Eq, PartialEq)]
84+
pub struct Message<E, const N: usize> {
85+
pub event: E,
86+
pub time: VectorClock<N>,
87+
}
88+
89+
impl<E, const N: usize> Message<E, N> {
90+
pub fn new(event: E, time: VectorClock<N>) -> Self {
91+
Self { event, time }
92+
}
93+
94+
pub fn happened_before(&self, other: &Message<E, N>) -> bool {
95+
self.time < other.time
96+
}
97+
}
98+
99+
pub trait Node<E: Clone, const N: usize> {
100+
fn send(&mut self, event: E) -> Message<E, N>;
101+
fn recv(&mut self, message: Message<E, N>) -> Message<E, N>;
102+
}
103+
104+
#[cfg(test)]
105+
mod test {
106+
use super::*;
107+
108+
#[test]
109+
fn point() {
110+
let t = VectorClock::<0>::new();
111+
assert_eq!(t, t);
112+
assert!(!(t < t));
113+
assert!(!(t > t));
114+
// can't tick a trivial clock
115+
}
116+
117+
#[test]
118+
fn scalar() {
119+
let mut t = VectorClock::<1>::new();
120+
assert_eq!(t, t);
121+
assert!(!(t < t));
122+
assert!(!(t > t));
123+
124+
let t0 = t;
125+
assert_eq!(t, t0);
126+
127+
t.tick(0);
128+
assert_ne!(t, t0);
129+
assert!(t > t0);
130+
131+
let t1 = t;
132+
assert!(t1 > t0);
133+
134+
t.tick(0);
135+
assert_ne!(t, t0);
136+
assert_ne!(t, t1);
137+
assert!(t > t0);
138+
assert!(t > t1);
139+
}
140+
141+
#[test]
142+
fn vector() {
143+
let mut t = VectorClock::<2>::new();
144+
assert_eq!(t, t);
145+
assert!(!(t < t));
146+
assert!(!(t > t));
147+
148+
let t0 = t;
149+
t.tick(0);
150+
assert_ne!(t, t0);
151+
assert!(t > t0);
152+
153+
let t1 = t;
154+
assert!(t1 > t0);
155+
t.tick(1);
156+
assert_ne!(t, t0);
157+
assert_ne!(t, t1);
158+
assert!(t > t0);
159+
assert!(t > t1);
160+
}
161+
162+
/// A labeled event.
163+
type TestEvent = &'static str;
164+
165+
/// A node in a manual message-passing network.
166+
#[derive(Debug, Eq, PartialEq)]
167+
struct TestNode<const N: usize> {
168+
me: NodeIndex,
169+
now: VectorClock<N>,
170+
log: Vec<Message<TestEvent, N>>,
171+
}
172+
173+
impl<const N: usize> TestNode<N> {
174+
fn new(myself: NodeIndex) -> Self {
175+
Self {
176+
me: myself,
177+
now: VectorClock::new(),
178+
log: vec![],
179+
}
180+
}
181+
}
182+
183+
/// Maintain vector clocks and log each message.
184+
impl<const N: usize> Node<TestEvent, N> for TestNode<{ N }> {
185+
fn send(&mut self, event: TestEvent) -> Message<TestEvent, N> {
186+
self.now.tick(self.me);
187+
188+
let message = Message::new(event, self.now);
189+
self.log.push(message.clone());
190+
message
191+
}
192+
193+
fn recv(&mut self, message: Message<TestEvent, N>) -> Message<TestEvent, N> {
194+
self.now.tick(self.me);
195+
196+
let Message { event, time } = message;
197+
self.now.join(&time);
198+
let message = Message::new(event, self.now);
199+
self.log.push(message.clone());
200+
message
201+
}
202+
}
203+
204+
/// Example from [Why Logical Clocks are Easy](https://queue.acm.org/detail.cfm?id=2917756)
205+
/// by Carlos Baquero and Nuno Preguiça, figure 3:
206+
/// ```
207+
/// ――― time ――→
208+
/// [1,0,0] [2,0,0] [3,0,0]
209+
/// node A a1 ――→ a2 ――→ a3
210+
/// ⭨
211+
/// [0,1,0] [2,2,0] [2,3,0]
212+
/// node B b1 ――→ b2 ――→ b3
213+
/// ⭨
214+
/// [0,0,1] [0,0,2] [2,3,3]
215+
/// node C c1 ――→ c2 ――→ c3
216+
/// ```
217+
#[test]
218+
fn baquero_preguiça_figure_3() {
219+
let mut a = TestNode::<3>::new(0);
220+
let mut b = TestNode::<3>::new(1);
221+
let mut c = TestNode::<3>::new(2);
222+
assert_eq!(a.now.0, [0, 0, 0], "clocks should start at 0");
223+
assert_eq!(a.now, b.now, "clocks should be equal at start");
224+
assert_eq!(b.now, c.now, "clocks should be equal at start");
225+
226+
// A
227+
let a1 = a.send("a1");
228+
assert!(!a1.happened_before(&a1));
229+
230+
let a2 = a.send("a2");
231+
assert!(a1.happened_before(&a2));
232+
233+
let a3 = a.send("a3");
234+
assert!(a1.happened_before(&a3));
235+
assert!(a2.happened_before(&a3));
236+
237+
// B
238+
let b1 = b.send("b1");
239+
assert!(!b1.happened_before(&a1));
240+
assert!(!b1.happened_before(&a2));
241+
assert!(!b1.happened_before(&a3));
242+
243+
b.recv(a2.clone());
244+
let b2 = b.send("b2");
245+
assert!(b1.happened_before(&b2));
246+
assert!(a2.happened_before(&b2));
247+
assert!(!b2.happened_before(&a1));
248+
assert!(!b2.happened_before(&a2));
249+
assert!(!b2.happened_before(&a3));
250+
251+
// C
252+
let c1 = c.send("c1");
253+
assert!(!c1.happened_before(&a1));
254+
assert!(!c1.happened_before(&a2));
255+
assert!(!c1.happened_before(&a3));
256+
assert!(!c1.happened_before(&b1));
257+
assert!(!c1.happened_before(&b2));
258+
259+
let c2 = c.send("c2");
260+
assert!(c1.happened_before(&c2));
261+
assert!(!c2.happened_before(&a1));
262+
assert!(!c2.happened_before(&a2));
263+
assert!(!c2.happened_before(&a3));
264+
assert!(!c2.happened_before(&b1));
265+
assert!(!c2.happened_before(&b2));
266+
assert!(!c2.happened_before(&b2));
267+
c.recv(b2.clone());
268+
269+
// Logs
270+
macro_rules! msg {
271+
($e:expr, $t:expr) => {
272+
Message {
273+
event: $e,
274+
time: VectorClock($t),
275+
}
276+
};
277+
}
278+
assert_eq!(
279+
a.log,
280+
vec![
281+
msg!("a1", [1, 0, 0]),
282+
msg!("a2", [2, 0, 0]),
283+
msg!("a3", [3, 0, 0]),
284+
]
285+
);
286+
assert_eq!(
287+
b.log,
288+
vec![
289+
msg!("b1", [0, 1, 0]),
290+
msg!("a2", [2, 2, 0]),
291+
msg!("b2", [2, 3, 0]),
292+
]
293+
);
294+
assert_eq!(
295+
c.log,
296+
vec![
297+
msg!("c1", [0, 0, 1]),
298+
msg!("c2", [0, 0, 2]),
299+
msg!("b2", [2, 3, 3]),
300+
]
301+
);
302+
}
303+
}

gossip/src/lib.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
//! Oxide Support Shell Gossip Protocol.
2+
//!
3+
//! The gossip protocol is used only between `sush` servers,
4+
//! and is not exposed externally (to the customer). It allows
5+
//! distribution of jobs across the rack and enforces job order.
6+
//!
7+
//! The ordering constraint is complicated by the fact that the
8+
//! rack may not know what time it is; therefore, we must impose
9+
//! a _logical time_, using [vector clocks](https://en.wikipedia.org/wiki/Vector_clock).
10+
11+
pub mod clocks;

0 commit comments

Comments
 (0)