|
| 1 | +## Event-thread body and FFI-thread liveness monitoring. |
| 2 | +## |
| 3 | +## Included from `ffi_context.nim` — inherits its imports, FFIContext type, |
| 4 | +## and the heartbeat-timing constants. Lives alongside `ffi_thread.nim` |
| 5 | +## so each thread's machinery is readable on its own. |
| 6 | +## |
| 7 | +## Responsibilities: |
| 8 | +## - Drain queued events into listener callbacks (queue producer lands in PR #69). |
| 9 | +## - Watch `ctx.ffiHeartbeat` and emit `NotRespondingEvent` / `RespondingEvent` |
| 10 | +## on FFI-thread stall and recovery transitions. |
| 11 | + |
| 12 | +type |
| 13 | + NotRespondingEvent* = object |
| 14 | + RespondingEvent* = object |
| 15 | + |
| 16 | +const |
| 17 | + NotRespondingEventName* = "not_responding" |
| 18 | + RespondingEventName* = "responding" |
| 19 | + |
| 20 | +proc dispatchToListeners[T]( |
| 21 | + ctx: ptr FFIContext[T], eventName: string, data: pointer, dataLen: int |
| 22 | +) = |
| 23 | + ## Holds reg.lock for the entire snapshot + invocation so concurrent |
| 24 | + ## add/remove on this registry blocks until dispatch returns. |
| 25 | + withLock ctx[].eventRegistry.lock: |
| 26 | + let listeners = ctx[].eventRegistry.byEvent.getOrDefault(eventName) |
| 27 | + if listeners.len == 0: |
| 28 | + chronicles.debug "no listener registered", event = eventName |
| 29 | + return |
| 30 | + foreignThreadGc: |
| 31 | + try: |
| 32 | + notifyListeners(listeners, RET_OK, data, dataLen) |
| 33 | + except Exception, CatchableError: |
| 34 | + notifyListenersErr( |
| 35 | + listeners, |
| 36 | + "Exception dispatching " & eventName & ": " & getCurrentExceptionMsg(), |
| 37 | + ) |
| 38 | + |
| 39 | +proc 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. |
| 43 | + let event = |
| 44 | + try: |
| 45 | + EventEnvelope[P](eventType: name, payload: payload).cborEncode() |
| 46 | + except CatchableError as exc: |
| 47 | + chronicles.error "liveness event encode failed", name = name, err = exc.msg |
| 48 | + return |
| 49 | + let dataPtr: pointer = |
| 50 | + if event.len > 0: cast[pointer](unsafeAddr event[0]) |
| 51 | + else: cast[pointer](emptyListenerPayload) |
| 52 | + ctx.dispatchToListeners(name, dataPtr, event.len) |
| 53 | + |
| 54 | +proc onNotResponding*(ctx: ptr FFIContext) = |
| 55 | + emitLivenessEvent(ctx, NotRespondingEventName, NotRespondingEvent()) |
| 56 | + |
| 57 | +proc 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. |
| 61 | + emitLivenessEvent(ctx, RespondingEventName, RespondingEvent()) |
| 62 | + |
| 63 | +proc dispatchQueuedEvent[T](ctx: ptr FFIContext[T], qe: QueuedEvent) = |
| 64 | + ## Frees `qe`'s c_malloc buffers on exit. |
| 65 | + 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) |
| 70 | + ctx.dispatchToListeners($qe.name, qe.data, qe.dataLen) |
| 71 | + |
| 72 | +proc drainEventQueue[T](ctx: ptr FFIContext[T]) = |
| 73 | + while true: |
| 74 | + let opt = ctx.eventQueue.tryDequeueEvent() |
| 75 | + if opt.isNone(): |
| 76 | + break |
| 77 | + ctx.dispatchQueuedEvent(opt.get()) |
| 78 | + |
| 79 | +type HeartbeatMonitor = object |
| 80 | + startedAt: Moment |
| 81 | + lastChange: Moment |
| 82 | + lastValue: int64 |
| 83 | + notifiedStale: bool |
| 84 | + |
| 85 | +proc init(T: type HeartbeatMonitor, ctx: ptr FFIContext): T = |
| 86 | + let now = Moment.now() |
| 87 | + T( |
| 88 | + startedAt: now, |
| 89 | + lastChange: now, |
| 90 | + lastValue: ctx.ffiHeartbeat.load(), |
| 91 | + notifiedStale: false, |
| 92 | + ) |
| 93 | + |
| 94 | +proc 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. |
| 99 | + if Moment.now() - hb.startedAt <= FFIHeartbeatStartDelay: |
| 100 | + return |
| 101 | + let cur = ctx.ffiHeartbeat.load() |
| 102 | + if cur != hb.lastValue: |
| 103 | + if hb.notifiedStale: |
| 104 | + onResponding(ctx) |
| 105 | + hb.lastValue = cur |
| 106 | + hb.lastChange = Moment.now() |
| 107 | + hb.notifiedStale = false |
| 108 | + elif not hb.notifiedStale and |
| 109 | + Moment.now() - hb.lastChange > FFIHeartbeatStaleThreshold: |
| 110 | + onNotResponding(ctx) |
| 111 | + hb.notifiedStale = true |
| 112 | + |
| 113 | +proc eventRun[T](ctx: ptr FFIContext[T]) {.async.} = |
| 114 | + var hb = HeartbeatMonitor.init(ctx) |
| 115 | + |
| 116 | + 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. |
| 119 | + discard await ctx.eventQueueSignal.wait().withTimeout(EventThreadTickInterval) |
| 120 | + |
| 121 | + ctx.drainEventQueue() |
| 122 | + |
| 123 | + if not ctx.running.load(): |
| 124 | + break |
| 125 | + hb.check(ctx) |
| 126 | + |
| 127 | +proc eventThreadBody[T](ctx: ptr FFIContext[T]) {.thread.} = |
| 128 | + ## Drains the event queue and runs the FFI-thread heartbeat check. |
| 129 | + ## Owns the queued `c_malloc` payloads until dispatch returns. |
| 130 | + defer: |
| 131 | + let fireRes = ctx.eventThreadExitSignal.fireSync() |
| 132 | + if fireRes.isErr(): |
| 133 | + error "failed to fire eventThreadExitSignal", err = fireRes.error |
| 134 | + |
| 135 | + try: |
| 136 | + waitFor eventRun(ctx) |
| 137 | + except CatchableError as exc: |
| 138 | + error "event thread exited with exception", error = exc.msg |
0 commit comments