@@ -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