fix: stop emitting receive events after channel_close (#4075)

This commit is contained in:
Darshan 2026-07-30 01:05:53 +05:30 committed by GitHub
parent 8125cd0262
commit ed8e881c1d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 168 additions and 7 deletions

View File

@ -78,6 +78,10 @@ type
channelReqs: ChannelReqs
brokerCtx: BrokerContext
receivedListener: MessageReceivedEventListener
sentListener: MessageSentEventListener
errorListener: MessageErrorEventListener
closed: bool
func init(
T: type ChannelReqState,
@ -102,7 +106,12 @@ func getSenderId*(self: ReliableChannel): SdsParticipantID {.inline.} =
self.senderId
proc stop*(self: ReliableChannel) {.async: (raises: []).} =
## Stops the SDS background loops. Persisted SDS state survives.
## Drops the event listeners and stops the SDS loops. Persisted SDS state survives.
## `closed` gates any in-flight receive handler that survives the listener drop.
self.closed = true
await MessageReceivedEvent.dropListener(self.brokerCtx, self.receivedListener)
await MessageSentEvent.dropListener(self.brokerCtx, self.sentListener)
await MessageErrorEvent.dropListener(self.brokerCtx, self.errorListener)
await self.sdsHandler.stop()
proc tryFinalizeChannelReq(self: ReliableChannel, channelReqId: RequestId) =
@ -248,6 +257,8 @@ proc send*(
proc reportReceived(self: ReliableChannel, deliverable: SdsDeliverable) =
## Tail of the ingress pipeline (reassemble -> emit).
if self.closed:
return
let reassembled = self.segmentation.handleIncomingSegment(deliverable.content)
if reassembled.isSome():
## Emit on the captured `brokerCtx` (the manager's), so the
@ -292,6 +303,8 @@ proc onMessageReceived(
## Invoked from this channel's `MessageReceivedEvent` listener, which
## already filtered on the spec marker and on `contentTopic`. The
## channel only sees the raw payload bytes for itself.
if self.closed:
return
## Notice that the following "request" is implemented implicitly as a broker call to
## the `Decrypt` request broker.
@ -361,7 +374,7 @@ proc new*(
## Keeping the listeners (and the handler procs they call) inside the
## channel lets `onMessageReceived` / `onMessageFinal` stay private —
## the manager doesn't need to know about them.
discard MessageReceivedEvent.listen(
chn.receivedListener = MessageReceivedEvent.listen(
chn.brokerCtx,
proc(evt: MessageReceivedEvent): Future[void] {.async: (raises: []).} =
## Drop foreign traffic (non-Reliable-Channel `meta`) and traffic
@ -372,22 +385,28 @@ proc new*(
return
await chn.onMessageReceived(evt.messageHash, evt.message.payload)
,
)
).valueOr:
error "MessageReceivedEvent.listen failed", channelId = channelId, error = error
MessageReceivedEventListener()
## Send-completion events are tagged with the per-segment messaging
## `requestId` — globally unique, so we don't need any channel filter
## up front. The handler scans this channel's pending entries for a
## match and is a no-op when the id belongs to a different channel.
discard MessageSentEvent.listen(
chn.sentListener = MessageSentEvent.listen(
chn.brokerCtx,
proc(evt: MessageSentEvent): Future[void] {.async: (raises: []).} =
chn.onMessageFinal(evt.requestId, MessagingOutcome.Sent),
)
).valueOr:
error "MessageSentEvent.listen failed", channelId = channelId, error = error
MessageSentEventListener()
discard MessageErrorEvent.listen(
chn.errorListener = MessageErrorEvent.listen(
chn.brokerCtx,
proc(evt: MessageErrorEvent): Future[void] {.async: (raises: []).} =
chn.onMessageFinal(evt.requestId, MessagingOutcome.Failed),
)
).valueOr:
error "MessageErrorEvent.listen failed", channelId = channelId, error = error
MessageErrorEventListener()
return chn

View File

@ -166,6 +166,148 @@ suite "Reliable Channel - ingress":
(await waku.stop()).expect("stop")
asyncTest "closed channel emits no channel event":
## Issue #4065: no ChannelMessageReceivedEvent after closeChannel.
const
channelId = ChannelId("test-channel-3")
contentTopic = ContentTopic("/reliable-channel/test/proto")
let appPayload = "after close".toBytes()
var waku: LogosDelivery
var manager: ReliableChannelManager
var brokerCtx: BrokerContext
lockNewGlobalBrokerContext:
brokerCtx = globalBrokerContext()
waku = (await LogosDelivery.new(createApiNodeConf())).expect("LogosDelivery.new")
manager = waku.reliableChannelManager
setNoopEncryption()
discard manager
.createReliableChannel(channelId, contentTopic, SdsParticipantID("local"))
.expect("createReliableChannel")
(await manager.closeChannel(channelId)).expect("closeChannel")
var fired = false
discard ChannelMessageReceivedEvent
.listen(
brokerCtx,
proc(evt: ChannelMessageReceivedEvent) {.async: (raises: []).} =
if evt.channelId == channelId:
fired = true
,
)
.expect("listen ChannelMessageReceivedEvent")
let remotePeer =
ReliabilityManager.new(SdsParticipantID("remote"), ReliabilityConfig.init())
let sdsWire = (
await remotePeer.wrapOutgoingMessage(
appPayload, "close-test-msg-1", SdsChannelID(channelId)
)
).expect("wrapOutgoingMessage")
let inboundMsg = WakuMessage(
payload: sdsWire,
contentTopic: contentTopic,
version: 0,
meta: LipWireReliableChannelVersion.toBytes(),
)
waku_message_events.MessageReceivedEvent.emit(
brokerCtx,
waku_message_events.MessageReceivedEvent(messageHash: "", message: inboundMsg),
)
await sleepAsync(100.milliseconds)
check not fired
(await waku.stop()).expect("stop")
asyncTest "in-flight receive does not emit after close":
## A message already inside the ingress pipeline when closeChannel
## runs must not emit after close returns.
const
channelId = ChannelId("test-channel-4")
contentTopic = ContentTopic("/reliable-channel/test/proto")
let appPayload = "in flight".toBytes()
var waku: LogosDelivery
var manager: ReliableChannelManager
var brokerCtx: BrokerContext
lockNewGlobalBrokerContext:
brokerCtx = globalBrokerContext()
waku = (await LogosDelivery.new(createApiNodeConf())).expect("LogosDelivery.new")
manager = waku.reliableChannelManager
setNoopEncryption()
## Gate decryption so the receive handler parks mid-pipeline.
let decryptGate = newFuture[void]("decrypt-gate")
Decrypt
.replaceProvider(
DefaultBrokerContext,
proc(payload: seq[byte]): Future[Result[Decrypt, string]] {.async.} =
await decryptGate
return ok(Decrypt(payload)),
)
.expect("replaceProvider")
discard manager
.createReliableChannel(channelId, contentTopic, SdsParticipantID("local"))
.expect("createReliableChannel")
var fired = false
discard ChannelMessageReceivedEvent
.listen(
brokerCtx,
proc(evt: ChannelMessageReceivedEvent) {.async: (raises: []).} =
if evt.channelId == channelId:
fired = true
,
)
.expect("listen ChannelMessageReceivedEvent")
let remotePeer =
ReliabilityManager.new(SdsParticipantID("remote"), ReliabilityConfig.init())
let sdsWire = (
await remotePeer.wrapOutgoingMessage(
appPayload, "inflight-test-msg-1", SdsChannelID(channelId)
)
).expect("wrapOutgoingMessage")
let inboundMsg = WakuMessage(
payload: sdsWire,
contentTopic: contentTopic,
version: 0,
meta: LipWireReliableChannelVersion.toBytes(),
)
waku_message_events.MessageReceivedEvent.emit(
brokerCtx,
waku_message_events.MessageReceivedEvent(messageHash: "", message: inboundMsg),
)
## Let the handler reach the decrypt await, then close mid-flight.
await sleepAsync(50.milliseconds)
(await manager.closeChannel(channelId)).expect("closeChannel")
decryptGate.complete()
await sleepAsync(100.milliseconds)
check not fired
## Restore pass-through decryption for the remaining tests.
Decrypt
.replaceProvider(
DefaultBrokerContext,
proc(payload: seq[byte]): Future[Result[Decrypt, string]] {.async.} =
return ok(Decrypt(payload)),
)
.expect("replaceProvider restore")
(await waku.stop()).expect("stop")
suite "Reliable Channel - send state machine":
asyncTest "MessageSentEvent finalises the channelReqId as Sent":
## Drives the real send pipeline (`send` -> segmentation -> SDS ->