Skip to main content

RESP012: keep subscription handlers non-blocking

The bundled warning detects known blocking operations inside await foreach consumers of RespireSubscription or IAsyncEnumerable<RespireMessage>, including implementations and derived interfaces, and WithCancellation / ConfigureAwait streams. It also checks inline lambdas and method groups declared in the same source file and registered with RespireSubscription.DeliveryGap.

async Task ConsumeAsync(
RespireSubscription subscription, Func<RespireMessage, Task> processAsync)
{
subscription.DeliveryGap += gap => Thread.Sleep(100); // RESP012

await foreach (var message in subscription)
{
Thread.Sleep(100); // RESP012
processAsync(message).Wait(); // RESP012
}
}

Recognized operations include Thread.Sleep / Join, task .Result, .Wait() and direct .GetAwaiter().GetResult() chains, Task.WaitAll / WaitAny, SemaphoreSlim.Wait, ManualResetEventSlim.Wait, and WaitHandle waits. Timed waits with a constant zero millisecond timeout or TimeSpan.Zero are excluded because they only probe availability. Thread.Sleep(0) still yields the consumer thread and is reported.

Blocking work in other methods, delegate variables, arbitrary synchronous I/O, locks, and CPU-intensive work is not inferred. Nested lambdas and local functions are excluded from message-loop traversal; work inside them may run elsewhere.

Await asynchronous work inside the message loop:

async Task ConsumeAsync(
RespireSubscription subscription, Func<RespireMessage, Task> processAsync,
CancellationToken stoppingToken)
{
await foreach (var message in subscription.WithCancellation(stoppingToken))
{
await processAsync(message);
}
}

DeliveryGap runs synchronously on the receive path. Keep it short and hand off work to a bounded worker queue; do not turn the callback into async void. Long processing in a message loop can still fill its subscription buffer even when it uses await. Choose buffering and overflow behavior deliberately; see pub/sub.

There is no automatic fix because the appropriate worker lifetime, queue policy, and cancellation behavior belong to the application. RESP009 already offers an await fix for eligible sync-over-async Respire calls.

To change the warning's severity in .editorconfig:

dotnet_diagnostic.RESP012.severity = suggestion