Skip to content

Commit 79994c7

Browse files
Fulmicoton/bug corner case liveness (#180)
* Adjusts a unit test following a change in behavior. Adjusts the #178 regression test to use a scenario where the sender's gc (120) strictly exceeds the receiver's max (70), which is the condition that actually triggers a reset in practice. * Chitchat version bump
1 parent 6ba3313 commit 79994c7

3 files changed

Lines changed: 55 additions & 20 deletions

File tree

chitchat-test/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ rust-version = "1.86"
77
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
88

99
[dependencies]
10-
chitchat = { version = "0.10.0", path = "../chitchat" }
10+
chitchat = { version = "0.10.1", path = "../chitchat" }
1111
poem = "3.1.11"
1212
poem-openapi = {version="5.1.15", features = ["swagger-ui"] }
1313
structopt = "0.3"

chitchat/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "chitchat"
3-
version = "0.10.0"
3+
version = "0.10.1"
44
edition = "2024"
55
license = "MIT"
66
authors = ["Quickwit, Inc. <hello@quickwit.io>"]

chitchat/src/state.rs

Lines changed: 53 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ impl NodeState {
158158
let compatible_without_reset =
159159
// Our last GC epoch happened after the delta's peer's
160160
node_delta.last_gc_version <= self.last_gc_version ||
161-
// We know about all of the operations that happened before this GC.
161+
// We know about all of the operations that happened up to this GC.
162162
node_delta.last_gc_version <= self.max_version();
163163

164164
if !compatible_without_reset {
@@ -1761,32 +1761,30 @@ mod tests {
17611761

17621762
// Regression test for https://github.com/quickwit-oss/chitchat/issues/178
17631763
//
1764-
// With the old monotonic property (max(max_version, last_gc_version), max_version),
1765-
// a reset delta with last_gc_version=100 and max_version=80 applied to a node
1766-
// with max_version=100, last_gc_version=50 would cause:
1767-
// before: (100, 100), after: (100, 80) => assertion failure
1764+
// A reset delta where last_gc_version > receiver's max_version and
1765+
// max_version < receiver's max_version (e.g. due to MTU truncation).
17681766
//
1769-
// The fix: monotonic_property is now (last_gc_version, max_version). Since
1770-
// last_gc_version strictly increases on reset, max_version can safely regress
1771-
// (e.g. due to MTU truncation) without violating the invariant.
1767+
// Receiver: gc=50, max=70. Sender: gc=120, max=80 (truncated by MTU).
1768+
// The sender's gc > receiver's max, so this forces a reset. After reset,
1769+
// monotonic_property goes from (50, 70) to (120, 80) which is >= because
1770+
// the first element (last_gc_version) strictly increased.
17721771
#[test]
17731772
fn test_apply_delta_reset_does_not_violate_monotonic_property() {
17741773
let mut node_state = NodeState::for_test();
17751774
node_state.set_with_version("key_a", "val_a", 50);
1776-
node_state.set_with_version("key_b", "val_b", 100);
1775+
node_state.set_with_version("key_b", "val_b", 70);
17771776
node_state.last_gc_version = 50;
17781777

1779-
assert_eq!(node_state.max_version(), 100);
1778+
assert_eq!(node_state.max_version(), 70);
17801779
assert_eq!(node_state.last_gc_version, 50);
17811780
let monotonic_before = node_state.monotonic_property();
17821781

1783-
// A reset delta where last_gc_version > our last_gc_version but
1784-
// max_version < our max_version (e.g. due to MTU truncation or
1785-
// the sender having a post-reset state with gc > max).
1782+
// A reset delta where last_gc_version > our max_version.
1783+
// The sender GC'd past our max, so we can't apply without reset.
17861784
let node_delta = NodeDelta {
17871785
chitchat_id: node_state.chitchat_id.clone(),
17881786
from_version_excluded: 0,
1789-
last_gc_version: 100,
1787+
last_gc_version: 120,
17901788
max_version: 80,
17911789
key_values: vec![KeyValueMutation {
17921790
key: "key_c".to_string(),
@@ -1798,22 +1796,59 @@ mod tests {
17981796

17991797
let delta_status = node_state.apply_delta(node_delta, Instant::now());
18001798

1801-
// The reset is accepted: last_gc_version advances from 50 to 100,
1802-
// so monotonic_property goes from (50, 100) to (100, 80) which is >=.
18031799
assert_eq!(delta_status, DeltaStatus::ApplyAfterReset);
18041800
let monotonic_after = node_state.monotonic_property();
1805-
assert_eq!(monotonic_after, (100, 80));
1801+
assert_eq!(monotonic_after, (120, 80));
18061802
assert!(monotonic_after >= monotonic_before);
18071803

18081804
assert_eq!(node_state.max_version(), 80);
1809-
assert_eq!(node_state.last_gc_version, 100);
1805+
assert_eq!(node_state.last_gc_version, 120);
18101806
// Old keys were wiped by the reset.
18111807
assert!(node_state.get("key_a").is_none());
18121808
assert!(node_state.get("key_b").is_none());
18131809
// New key from the delta was applied.
18141810
assert_eq!(node_state.get("key_c").unwrap(), "val_c");
18151811
}
18161812

1813+
// Similar to test_cluster_state_apply_delta_last_gc_equal_max_does_not_reset,
1814+
// but exercises apply_delta at the NodeState level directly.
1815+
//
1816+
// When A has gc=50, max=70 and B has gc=70, max=100, B's gc equals A's max.
1817+
// B decides no reset is needed (digest_max_version < last_gc_version is 70 < 70 = false).
1818+
// A must accept this non-reset delta because it has seen every version up to
1819+
// the GC boundary. Without the fix (< instead of <=), A would reject the delta
1820+
// permanently — neither side would change state, causing a liveness bug.
1821+
#[test]
1822+
fn test_delta_accepted_when_sender_gc_equals_receiver_max() {
1823+
let node_id = ChitchatId::for_local_test(10_001);
1824+
1825+
// Node A (receiver): gc=50, max=70
1826+
let mut node_state_a = NodeState::for_test();
1827+
node_state_a.set_with_version("key_a", "val_a", 70);
1828+
node_state_a.last_gc_version = 50;
1829+
assert_eq!(node_state_a.max_version(), 70);
1830+
1831+
// Build the delta that B would send: gc=70, from_version=70, max=100.
1832+
// B decided no reset was needed, so from_version_excluded = A's max = 70.
1833+
let node_delta = NodeDelta {
1834+
chitchat_id: node_id,
1835+
from_version_excluded: 70,
1836+
last_gc_version: 70,
1837+
max_version: 100,
1838+
key_values: vec![KeyValueMutation {
1839+
key: "key_b".to_string(),
1840+
value: "val_b".to_string(),
1841+
version: 100,
1842+
status: DeletionStatusMutation::Set,
1843+
}],
1844+
};
1845+
1846+
let delta_status = node_state_a.apply_delta(node_delta, Instant::now());
1847+
assert_eq!(delta_status, DeltaStatus::Apply);
1848+
assert_eq!(node_state_a.max_version(), 100);
1849+
assert_eq!(node_state_a.get("key_b").unwrap(), "val_b");
1850+
}
1851+
18171852
#[test]
18181853
fn test_cluster_state_apply_delta_last_gc_equal_max_does_not_reset() {
18191854
let mut cluster_state = ClusterState::default();

0 commit comments

Comments
 (0)