Skip to content

Commit aedf8e3

Browse files
feat: consolidate messageHistory/messageCache/messageSenders into one OrderedTable
1 parent 3cf816d commit aedf8e3

4 files changed

Lines changed: 95 additions & 76 deletions

File tree

sds.nim

Lines changed: 24 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -116,18 +116,12 @@ proc wrapOutgoingMessage*(
116116
)
117117

118118
channel.bloomFilter.add(msg.messageId)
119-
rm.addToHistory(msg.messageId, channelId)
119+
# The full SdsMessage carries senderId and content, so a single
120+
# addToHistory replaces the old triple-write to messageHistory,
121+
# messageCache, and messageSenders.
122+
rm.addToHistory(msg, channelId)
120123

121-
# SDS-R: record sender for future causal-history entries
122-
if rm.participantId.len > 0:
123-
channel.messageSenders[msg.messageId] = rm.participantId
124-
125-
let serialized = serializeMessage(msg)
126-
if serialized.isOk():
127-
# SDS-R: cache serialized bytes so we can serve our own message on repair
128-
if channel.messageCache.len < rm.config.maxMessageHistory:
129-
channel.messageCache[msg.messageId] = serialized.get()
130-
return serialized
124+
return serializeMessage(msg)
131125
except Exception:
132126
error "Failed to wrap message",
133127
channelId = channelId, msg = getCurrentExceptionMsg()
@@ -156,7 +150,7 @@ proc processIncomingBuffer(rm: ReliabilityManager, channelId: SdsChannelID) {.gc
156150
continue
157151

158152
if msgId in channel.incomingBuffer:
159-
rm.addToHistory(msgId, channelId)
153+
rm.addToHistory(channel.incomingBuffer[msgId].message, channelId)
160154
if not rm.onMessageReady.isNil():
161155
rm.onMessageReady(msgId, channelId)
162156
processed.incl(msgId)
@@ -200,35 +194,31 @@ proc unwrapReceivedMessage*(
200194
rm.updateLamportTimestamp(msg.lamportTimestamp, channelId)
201195
rm.reviewAckStatus(msg)
202196

203-
# SDS-R: cache the raw message for potential repair responses
204-
if channel.messageCache.len < rm.config.maxMessageHistory:
205-
channel.messageCache[msg.messageId] = message
206-
207-
# SDS-R: record sender so our future causal-history entries carry it
208-
if msg.senderId.len > 0:
209-
channel.messageSenders[msg.messageId] = msg.senderId
210-
211-
# SDS-R: process incoming repair requests from this message
197+
# SDS-R: process incoming repair requests from this message. We can only
198+
# answer for messages we have actually delivered (i.e. that live in
199+
# messageHistory) — buffered-but-undelivered messages are not in a state
200+
# to confidently rebroadcast.
212201
let now = getTime()
213202
for repairEntry in msg.repairRequest:
214203
# Remove from our own outgoing repair buffer (someone else is also requesting)
215204
channel.outgoingRepairBuffer.del(repairEntry.messageId)
216-
# Check if we can respond to this repair request
217-
if repairEntry.messageId in channel.messageCache and
205+
if repairEntry.messageId in channel.messageHistory and
218206
rm.participantId.len > 0 and repairEntry.senderId.len > 0:
219207
if isInResponseGroup(
220208
rm.participantId, repairEntry.senderId,
221209
repairEntry.messageId, rm.config.numResponseGroups
222210
):
223-
let tResp = computeTResp(
224-
rm.participantId, repairEntry.senderId,
225-
repairEntry.messageId, rm.config.repairTMax
226-
)
227-
channel.incomingRepairBuffer[repairEntry.messageId] = IncomingRepairEntry(
228-
inHistEntry: repairEntry,
229-
cachedMessage: channel.messageCache[repairEntry.messageId],
230-
minTimeRepairResp: now + tResp,
231-
)
211+
let serialized = serializeMessage(channel.messageHistory[repairEntry.messageId])
212+
if serialized.isOk():
213+
let tResp = computeTResp(
214+
rm.participantId, repairEntry.senderId,
215+
repairEntry.messageId, rm.config.repairTMax
216+
)
217+
channel.incomingRepairBuffer[repairEntry.messageId] = IncomingRepairEntry(
218+
inHistEntry: repairEntry,
219+
cachedMessage: serialized.get(),
220+
minTimeRepairResp: now + tResp,
221+
)
232222

233223
var missingDeps = rm.checkDependencies(msg.causalHistory, channelId)
234224

@@ -242,7 +232,7 @@ proc unwrapReceivedMessage*(
242232
channel.incomingBuffer[msg.messageId] =
243233
IncomingMessage.init(message = msg, missingDeps = initHashSet[SdsMessageID]())
244234
else:
245-
rm.addToHistory(msg.messageId, channelId)
235+
rm.addToHistory(msg, channelId)
246236
# Unblock any buffered messages that were waiting on this one.
247237
for pendingId, entry in channel.incomingBuffer:
248238
if msg.messageId in entry.missingDeps:
@@ -435,13 +425,11 @@ proc resetReliabilityManager*(rm: ReliabilityManager): Result[void, ReliabilityE
435425
try:
436426
for channelId, channel in rm.channels:
437427
channel.lamportTimestamp = 0
438-
channel.messageHistory.setLen(0)
428+
channel.messageHistory.clear()
439429
channel.outgoingBuffer.setLen(0)
440430
channel.incomingBuffer.clear()
441431
channel.outgoingRepairBuffer.clear()
442432
channel.incomingRepairBuffer.clear()
443-
channel.messageCache.clear()
444-
channel.messageSenders.clear()
445433
channel.bloomFilter =
446434
RollingBloomFilter.init(rm.config.bloomFilterCapacity, rm.config.bloomFilterErrorRate)
447435
rm.channels.clear()

sds/sds_utils.nim

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,9 @@ proc cleanup*(rm: ReliabilityManager) {.raises: [].} =
2121
for channelId, channel in rm.channels:
2222
channel.outgoingBuffer.setLen(0)
2323
channel.incomingBuffer.clear()
24-
channel.messageHistory.setLen(0)
24+
channel.messageHistory.clear()
2525
channel.outgoingRepairBuffer.clear()
2626
channel.incomingRepairBuffer.clear()
27-
channel.messageCache.clear()
28-
channel.messageSenders.clear()
2927
rm.channels.clear()
3028
except Exception:
3129
error "Error during cleanup", error = getCurrentExceptionMsg()
@@ -42,17 +40,25 @@ proc cleanBloomFilter*(
4240
error = getCurrentExceptionMsg(), channelId = channelId
4341

4442
proc addToHistory*(
45-
rm: ReliabilityManager, msgId: SdsMessageID, channelId: SdsChannelID
43+
rm: ReliabilityManager, msg: SdsMessage, channelId: SdsChannelID
4644
) {.gcsafe, raises: [].} =
45+
## Inserts a delivered message into the channel's history map and evicts the
46+
## eldest entries when the bound is exceeded. The full SdsMessage is kept so
47+
## senderId is available for downstream causal-history population and the
48+
## bytes can be re-serialized on demand to answer SDS-R repair requests.
4749
try:
4850
if channelId in rm.channels:
4951
let channel = rm.channels[channelId]
50-
channel.messageHistory.add(msgId)
51-
if channel.messageHistory.len > rm.config.maxMessageHistory:
52-
channel.messageHistory.delete(0)
52+
channel.messageHistory[msg.messageId] = msg
53+
while channel.messageHistory.len > rm.config.maxMessageHistory:
54+
var firstKey: SdsMessageID
55+
for k in channel.messageHistory.keys:
56+
firstKey = k
57+
break
58+
channel.messageHistory.del(firstKey)
5359
except Exception:
5460
error "Failed to add to history",
55-
channelId = channelId, msgId = msgId, error = getCurrentExceptionMsg()
61+
channelId = channelId, msgId = msg.messageId, error = getCurrentExceptionMsg()
5662

5763
proc updateLamportTimestamp*(
5864
rm: ReliabilityManager, msgTs: int64, channelId: SdsChannelID
@@ -134,14 +140,17 @@ proc getRecentHistoryEntries*(
134140
try:
135141
if channelId in rm.channels:
136142
let channel = rm.channels[channelId]
137-
let recentMessageIds = channel.messageHistory[max(0, channel.messageHistory.len - n) .. ^1]
143+
var orderedIds: seq[SdsMessageID] = @[]
144+
for msgId in channel.messageHistory.keys:
145+
orderedIds.add(msgId)
146+
let recentMessageIds =
147+
orderedIds[max(0, orderedIds.len - n) .. ^1]
138148
var entries: seq[HistoryEntry] = @[]
139149
for msgId in recentMessageIds:
140150
var entry = HistoryEntry(messageId: msgId)
141151
if not rm.onRetrievalHint.isNil():
142152
entry.retrievalHint = rm.onRetrievalHint(msgId)
143-
if msgId in channel.messageSenders:
144-
entry.senderId = channel.messageSenders[msgId]
153+
entry.senderId = channel.messageHistory[msgId].senderId
145154
entries.add(entry)
146155
return entries
147156
else:
@@ -175,7 +184,10 @@ proc getMessageHistory*(
175184
withLock rm.lock:
176185
try:
177186
if channelId in rm.channels:
178-
return rm.channels[channelId].messageHistory
187+
var ids: seq[SdsMessageID] = @[]
188+
for msgId in rm.channels[channelId].messageHistory.keys:
189+
ids.add(msgId)
190+
return ids
179191
else:
180192
return @[]
181193
except Exception:
@@ -246,11 +258,9 @@ proc removeChannel*(
246258
let channel = rm.channels[channelId]
247259
channel.outgoingBuffer.setLen(0)
248260
channel.incomingBuffer.clear()
249-
channel.messageHistory.setLen(0)
261+
channel.messageHistory.clear()
250262
channel.outgoingRepairBuffer.clear()
251263
channel.incomingRepairBuffer.clear()
252-
channel.messageCache.clear()
253-
channel.messageSenders.clear()
254264
rm.channels.del(channelId)
255265
return ok()
256266
except Exception:

sds/types/channel_context.nim

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,36 @@
11
import std/tables
22
import ./sds_message_id
3+
import ./sds_message
34
import ./rolling_bloom_filter
45
import ./unacknowledged_message
56
import ./incoming_message
67
import ./repair_entry
78
export
8-
sds_message_id, rolling_bloom_filter, unacknowledged_message, incoming_message,
9-
repair_entry
9+
sds_message_id, sds_message, rolling_bloom_filter, unacknowledged_message,
10+
incoming_message, repair_entry
1011

1112
type ChannelContext* = ref object
1213
lamportTimestamp*: int64
13-
messageHistory*: seq[SdsMessageID]
14+
messageHistory*: OrderedTable[SdsMessageID, SdsMessage]
15+
## Single source of truth for delivered messages. Holds the deserialized
16+
## SdsMessage (which carries senderId, lamportTimestamp, content, etc.) so
17+
## causal history, sender lookup, and SDS-R repair responses can all be
18+
## answered from one place. OrderedTable preserves insertion order for
19+
## causal-history tail access and FIFO eviction at maxMessageHistory.
1420
bloomFilter*: RollingBloomFilter
1521
outgoingBuffer*: seq[UnacknowledgedMessage]
1622
incomingBuffer*: Table[SdsMessageID, IncomingMessage]
1723
## SDS-R buffers
1824
outgoingRepairBuffer*: Table[SdsMessageID, OutgoingRepairEntry]
1925
incomingRepairBuffer*: Table[SdsMessageID, IncomingRepairEntry]
20-
messageCache*: Table[SdsMessageID, seq[byte]]
21-
## Cached serialized messages for repair responses
22-
messageSenders*: Table[SdsMessageID, SdsParticipantID]
23-
## SDS-R: msgId -> original sender, used to populate causal-history senderId
2426

2527
proc new*(T: type ChannelContext, bloomFilter: RollingBloomFilter): T =
2628
return T(
2729
lamportTimestamp: 0,
28-
messageHistory: @[],
30+
messageHistory: initOrderedTable[SdsMessageID, SdsMessage](),
2931
bloomFilter: bloomFilter,
3032
outgoingBuffer: @[],
3133
incomingBuffer: initTable[SdsMessageID, IncomingMessage](),
3234
outgoingRepairBuffer: initTable[SdsMessageID, OutgoingRepairEntry](),
3335
incomingRepairBuffer: initTable[SdsMessageID, IncomingRepairEntry](),
34-
messageCache: initTable[SdsMessageID, seq[byte]](),
35-
messageSenders: initTable[SdsMessageID, SdsParticipantID](),
3636
)

tests/test_reliability.nim

Lines changed: 36 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1157,7 +1157,7 @@ suite "SDS-R: Repair Buffer Management":
11571157

11581158
let channel = rm.channels[testChannel]
11591159

1160-
# First, cache a message so we can respond to a repair request for it
1160+
# First, seed delivered history so we can respond to a repair request for it
11611161
let cachedMsg = SdsMessage(
11621162
messageId: "cached-msg",
11631163
lamportTimestamp: 1,
@@ -1166,8 +1166,7 @@ suite "SDS-R: Repair Buffer Management":
11661166
content: @[byte(99)],
11671167
bloomFilter: @[],
11681168
)
1169-
let cachedBytes = serializeMessage(cachedMsg).get()
1170-
channel.messageCache["cached-msg"] = cachedBytes
1169+
channel.messageHistory["cached-msg"] = cachedMsg
11711170

11721171
# Receive a message with a repair request for "cached-msg"
11731172
let msgWithRepair = SdsMessage(
@@ -1361,7 +1360,14 @@ suite "SDS-R: Lifecycle and State":
13611360
defer: rm.cleanup()
13621361
check rm.ensureChannel(testChannel).isOk()
13631362
let channel = rm.channels[testChannel]
1364-
channel.messageCache["m-wanted"] = @[byte(99), 99, 99]
1363+
channel.messageHistory["m-wanted"] = SdsMessage(
1364+
messageId: "m-wanted",
1365+
lamportTimestamp: 1,
1366+
causalHistory: @[],
1367+
channelId: testChannel,
1368+
content: @[byte(99), 99, 99],
1369+
bloomFilter: @[],
1370+
)
13651371

13661372
rm.setCallbacks(
13671373
proc(msgId: SdsMessageID, ch: SdsChannelID) {.gcsafe.} = discard,
@@ -1381,19 +1387,21 @@ suite "SDS-R: Lifecycle and State":
13811387
discard rm.unwrapReceivedMessage(serializeMessage(msg).get())
13821388
check "m-wanted" notin channel.incomingRepairBuffer
13831389

1384-
test "wrapOutgoingMessage caches bytes and records sender":
1390+
test "wrapOutgoingMessage records the message in history with our senderId":
13851391
# Proves Bug 1 is fixed — the original sender can serve her own message.
1392+
# In the consolidated history model, the SdsMessage itself carries senderId
1393+
# and can be re-serialized on demand for repair, so a single membership
1394+
# check + senderId read covers both halves of the original assertion.
13861395
let rm = newReliabilityManager(participantId = "alice").get()
13871396
defer: rm.cleanup()
13881397
check rm.ensureChannel(testChannel).isOk()
13891398

13901399
discard rm.wrapOutgoingMessage(@[byte(1), 2, 3], "m1", testChannel)
13911400
let channel = rm.channels[testChannel]
13921401
check:
1393-
"m1" in channel.messageCache
1394-
channel.messageCache["m1"].len > 0
1395-
"m1" in channel.messageSenders
1396-
channel.messageSenders["m1"] == "alice"
1402+
"m1" in channel.messageHistory
1403+
channel.messageHistory["m1"].senderId == "alice"
1404+
channel.messageHistory["m1"].content == @[byte(1), 2, 3]
13971405

13981406
test "getRecentHistoryEntries carries senderId for own messages":
13991407
let rm = newReliabilityManager(participantId = "alice").get()
@@ -1423,17 +1431,23 @@ suite "SDS-R: Lifecycle and State":
14231431
cachedMessage: @[byte(1)],
14241432
minTimeRepairResp: getTime(),
14251433
)
1426-
channel.messageCache["c"] = @[byte(2)]
1427-
channel.messageSenders["c"] = "someone"
1434+
channel.messageHistory["c"] = SdsMessage(
1435+
messageId: "c",
1436+
lamportTimestamp: 1,
1437+
causalHistory: @[],
1438+
channelId: testChannel,
1439+
content: @[byte(2)],
1440+
bloomFilter: @[],
1441+
senderId: "someone",
1442+
)
14281443

14291444
check rm.resetReliabilityManager().isOk()
14301445
check rm.ensureChannel(testChannel).isOk()
14311446
let ch2 = rm.channels[testChannel]
14321447
check:
14331448
ch2.outgoingRepairBuffer.len == 0
14341449
ch2.incomingRepairBuffer.len == 0
1435-
ch2.messageCache.len == 0
1436-
ch2.messageSenders.len == 0
1450+
ch2.messageHistory.len == 0
14371451

14381452
test "SDS-R state is isolated per channel":
14391453
let rm = newReliabilityManager(participantId = "alice").get()
@@ -1475,7 +1489,14 @@ suite "SDS-R: Lifecycle and State":
14751489
)
14761490

14771491
# Carol already has M1 in history and has a pending incomingRepairBuffer entry
1478-
channel.messageHistory.add("m1")
1492+
channel.messageHistory["m1"] = SdsMessage(
1493+
messageId: "m1",
1494+
lamportTimestamp: 1,
1495+
causalHistory: @[],
1496+
channelId: testChannel,
1497+
content: @[byte(1)],
1498+
bloomFilter: @[],
1499+
)
14791500
channel.incomingRepairBuffer["m1"] = IncomingRepairEntry(
14801501
inHistEntry: HistoryEntry(messageId: "m1", senderId: "alice"),
14811502
cachedMessage: @[byte(1)],
@@ -1799,7 +1820,7 @@ suite "SDS-R: Multi-Participant Integration":
17991820
let bob = bus.addPeer("bob", cfg)
18001821
let carol = bus.addPeer("carol", cfg)
18011822

1802-
# Both Bob and Carol receive the original M1 (so both have it in messageCache).
1823+
# Both Bob and Carol receive the original M1 (so both have it in messageHistory).
18031824
bus.broadcast("alice", @[byte(1)], chosenMsg)
18041825

18051826
# Now Dave arrives: build a fake requester message manually so its repair_request

0 commit comments

Comments
 (0)