From 466c8e042b140078d9c800ef420d99bf9f7cb7f8 Mon Sep 17 00:00:00 2001 From: Norbert Hartl Date: Wed, 22 Jul 2026 12:27:38 +0200 Subject: [PATCH] Add tombstone GC for GossipListValue MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tombstones (removed items) were never physically dropped from the entries dict, only ever marked present: false — fine for correctness (each GossipListValue is always replaced as a whole atomic unit via the enclosing GossipApplicationState's version, so there's no per-item merge/resurrection risk the way Cassandra's cell-level tombstones have), but unbounded over a long-running process with many distinct items added then removed. gcTombstonesOlderThan: drops any tombstoned entry whose itemVersion is more than the given gap behind the list's own versionCounter. Uses the list's own logical version clock rather than wall-clock time, since nothing in Gossip carries real timestamps. Deliberately left as an explicit method the caller invokes with its own threshold, not auto-wired into Gossiper's periodic gossip loop — picking a concrete grace window is a policy call that depends on gossip round frequency, not something to default silently. New GossipListValueTest (5 tests) covers add/remove/tombstone semantics and the GC boundary (kept when recent, removed when past the gap, present items never touched). Co-Authored-By: Claude Sonnet 5 --- .../Gossip-Tests/GossipListValueTest.class.st | 61 +++++++++++++++++++ source/Gossip/GossipListValue.class.st | 6 ++ 2 files changed, 67 insertions(+) create mode 100644 source/Gossip-Tests/GossipListValueTest.class.st diff --git a/source/Gossip-Tests/GossipListValueTest.class.st b/source/Gossip-Tests/GossipListValueTest.class.st new file mode 100644 index 0000000..4a47012 --- /dev/null +++ b/source/Gossip-Tests/GossipListValueTest.class.st @@ -0,0 +1,61 @@ +Class { + #name : 'GossipListValueTest', + #superclass : 'TestCase', + #category : 'Gossip-Tests', + #package : 'Gossip-Tests' +} + +{ #category : 'as yet unclassified' } +GossipListValueTest >> testAddMakesItemPresent [ + | lv | + lv := GossipListValue new. + lv add: #a. + self assert: (lv includes: #a). + self assert: lv presentItems asArray equals: #(#a) +] + +{ #category : 'as yet unclassified' } +GossipListValueTest >> testGcTombstonesOlderThanKeepsRecentTombstones [ + | lv | + lv := GossipListValue new. + lv add: #a. + lv remove: #a. + lv add: #b. + lv gcTombstonesOlderThan: 5. + self assert: (lv entries includesKey: #a) +] + +{ #category : 'as yet unclassified' } +GossipListValueTest >> testGcTombstonesOlderThanNeverRemovesPresentItems [ + | lv | + lv := GossipListValue new. + lv add: #a. + lv add: #b. + lv add: #c. + lv gcTombstonesOlderThan: 0. + self assert: (lv entries includesKey: #a). + self assert: lv presentItems asSortedCollection asArray equals: #(#a #b #c) +] + +{ #category : 'as yet unclassified' } +GossipListValueTest >> testGcTombstonesOlderThanRemovesOldTombstones [ + | lv | + lv := GossipListValue new. + lv add: #a. + lv remove: #a. + lv add: #b. + lv add: #c. + lv gcTombstonesOlderThan: 1. + self deny: (lv entries includesKey: #a) +] + +{ #category : 'as yet unclassified' } +GossipListValueTest >> testRemoveTombstonesRatherThanDeletes [ + | lv | + lv := GossipListValue new. + lv add: #a. + lv remove: #a. + self deny: (lv includes: #a). + self assert: lv presentItems isEmpty. + self assert: (lv entries includesKey: #a) +] diff --git a/source/Gossip/GossipListValue.class.st b/source/Gossip/GossipListValue.class.st index 8ac0017..f17ca05 100644 --- a/source/Gossip/GossipListValue.class.st +++ b/source/Gossip/GossipListValue.class.st @@ -19,6 +19,12 @@ GossipListValue >> entries [ ^ entries ] +{ #category : 'as yet unclassified' } +GossipListValue >> gcTombstonesOlderThan: aVersionGap [ + entries := entries reject: [ :pair | + pair first not and: [ (versionCounter - pair second) > aVersionGap ] ] +] + { #category : 'not defined protocol' } GossipListValue >> includes: anItem [ ^ (entries at: anItem ifAbsent: [ ^ false ]) first