.NET Heisenbug Mystery Theater: How Did an Exception Escape its Catch Block?
A painful lesson on atomicity and the assignment of structs.
21 minutes to readOver the past several months the Akka.NET team has had reports of the following Exception
popping up unexpectedly throughout many of our plugins and end-user applications that use the Akka.Streams1 SelectAsync
stage - such as Akka.Streams.Kafka and Akka.Persistence.Sql:
That error message seems simple enough - it comes from here inside GraphStage.cs
:
[InternalApi] public void InternalOnDownstreamFinish(Exception cause) { try { if (cause == null) throw new ArgumentException("Cancellation cause must not be null", nameof(cause));
In Akka.Streams parlance, a stream gets cancelled when an unhandled Exception
is thrown and that error should be propagated all the way down to this GraphStage.InternalOnDownstreamFinish
method so we can log why the stream is being cancelled / terminated.
Here’s the mystery - this is the code that “threw” the Exception
inside Akka.Persistence.Sql for instance:
.SelectAsync( JournalConfig.DaoConfig.Parallelism, async promisesAndRows => { try { await WriteJournalRows(promisesAndRows.Rows); foreach (var taskCompletionSource in promisesAndRows.Tcs) taskCompletionSource.TrySetResult(NotUsed.Instance); } catch (Exception e) { foreach (var taskCompletionSource in promisesAndRows.Tcs) taskCompletionSource.TrySetException(e); } return NotUsed.Instance; })