Under concurrent GPIO or LLM requests that read/write Pi hardware, Ktor route handlers could stall or time out. The IO dispatcher thread pool would fill with threads parked inside runBlocking waiting for gRPC responses from krill-pi4j.
ServerPiManager.getServerInfo(), readPinState(), setHigh(), and setLow() all wrapped the underlying gRPC calls (which are already suspend functions from Pi4jClient) in runBlocking { }. This blocked whichever thread called them — typically an IO dispatcher thread from a Ktor route handler — until the gRPC response arrived. Under concurrent requests, this could exhaust the IO thread pool (64 threads by default) and stall unrelated coroutines such as DB writes and SSE emissions. The PiManager interface declared these as plain fun, forcing callers to block regardless of whether they were in a coroutine context.
setLow, setHigh, readPinState, and getServerInfo in PiManagerContainer.kt to suspend fun.runBlocking { } from all four methods in ServerPiManager.kt; the Pi4jClient methods they call are already suspend, so no wrapper is needed.ServerNodeManager.readNode(): it called readPinState on every Pin read, which would have prevented readNode from remaining non-suspend. Routes that need live state already call piManager.readPinState() directly in their Ktor suspend handlers; internal consumers (processors, observers) correctly use the persisted state kept current by the PIN_CHANGED event path.PinReconciliationTask.reconcile() a suspend fun (it calls readPinState and is invoked from the already-suspend start()).PinReconciliationTaskTest to use coEvery instead of every for the now-suspend readPinState mock.PiManager (or any interface that wraps async I/O), declare them suspend fun from the start. A non-suspend signature forces runBlocking on callers; a suspend signature is composable and free.suspend functions, never wrap them in runBlocking — the runBlocking is only needed when bridging to non-coroutine code (e.g., main()). A suspend caller can just await directly.override fun foo() { runBlocking { client.bar() } } is a flag for IO dispatcher starvation. Search for runBlocking in server code whenever adding blocking I/O; if the call site is reachable from a Ktor handler or a coroutine, it is almost certainly wrong.