55# # so each thread's machinery is readable on its own.
66# #
77# # Responsibilities:
8- # # - Drain queued events into listener callbacks (queue producer lands in PR #69) .
8+ # # - Drain queued events into listener callbacks.
99# # - Watch `ctx.ffiHeartbeat` and emit `NotRespondingEvent` / `RespondingEvent`
1010# # on FFI-thread stall and recovery transitions.
1111
@@ -37,14 +37,13 @@ proc dispatchToListeners[T](
3737 )
3838
3939proc emitLivenessEvent [T, P](ctx: ptr FFIContext [T], name: string , payload: P) =
40- # # Encodes a zero-field liveness event (`NotRespondingEvent`,
41- # # `RespondingEvent`) and dispatches it directly to listeners, bypassing
42- # # the event queue (which may itself be wedged). Runs on the event thread.
40+ # # Encodes a liveness event and dispatches directly to listeners (bypassing
41+ # # the queue, which may be wedged). Runs on the event thread.
4342 let event =
4443 try :
4544 EventEnvelope [P](eventType: name, payload: payload).cborEncode ()
46- except CatchableError as exc :
47- chronicles.error " liveness event encode failed" , name = name, err = exc .msg
45+ except CatchableError as e :
46+ chronicles.error " liveness event encode failed" , name = name, err = e .msg
4847 return
4948 let dataPtr: pointer =
5049 if event.len > 0 : cast [pointer ](unsafeAddr event[0 ])
@@ -55,18 +54,14 @@ proc onNotResponding*(ctx: ptr FFIContext) =
5554 emitLivenessEvent (ctx, NotRespondingEventName , NotRespondingEvent ())
5655
5756proc onResponding * (ctx: ptr FFIContext ) =
58- # # Fired once when the FFI thread's heartbeat starts advancing again
59- # # after a `NotRespondingEvent`. Lets consumers clear any "library
60- # # hung" UI state without polling.
57+ # # Fired once when the heartbeat resumes after a NotRespondingEvent.
58+ # # Lets consumers clear any "library hung" UI state without polling.
6159 emitLivenessEvent (ctx, RespondingEventName , RespondingEvent ())
6260
6361proc dispatchQueuedEvent [T](ctx: ptr FFIContext [T], qe: QueuedEvent ) =
6462 # # Frees `qe`'s c_malloc buffers on exit.
6563 defer :
66- if not qe.name.isNil ():
67- c_free (cast [pointer ](qe.name))
68- if not qe.data.isNil ():
69- c_free (qe.data)
64+ freeEventBuffers (qe.name, qe.data)
7065 ctx.dispatchToListeners ($ qe.name, qe.data, qe.dataLen)
7166
7267proc drainEventQueue [T](ctx: ptr FFIContext [T]) =
@@ -92,10 +87,8 @@ proc init(T: type HeartbeatMonitor, ctx: ptr FFIContext): T =
9287 )
9388
9489proc check [T](hb: var HeartbeatMonitor , ctx: ptr FFIContext [T]) =
95- # # Fires `onNotResponding` once the FFI thread's heartbeat counter stops
96- # # advancing past the stale threshold, and fires `onResponding` once it
97- # # starts advancing again. Both transitions latch so each is emitted at
98- # # most once per stall episode.
90+ # # Fires onNotResponding / onResponding on heartbeat stall / recovery.
91+ # # Both transitions latch — each fires at most once per stall episode.
9992 if Moment .now () - hb.startedAt <= FFIHeartbeatStartDelay :
10093 return
10194 let cur = ctx.ffiHeartbeat.load ()
@@ -112,14 +105,19 @@ proc check[T](hb: var HeartbeatMonitor, ctx: ptr FFIContext[T]) =
112105
113106proc eventRun [T](ctx: ptr FFIContext [T]) {.async .} =
114107 var hb = HeartbeatMonitor .init (ctx)
108+ var notifiedStuck = false # latched forever — eventQueueStuck is sticky terminal.
115109
116110 while ctx.running.load ():
117- # Wake on enqueue or tick — whichever first. The enqueue path lands in PR #69;
118- # until then the wait always times out and we fall through to the heartbeat check.
111+ # Wake on enqueue or tick — whichever first.
119112 discard await ctx.eventQueueSignal.wait ().withTimeout (EventThreadTickInterval )
120113
121114 ctx.drainEventQueue ()
122115
116+ # Fire after drain so reg.lock is free — FFI-thread would deadlock here.
117+ if not notifiedStuck and ctx.eventQueueStuck.load ():
118+ onNotResponding (ctx)
119+ notifiedStuck = true
120+
123121 if not ctx.running.load ():
124122 break
125123 hb.check (ctx)
@@ -134,5 +132,5 @@ proc eventThreadBody[T](ctx: ptr FFIContext[T]) {.thread.} =
134132
135133 try :
136134 waitFor eventRun (ctx)
137- except CatchableError as exc :
138- error " event thread exited with exception" , error = exc .msg
135+ except CatchableError as e :
136+ error " event thread exited with exception" , error = e .msg
0 commit comments