Compare commits
1 commit
fe500e9e69
...
5da7604fd8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5da7604fd8 |
1 changed files with 0 additions and 139 deletions
|
|
@ -1,139 +0,0 @@
|
||||||
//! Bug-hunt tests for swactor-gossip.
|
|
||||||
//!
|
|
||||||
//! Each test asserts correct protocol behaviour and **fails** when
|
|
||||||
//! the bug is present.
|
|
||||||
|
|
||||||
use swactor::runtime::{Inbox, Runtime, RuntimeConfig};
|
|
||||||
use swactor_gossip::{GossipActor, GossipMessage, GossipQueryResponse, GossipState};
|
|
||||||
|
|
||||||
// ═════════════════════════════════════════════════════════════════════════
|
|
||||||
// Bug 1 — GossipState::merge drops remote value on version ties
|
|
||||||
//
|
|
||||||
// `merge` uses `remote.version > local.version`. When two nodes
|
|
||||||
// independently `set` the same key, both create version 1. Neither
|
|
||||||
// value dominates the other, so the nodes never converge — permanent
|
|
||||||
// split-brain.
|
|
||||||
//
|
|
||||||
// A correct LWW register needs a tiebreaker (e.g. node-id comparison
|
|
||||||
// or a Lamport timestamp) so that one value always wins.
|
|
||||||
// ═════════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
/// Direct GossipState unit test — no runtime needed.
|
|
||||||
#[test]
|
|
||||||
fn merge_equal_version_causes_permanent_divergence() {
|
|
||||||
let mut state_a = GossipState::new();
|
|
||||||
let mut state_b = GossipState::new();
|
|
||||||
|
|
||||||
// Both independently set "x" → both get version 1.
|
|
||||||
state_a.set("x".into(), b"alpha".to_vec());
|
|
||||||
state_b.set("x".into(), b"beta".to_vec());
|
|
||||||
|
|
||||||
assert_eq!(state_a.get("x").unwrap().version, 1);
|
|
||||||
assert_eq!(state_b.get("x").unwrap().version, 1);
|
|
||||||
|
|
||||||
// Simulate a full gossip exchange (both directions).
|
|
||||||
state_a.merge(&state_b);
|
|
||||||
state_b.merge(&state_a);
|
|
||||||
|
|
||||||
// Extra rounds — shouldn't matter, but proves the deadlock is permanent.
|
|
||||||
for _ in 0..10 {
|
|
||||||
state_a.merge(&state_b);
|
|
||||||
state_b.merge(&state_a);
|
|
||||||
}
|
|
||||||
|
|
||||||
let val_a = &state_a.get("x").unwrap().value;
|
|
||||||
let val_b = &state_b.get("x").unwrap().value;
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
val_a, val_b,
|
|
||||||
"BUG: permanent divergence — A holds {:?}, B holds {:?}, \
|
|
||||||
both at version 1. merge() rejects the remote value when \
|
|
||||||
versions are equal, so neither node ever adopts the other's value.",
|
|
||||||
String::from_utf8_lossy(val_a),
|
|
||||||
String::from_utf8_lossy(val_b),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Full actor-level test: two gossip nodes with concurrent writes.
|
|
||||||
#[test]
|
|
||||||
fn concurrent_writes_diverge_through_gossip_actors() {
|
|
||||||
let rt = Runtime::new(RuntimeConfig::default());
|
|
||||||
|
|
||||||
let node_a = rt.spawn(GossipActor::new()).unwrap();
|
|
||||||
let node_b = rt.spawn(GossipActor::new()).unwrap();
|
|
||||||
|
|
||||||
// Wire bidirectional peering.
|
|
||||||
rt.send_to(node_a, GossipMessage::AddPeer(node_b)).unwrap();
|
|
||||||
rt.send_to(node_b, GossipMessage::AddPeer(node_a)).unwrap();
|
|
||||||
for _ in 0..5 {
|
|
||||||
rt.tick();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Both set the same key to different values → version 1 on each.
|
|
||||||
rt.send_to(
|
|
||||||
node_a,
|
|
||||||
GossipMessage::Set {
|
|
||||||
key: "x".into(),
|
|
||||||
value: b"alpha".to_vec(),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
rt.send_to(
|
|
||||||
node_b,
|
|
||||||
GossipMessage::Set {
|
|
||||||
key: "x".into(),
|
|
||||||
value: b"beta".to_vec(),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
for _ in 0..5 {
|
|
||||||
rt.tick();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run many gossip rounds.
|
|
||||||
for _ in 0..50 {
|
|
||||||
rt.send_to(node_a, GossipMessage::DoGossipRound).unwrap();
|
|
||||||
rt.send_to(node_b, GossipMessage::DoGossipRound).unwrap();
|
|
||||||
for _ in 0..10 {
|
|
||||||
rt.tick();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Query both nodes.
|
|
||||||
let inbox_a: Inbox<GossipQueryResponse> = rt.new_inbox().unwrap();
|
|
||||||
let inbox_b: Inbox<GossipQueryResponse> = rt.new_inbox().unwrap();
|
|
||||||
|
|
||||||
rt.send_to(
|
|
||||||
node_a,
|
|
||||||
GossipMessage::Query {
|
|
||||||
key: "x".into(),
|
|
||||||
reply_to: *inbox_a.addr(),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
rt.send_to(
|
|
||||||
node_b,
|
|
||||||
GossipMessage::Query {
|
|
||||||
key: "x".into(),
|
|
||||||
reply_to: *inbox_b.addr(),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
for _ in 0..10 {
|
|
||||||
rt.tick();
|
|
||||||
}
|
|
||||||
|
|
||||||
let resp_a = inbox_a.try_recv().expect("response from A");
|
|
||||||
let resp_b = inbox_b.try_recv().expect("response from B");
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
resp_a.value, resp_b.value,
|
|
||||||
"BUG: permanent split-brain — node A has {:?}, node B has {:?} \
|
|
||||||
(versions {:?} vs {:?}). merge() rejects the remote value on \
|
|
||||||
equal versions, so the two nodes can never agree.",
|
|
||||||
resp_a.value.as_ref().map(|v| String::from_utf8_lossy(v).to_string()),
|
|
||||||
resp_b.value.as_ref().map(|v| String::from_utf8_lossy(v).to_string()),
|
|
||||||
resp_a.version,
|
|
||||||
resp_b.version,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Loading…
Reference in a new issue