Skip to content

Backport dev changes to v1.5 for 1.5.66 release - #8186

Merged
Aaronontheweb merged 35 commits into
akkadotnet:v1.5from
Aaronontheweb:backport/1.5.66
Apr 24, 2026
Merged

Backport dev changes to v1.5 for 1.5.66 release#8186
Aaronontheweb merged 35 commits into
akkadotnet:v1.5from
Aaronontheweb:backport/1.5.66

Conversation

@Aaronontheweb

Copy link
Copy Markdown
Member

Summary

Cherry-picks all unported commits from dev to v1.5 for the 1.5.66 release. Includes 33 cherry-picks + release notes commit.

Key Changes

Akka.Streams: OpenTelemetry Trace Context Propagation (#8160)

Akka.Streams: Non-Blocking Materialized-Value TCS (#8161)

Akka.Persistence

Akka.Core

New Features

Docs & Benchmarks

Test Stability

Conflict Resolutions

  • API approval .verified.txt files: took dev's version (latest API surface)
  • MemoryJournal.cs / MemorySnapshotStore.cs: took dev's version (whole-file redesign)
  • CurrentEventsByTagSpec.cs: removed stale FluentAssertions using (already removed on v1.5)
  • ClusterLogSpec.cs: took dev's EventFilter-based test, added missing EnsureEventBusListenerReadyAsync helper

Test plan

  • Build passes locally (dotnet build -c Release)
  • CI passes on v1.5
  • Persistence tests pass
  • Streams tests pass

Aaronontheweb and others added 30 commits April 24, 2026 20:41
…dotnet#8108)

* Add WaitForReadSideAsync() hook to Persistence TCK query specs

Add a protected virtual extensibility point to CurrentAllEventsSpec,
CurrentEventsByTagSpec, and CurrentEventsByPersistenceIdSpec that is
called after events are written and before queries are executed.

Backends with eventually-consistent read models (EventStore, Kafka,
etc.) can override this method to wait for their read side to catch up,
instead of being forced to override entire TCK test methods with
Thread.Sleep hacks.

Evidence: akkadotnet/Akka.Persistence.EventStore#76

* Make TCK query specs tolerant of eventually-consistent backends

Replace the virtual WaitForReadSideAsync() hook with inline polling
using AwaitConditionAsync. Each test now polls its respective Current*
query until the expected number of events are indexed before running
assertions. This is deterministic: synchronous backends pass on the
first poll, eventually-consistent backends converge.

Fixes the root cause of flaky TCK failures in backends like EventStore
where projections are eventually consistent. Previously, these backends
were forced to override entire test methods with Thread.Sleep hacks.

Evidence: akkadotnet/Akka.Persistence.EventStore#76
…et#647) (akkadotnet#8090)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Aaron Stannard <aaron@aaronstannard.com>
* Add null guards for unguarded Type.GetType calls (akkadotnet#1300)

Mechanical fixes for sites where Type.GetType() result was passed
to Activator.CreateInstance without a null check. All four sites
would throw NullReferenceException at runtime if the type name
could not be resolved - these changes replace the NRE with a
descriptive exception message.

Behavioral change: on misconfigured type names, these sites now
throw ConfigurationException or ArgumentException (with the bad
type name in the message) instead of NullReferenceException.

Sites fixed:
- ActorSystemImpl.ConfigureProvider: replaced Debug.Assert with
  ConfigurationException (assert is stripped in Release builds)
- Persistence internal-stash-overflow-strategy: added null check
  before Activator.CreateInstance
- Dns.DnsExt: broke inline Type.GetType into checked variable
- TransportAdapters: added null check (removed ReSharper suppression
  comment that acknowledged the issue)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Add null guards in EventAdapters for config-driven Type.GetType (akkadotnet#1300)

Startup-time config resolution - both sites already threw on null
Type.GetType results, just with misleading error messages:

- EventAdapters line 330: null type used as ConcurrentDictionary key
  threw ArgumentNullException. Now throws ConfigurationException
  with the unresolvable type name.
- EventAdapters.Instantiate line 436: null type passed to
  IsAssignableFrom returned false, threw ArgumentException saying
  type is 'not assignable' when the real problem is the type could
  not be resolved. Now throws ConfigurationException.

Behavioral change: different exception types
(ConfigurationException vs ArgumentNullException/ArgumentException)
and earlier throw point. Both sites are called during ActorSystem
startup from persistence HOCON config.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Add null guards in serializer deserialization paths (akkadotnet#1300)

Wire protocol and deserialization sites where Type.GetType resolves
type names from serialized data. All sites already crashed on null
(NRE or ArgumentNullException from MakeGenericMethod). These
changes replace the crash with a descriptive exception.

Behavioral change: different exception types and earlier throw
point. All callers in the remote/cluster transport layer use
catch (Exception) so the exception type change has no impact on
error handling behavior.

Sites fixed:
- DaemonMsgCreateSerializer.PropsFromProto: null actorClass was
  passed to Props constructor creating silently broken Props.
  Now throws SerializationException.
- ReplicatedDataSerializer (5 sites): null type passed to
  MakeGenericMethod for ORSet, GSet, LWWRegister, DeltaGroup
  deserialization. Now throws SerializationException with the
  unresolvable type name.
- Props.TypeName private setter: null from Type.GetType stored in
  _inputType field, causing delayed NRE. Now throws
  TypeLoadException. Setter is private, only called during JSON
  deserialization.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Add test for EventAdapters with unresolvable binding type (akkadotnet#1300)

Verifies that EventAdapters.Create throws ConfigurationException
with the unresolvable type name when a binding references a type
that cannot be resolved by Type.GetType.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Gregorius Soedharmo <arkatufus@yahoo.com>
Co-authored-by: Aaron Stannard <aaron@aaronstannard.com>
)

Remove racy Identify-based shard-is-dead checks from
PersistentClusterSharding_should_recover_entities_upon_restart and
PersistentClusterSharding_should_permanently_stop_entities_which_passivate.

After akkadotnet#8055 added a backup ShardStopped notification to the coordinator,
the coordinator immediately re-allocates shards with rememberEntities
enabled. The tests used ActorSelection-based Identify to verify the shard
was dead after HandOff, but the coordinator re-creates the shard at the
same path before the check runs. ShardStopped receipt already confirms
the handoff succeeded - the Identify check was redundant.

Co-authored-by: Aaron Stannard <aaron@aaronstannard.com>
Co-authored-by: Gregorius Soedharmo <arkatufus@yahoo.com>
* [xUnit 3] Convert Akka.Streams.Tests.TCK

* Remove non-needed project

* Add description to Akka.Streams.Tests.TCK project

---------

Co-authored-by: Aaron Stannard <aaron@petabridge.com>
…ations (akkadotnet#8115)

Convert SeqNo from a sealed class to a readonly record struct, eliminating
per-instance heap allocations in the Akka.Remote reliable delivery hot path.
The record struct auto-generates Equals, GetHashCode, ==, and != operators,
removing ~80 lines of boilerplate while preserving the custom wrap-around
comparison operators needed for 64-bit overflow handling.

Update Send, Message, and codec method signatures to use SeqNo? for optional
sequence numbers, with explicit IHasSequenceNumber interface implementations
to maintain non-nullable access through the buffer APIs.
* [xUnit 3] Fix broken Akka.Remote.TestKit.Xunit2

* Update API Approval list

* Add local only test project for Akka.Remote.TestKit.Xunit2
…us.Down (akkadotnet#8128)

Test fix: Replace RegisterOnMemberRemoved with IsTerminated check

The DownAsync and ClusterLogVerboseDefaultSpec tests used
RegisterOnMemberRemoved to wait for cluster shutdown completion. This
callback fires via OnMemberStatusChangedListener.PostStop, which requires
the full actor stop hierarchy to complete after System.Stop is called on
the cluster daemons.

On a single-node cluster that downs itself, the member never transitions
from Down to Removed because a downed node is excluded from leader
election and only the leader performs the Down-to-Removed transition. The
callback only fires as a side effect of PostStop when the cluster daemons
are stopped via ShutdownSelfWhenDown. On loaded CI systems (particularly
Linux), the full stop propagation through the actor hierarchy can exceed
the 10-second timeout.

Replace with AwaitConditionAsync for Cluster.IsTerminated, which becomes
true at the very start of Shutdown() - before System.Stop is called and
before any actor stop propagation. This is both faster and more reliable.

Also remove unused Akka.TestKit.Extensions and Xunit.Sdk imports.

Doc fix: Correct XML doc references in RegisterOnMemberRemoved and
OnMemberStatusChangedListener

Several XML doc comments incorrectly referenced MemberStatus.Down instead
of MemberStatus.Removed. The RegisterOnMemberRemoved param doc, the
OnMemberStatusChangedListener constructor exception doc, and the PreStart
and PostStop exception docs all stated Down where Removed is the actual
accepted value.

Co-authored-by: Aaron Stannard <aaron@aaronstannard.com>
akkadotnet#8122)

Co-authored-by: Aaron Stannard <aaron@aaronstannard.com>
Replace hardcoded 2s TestLatch timeout with TestKitSettings.DefaultTimeout
(5s, supports time dilation). This matches the pattern used by every other
latch call in ReceiveTimeoutSpec and prevents flakes under CI load.
…er (akkadotnet#8154)

The WithinAsync wrapper doesn't modify AwaitAssertAsync's timeout behavior
when an explicit duration is passed. RemainingOrDilated only uses the Within
remaining time when duration is null; otherwise it just dilates the explicit
value. The wrapper was purely a wall-clock timing guard that flaked under CI
resource pressure (block overshot the 2s dilated max by ~0.5s).

The tests still verify the core behavior: AwaitAssertAsync throws
EqualException when the assertion always fails.
…tnet#8155)

The base MultiNodeClusterSpec config sets run-coordinated-shutdown-when-down
to off, which is needed by tests that observe cluster state after downing.
NodeChurnSpec doesn't need this — it tests gossip payload size, and
AwaitRemoved has already finished observing cluster state before Shutdown()
is called.

On even rounds (Down path), the transient systems never started coordinated
shutdown, so Shutdown() had to run the entire termination sequence from
scratch within the default 5s timeout. On odd rounds (Leave path), systems
were already terminating.

Override the setting to on (matching the production default in Cluster.conf)
so both paths start coordinated shutdown automatically. By the time
Shutdown() runs, the systems are already well into termination.
…otnet#8158)

The two virtual hooks OnBeforeTaskStarted / OnAfterTaskCompleted have
always been protected, clearly intended as extension points for
libraries that need to observe async message handler lifetimes. But the
ctor was internal, blocking any external subclass from actually hooking
them without reflection or runtime codegen.

Widening the ctor to protected internal finishes the extension point
that was already half-exposed. protected lets external libraries
subclass; the internal part preserves the existing new
ActorTaskScheduler(this) call site in ActorCell without requiring a
derived type there.

Motivation: Phobos (the Akka.NET observability plugin) needs to
subclass ActorTaskScheduler to keep akka.msg.recv spans open for the
full duration of ReceiveAsync / CommandAsync handlers. Today the span
closes at the first await yield point, hiding all post-await work from
traces. Customers running Phobos on both .NET and .NET Framework hit
this, and we need a cross-runtime fix — IgnoresAccessChecksTo works
only on CoreCLR, reflection can't synthesize a subclass with an
inaccessible base ctor, and runtime codegen is AOT-hostile. Widening
the ctor is the clean fix.

No behavior change. Pure access widening, no caller breaks.
* Add Akka.Streams throughput benchmarks for trace-context baseline

Adds StreamThroughputBenchmarks with 4 benchmarks measuring element
throughput for different stream topologies:

- Linear_Pipeline_Throughput: Source → 2 Selects → Sink
- Deep_Pipeline_5_Stages_Throughput: Source → 5 Selects → Sink
- Merge_Fan_In_Throughput: 2 Sources → Merge → Sink
- Batch_Fan_In_Throughput: Source → Batch → SelectMany → Sink

These establish baseline throughput numbers before trace-context
propagation is added in PR akkadotnet#8160. Uses OperationsPerInvoke to report
per-element throughput.

* Add ThroughputBenchmarkConfig with Req/sec column

Reports elements per second for easier throughput comparison
between baseline and trace-context branches.
…llback (akkadotnet#8173)

* Add allow-unregistered-types setting to disable default serializer fallback (akkadotnet#8164)

When set to false, FindSerializerForType will throw SerializationException
if no explicit serializer binding exists, rather than falling back to the
System.Object serializer (Newtonsoft.Json by default).

This enables strict serialization control for security-sensitive applications
where only explicitly registered types should be serializable.

* Add security documentation for allow-unregistered-types setting

Documents the new serialization security setting with guidance on when
and why to disable the default serializer fallback.

* Add default config assertion and update security docs

- Add test asserting allow-unregistered-types defaults to true
- Add link to serialization security guidance in remoting security docs

* Fix MD050 markdown lint: use consistent asterisk bold style
…Switch, Source.Never) (akkadotnet#8170)

Replace default TaskCompletionSource<T>() with
TaskEx.NonBlockingTaskCompletionSource<T>() in:
- KillSwitch.cs — UniqueKillSwitchStage / UniqueBidiKillSwitchStage
  promises (L225/L291) and SharedKillSwitch._shutdownPromise (L445)
- Dsl/Source.cs — Source.Never<T>()

These sites are benign for the interpreter-inlining hazard described in
akkadotnet#8161 (the KillSwitch tasks are consumed internally via ContinueWith-
wrapped async callbacks; Source.Never's TCS is never completed). Flipping
them is a cheap consistency cleanup so the whole Akka.Streams module
converges on TaskEx.NonBlockingTaskCompletionSource<T>() and future
contributors don't accidentally copy the old pattern.

Part of akkadotnet#8161. No public API or wire-format changes.

Co-authored-by: Aaron Stannard <aaron@aaronstannard.com>
…cpStages (akkadotnet#8169)

Replace default TaskCompletionSource<T>() with
TaskEx.NonBlockingTaskCompletionSource<T>() for:
- ConnectionSourceStageLogic._unbindPromise — returned to user via
  StreamTcp.ServerBinding's unbind lambda; completed on stage-actor
  thread. This site was missing from the audit in akkadotnet#8161.
- OutgoingConnectionStage._localAddressPromise and
  _outgoingConnectionPromise (from akkadotnet#8161 audit)

The localAddress -> outgoingConnection ContinueWith(..., AttachedToParent)
chain is unchanged: that internal hop runs lightweight TrySetResult code
on the TCP actor thread, while user continuations on the materialized
OutgoingConnection task now dispatch asynchronously as intended.

Part of akkadotnet#8161. No public API or wire-format changes.
…on-TCP IO (akkadotnet#8168)

Replace default TaskCompletionSource<T>() with
TaskEx.NonBlockingTaskCompletionSource<T>() for IO result / wake-up TCS
in:
- Implementation/IO/IOSinks.cs — FileSink, OutputStreamSink
- Implementation/IO/IOSources.cs — FileSource, InputStreamSource
- Implementation/IO/OutputStreamSourceStage.cs — WakeUp

Completion for these stages happens on the stage-actor thread; flipping
to RunContinuationsAsynchronously prevents user `await ioResult`
continuations from inlining on that thread.

Part of akkadotnet#8161. No public API or wire-format changes.
…inks + Sources + Stages (akkadotnet#8167)

Replace default TaskCompletionSource<T>() with
TaskEx.NonBlockingTaskCompletionSource<T>() for materialized-value TCS
in:
- Implementation/Sinks.cs — SeqStage, QueueSink, LazySink, and the
  FirstOrDefaultStage / LastOrDefaultStage types (public API surface,
  currently uninstantiated; flipped for consistency)
- Implementation/Sources.cs — Source.Queue completion
  (WatchCompletionAsync), LazySource
- Implementation/Stages/Stages.cs — FirstOrDefault<TIn> and
  LastOrDefault<TIn>, the LIVE implementations backing
  Sink.First<T>() / Sink.FirstOrDefault<T>() / Sink.Last<T>() /
  Sink.LastOrDefault<T>() via Dsl/Sink.cs

The audit in akkadotnet#8161 pointed at the Sinks.cs FirstOrDefault / LastOrDefault
types, but those types are never actually constructed — the user-facing
Sink.First / .Last APIs are backed by the types in Stages/Stages.cs,
which this commit also flips. Prevents user continuations from inlining
on the stream interpreter thread when the TCS fires.

Part of akkadotnet#8161. No public API or wire-format changes.
…ore stage infra + Fusing (akkadotnet#8166)

Replace default TaskCompletionSource<T>() with
TaskEx.NonBlockingTaskCompletionSource<T>() for materialized-value / feedback
TCS in:
- Stage/GraphStage.cs — NoPromise sentinel and ConcurrentAsyncCallback
  InvokeWithFeedback
- Implementation/SetupStage.cs — SetupFlowStage, SetupSourceStage
- Implementation/Fusing/GraphStages.cs — TerminationWatcher,
  MaterializedValueSource, TaskFlattenSource, IgnoreSink
- Implementation/Fusing/Ops.cs — LazyFlow

Prevents user continuations from inlining on the stream interpreter
thread when the TCS fires. Part of akkadotnet#8161. No public API or wire-format
changes.
…treamRef + DSL stages (akkadotnet#8165)

Replace default TaskCompletionSource<T>() with
TaskEx.NonBlockingTaskCompletionSource<T>() for materialized-value TCS
in:
- StreamRef: SourceRefImpl, SinkRefImpl
- DSL: LastElement, Valve (Flip / GetMode / materialized switch)

Without RunContinuationsAsynchronously, a user `await materializedTask`
or `ContinueWith(..., ExecuteSynchronously)` registered before the TCS
fires runs inline on the stream interpreter thread, which can stall
other fused stages. This part of akkadotnet#8161 closes that hole for the listed
call sites. No public API or wire-format changes.

Co-authored-by: Aaron Stannard <aaron@aaronstannard.com>
…kkadotnet#8175)

RememberEntitiesFailureSpec graceful stop test: Replace Thread.Sleep(250)
with event-driven ShardStoreCreated waiting, matching the pattern used by
the working shardStoreStart test. The old approach sent ClearFail to a
potentially dead actor ref after the shard restarted. Convert to async
throughout.

DotNettySslSetupSpec: Reduce inner ExpectMsgAsync timeout from 3s to 500ms
inside AwaitAssertAsync loops. The 3s inner timeout consumed the 30s retry
budget in ~9 attempts, leaving insufficient retries for slow TLS handshakes
on loaded CI machines.
…async TestKit APIs (akkadotnet#8176)

The "start some shards in both regions" step wrapped a serial
Tell-then-ExpectMsg(n, 1s) loop around 20 sharded-delivery round trips.
On a cold shard, the first iteration has to pay for coordinator journal
initialization, cross-node remote delivery, consumer start, and a remote
probe reply - observed at ~870 ms on a Windows CI VM before the reply
round trip even begins. A 1-second per-iteration budget leaves no race
margin on the first entity.

Widen the per-iteration budget to 10 s (the outer Within(30s) still caps
the aggregate) and rebuild the loop so LastSender.Path.Address is read
per envelope rather than after a batch drain. Also convert the entire
spec to the async TestKit API surface - RunOnAsync, EnterBarrierAsync,
WithinAsync, AwaitAssertAsync, ExpectMsgAsync, ReceiveNAsync - to
eliminate the sync-over-async .GetAwaiter().GetResult() calls that the
sync methods route through internally.
…adotnet#8160)

* Akka.Streams: per-element ActivityContext carry through GraphInterpreter

Adds end-to-end OpenTelemetry trace-context continuity through stream
graphs. When an external producer offers an element to a Source.Queue
(or any other stream source) while a parent Activity is alive on the
producer thread, the framework now propagates that context to downstream
stage handlers across dispatcher boundaries where AsyncLocal would
otherwise be lost.

Design mirrors PR akkadotnet#7995 (LogEvent.ActivityContext): capture on the
producer side before the boundary hop, carry it in a framework-owned
field, restore it on the consumer side before invoking the user's stage
handler.

Changes:
- New Akka.Streams.Implementation.StreamsDiagnostics with a framework-
  owned ActivitySource("Akka.Streams"). Register via .AddSource(...) on
  your OTel TracerProvider to opt in.
- Connection.SlotContext parallels Connection.Slot and carries the
  ActivityContext of the element currently in flight on that connection.
- GraphStageLogic.Push captures Activity.Current?.Context into
  SlotContext when writing the slot; Grab clears it alongside the slot.
- GraphInterpreter.ProcessPush starts an "akka.stream.stage <name>"
  Activity with the slot's captured context as parent, sets
  Activity.Current, calls the handler, and disposes in a finally.
  Mirrors the existing previousInterpreter try/finally restore pattern.
- QueueSource Offer<T> carries IngressContext captured from
  Activity.Current at OfferAsync call time; the source's async callback
  handler starts an "akka.stream.offer Source.Queue" ingress span with
  that context as parent before calling Push (which propagates it
  forward via SlotContext).
- Zero-allocation fast path: when the "Akka.Streams" source has no
  listeners or Activity.Current was null at the producer, StartActivity
  returns null and no spans / extra work are performed.

Initial scope covers Source.Queue ingress and sync + async downstream
stages (Select, SelectAsync, Sink.Seq). A follow-up commit generalizes
the ingress capture to all GetAsyncCallback-based sources via the
shared GraphStageLogicWithCallbackWrapper / ConcurrentAsyncCallback
primitive.

Tests: src/core/Akka.Streams.Tests/Implementation/StreamsDiagnosticsSpec.cs
covers three scenarios:
1. sync Select pipeline — stage spans emitted with correct parent chain
2. SelectAsync with user span inside the async lambda — end-to-end
   continuity validated across the async boundary
3. no producer context — zero spans emitted, so background streams
   (Source.Tick, internal sources, etc.) stay invisible to tracing

* Akka.Streams: generalize trace ingress capture via shared callback primitive

Moves the Source.Queue-specific ingress span from QueueSource into the
shared GraphStageLogicWithCallbackWrapper / ConcurrentAsyncCallback
primitive, so every GetAsyncCallback-based source (Source.Queue,
custom StageActor-based sources, anything using the wrapper) gets
producer-thread trace context capture for free without per-stage code.

Key change: capture Activity.Current?.Context on the PRODUCER thread at
InvokeCallbacks (not inside the interpreter-thread callback) so queued
args — i.e. ones pushed before InitCallback runs — retain the producer's
context through the pending queue. This was the missing piece previously:
only the already-initialized fast path captured context, and any call
that raced PreStart landed on the interpreter thread with
Activity.Current = null.

Changes:
- GraphStageLogicWithCallbackWrapper.NotInitialized.Args now holds
  (arg, ActivityContext?) tuples captured at InvokeCallbacks
  producer-side.
- InitCallback drains queued args with the captured context restored
  via an "akka.stream.ingress.queued" Activity.
- ConcurrentAsyncCallback.InvokeWithPromise captures Activity.Current
  ?.Context at Invoke time and wraps the handler with an
  "akka.stream.ingress {stage}" Activity, so downstream Push captures
  it into Connection.SlotContext and ProcessPush creates the first
  stage span with it as parent.
- Source.Queue-specific IngressContext code in Sources.cs is reverted
  (redundant with the generalized path).

Tests (StreamsDiagnosticsSpec.cs):
- ProducerActivityContext_should_propagate_to_downstream_stage_spans:
  still passes, now exercising the generalized path.
- User_span_inside_SelectAsync_lambda_should_parent_to_stage_span:
  still passes, proving async-boundary continuity through the new path.
- Multiple_offers_from_different_traced_scopes_should_preserve_distinct_traces:
  NEW — multi-producer interleaving scenario; verifies that two offers
  from two distinct traced scopes through the same Source.Queue land
  in their own traces, never mixed.
- No_producer_context_should_produce_no_stream_spans:
  still passes, proving the zero-overhead no-trace path.

Source.ActorRef is structurally different (uses ActorPublisher /
reactive streams, not GetAsyncCallback) and is not covered here. A
separate change would be needed to extend the same producer-side
capture to that ingress family.

* Akka.Streams: fan-in stage span linking with ActivityLinks

When a stage like Batch or BatchWeighted merges N input elements into one
output, downstream tracing needs to preserve the trace identity of every
contributing input — not just the one that happened to trigger the flush.
OpenTelemetry's canonical answer for fan-in is span links: pick one primary
parent, attach the rest as ActivityLinks.

Wiring:

- Connection gains SlotLinks (ActivityContext[]) alongside SlotContext, plus
  PendingPushPrimaryContext / PendingPushLinks as a per-push override hook.
- GraphStageLogic.Push consumes the pending override when present and falls
  back to Activity.Current capture otherwise, preserving existing behavior
  for non-fan-in stages.
- GraphInterpreter.ProcessPush starts the downstream stage Activity with
  both the primary context as parent and the accumulated SlotLinks as
  ActivityLinks, tagging the span with the link count for visibility.
- GraphStageLogic exposes two internal APIs for fan-in stages:
  CurrentInletTraceContext (read upstream ctx before Grab clears it) and
  SetFanInTraceContext (stage the next Push as a fan-in flush).
- Grab clears SlotLinks alongside SlotContext.

Batch.Logic uses the new API: OnPush captures each inbound element's trace
context, accumulating across the aggregate. Flush emits the first input's
context as primary parent and the rest as links, then clears. Pending
element state carries its own context across the flush/reseed boundary so
that the element triggering a boundary flush also contributes to the next
aggregate's links correctly. BatchWeighted uses the same Logic class and
inherits the behavior with no further changes.

StreamsDiagnosticsSpec adds BatchWeighted_should_link_all_input_traces_via_
ActivityLinks_on_flushed_stage_span: three producers in distinct traced
scopes offer into a Source.Queue upstream of BatchWeighted, with a gated
SelectAsync(1) downstream holding Batch's outlet busy while the elements
accumulate. The test asserts that the downstream stage span has the first
producer's TraceId as its primary parent and forward links to the other
two producer traces. All 19 existing StreamsDiagnosticsSpec and Batch
tests continue to pass.

Still to do (tracked separately): GroupedWithin, Merge, MergePreferred,
Concat wiring; fan-out validation tests for Broadcast and Balance; and
the broader multi-topology regression suite.

* Akka.Streams: fan-in/fan-out stage trace propagation + spec suite

Extends the fan-in linking mechanism introduced for Batch/BatchWeighted to
every built-in fan-in stage, plus validation coverage for fan-out stages
and a regression guard against tracing background (untraced) streams.

Fan-in wiring:

- GroupedWeightedWithin.Logic (covers GroupedWithin/GroupedWeightedWithin)
  accumulates each inbound element's ActivityContext alongside _buffer,
  plus the pending-element context for boundary overflow. On EmitGroup it
  emits the first collected context as the primary parent and the rest as
  ActivityLinks via SetFanInTraceContext.
- Merge.Logic captures the inlet's SlotContext on both the fast path
  (outlet immediately available) and the slow path (element enqueued for
  later DequeueAndDispatch), staging it as the downstream Push's primary
  parent so trace continuity survives the OnPull boundary where
  Activity.Current would otherwise be null.
- MergePreferred.Logic does the same for both the preferred and secondary
  Emit paths.
- Concat.Logic does the same for every secondary inlet's OnPush.

Merge/MergePreferred/Concat are 1-to-1 pass-throughs, so they never
actually attach multiple ActivityLinks — the "fan-in" API is used only to
override the primary parent from Activity.Current (which on OnPull is
null) to the captured upstream inlet context. Batch/BatchWeighted and
GroupedWeightedWithin are true fan-ins that do attach N-1 links.

Fan-out stages (Broadcast, Balance) require no changes. Each downstream
branch's ProcessPush already captures Activity.Current (which is the
fan-out stage's own span, itself parented to the upstream slot context),
so every branch inherits the producer's trace id via the existing Phase
1/2 machinery.

Test coverage reorganized into four focused spec files under
src/core/Akka.Streams.Tests/Implementation/:

- StreamsDiagnosticsSpec (unchanged existing 4): linear chain + basic
  Source.Queue ingress + multi-producer-scope separation
- StreamsFanInSpec (5 new): BatchWeighted first-wins + link-count,
  GroupedWithin fan-in, Merge and Concat pass-through trace preservation
- StreamsFanOutSpec (2 new): Broadcast and Balance trace-id propagation
  to every branch
- StreamsRegressionSpec (3 new): background Source.Tick silence
  (regression guard against interpreter-level span explosion), mixed
  traced/untraced Merge, GraphDsl-composed sub-graph end-to-end
- StreamsActivityCollector: shared internal helper extracted from
  StreamsDiagnosticsSpec so every spec file can subscribe to the
  "Akka.Streams" ActivitySource the same way

Verification: 14/14 new trace specs pass. 618/618 Dsl.Flow* tests pass
(all Batch/Merge/Concat/MergePreferred/GroupedWithin behavior). 288/288
Implementation + Fusing tests pass. Zero behavioral regressions from
the new instrumentation primitives.

* Akka.Streams: bump forged build to 1.5.99.3

Pins Akka.Streams.csproj to Version=1.5.99.3 while keeping
AssemblyVersion=1.5.60.0 for binary compat with the stable Akka 1.5.60
package consumed by downstream consumers. The root VersionPrefix bumps
to 1.5.60 so that when Akka.Streams.csproj is packed via ProjectReference,
the Akka dependency in the resulting nuspec resolves to 1.5.60 (which
exists on nuget.org) rather than a phantom 1.5.99.x that does not.

The 1.5.99.3 nupkg carries the fan-in linking machinery plus the new
StreamsFanInSpec / StreamsFanOutSpec / StreamsRegressionSpec suite and
is published to the testlab feed so downstream consumers can
validate end-to-end trace continuity through a real pipeline.

* Akka.Streams: add StreamsTraceRenderingSpec + trace-sample documentation artifacts

Adds a focused xUnit spec that materializes six representative stream
topologies, captures the real Activity spans the interpreter emits, and
renders each scenario as an ASCII span-tree markdown file under
src/core/Akka.Streams.Tests/Implementation/trace-samples/. The renderings
are documentation artifacts answering two questions a maintainer would
otherwise have to run the framework to answer:

- Which stages appear as spans under which topology?
- How do fan-in ActivityLinks actually look in the emitted output?

Scenarios covered:

- linear-chain.md            Source.Queue -> Select -> Sink.Seq
- selectasync-user-span.md   SelectAsync with a user span inside the lambda
- batchweighted-fan-in.md    3x concurrent producers merged into one
                             BatchWeighted aggregate; shows the flushed
                             downstream span carrying 2 ActivityLinks
- merge-two-sources.md       Merge pass-through with 2 independent
                             producer traces both reaching the downstream
                             Select with their own TraceId intact
- broadcast-two-branches.md  Broadcast(2) fan-out, both branches inherit
                             the producer TraceId
- untraced-tick-zero-spans.md  Background Source.Tick regression guard —
                             zero spans emitted, proving the cardinality
                             guarantee

All six span trees are built from real Activity objects produced by a
live GraphInterpreter in each test method, not from hand-drawn data.

Rendering to disk is opt-in via the AKKA_STREAMS_RENDER_TRACE_SAMPLES=1
environment variable so day-to-day test runs do not churn the committed
markdown files with fresh TraceId / SpanId hex strings. Default test
runs still execute each scenario end-to-end and dump the tree to the
xUnit Output writer — they just don't overwrite the committed samples.

Includes a trace-samples/README.md with a scenarios table, the
regeneration command, and an explanation of how the renderings are
produced so future readers can audit or extend the set.

* Akka.Streams: regenerate API approval + force W3C Activity format in stream trace tests

Two CI fixes for PR akkadotnet#8160, both test-side only. No library code changes.

## Akka.API.Tests CoreAPISpec.ApproveStreams (all 3 platforms)

The public-API approval baselines at
src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveStreams.{DotNet,Net}.verified.txt
did not yet include the new public surface introduced by this branch:

- Akka.Streams.Implementation.StreamsDiagnostics (InternalApi-tagged class)
  with ActivitySourceName, ActivitySource, and GetStageName members
- Connection.SlotContext, SlotLinks, PendingPushPrimaryContext, PendingPushLinks
  (new public properties on the nested Connection type in GraphInterpreter)
- GraphStageLogic.CurrentInletTraceContext<T>(Inlet<T>)
- GraphStageLogic.SetFanInTraceContext<T>(Outlet<T>, ActivityContext, IReadOnlyList<ActivityContext>)

Regenerated both baseline files (byte-identical except for the TargetFramework
assembly attribute line — v6.0 vs netstandard2.0). Preserves CRLF line endings
and UTF-8 BOM to match the repo's existing convention for verified files.

## Streams trace specs — netfx_tests_windows (net48 only)

Nine Streams*Spec tests assert against ActivityTraceId equality, but on
.NET Framework 4.8 every test produced all-zero TraceIds
(00000000000000000000000000000000), causing every cross-span comparison
to fail despite both sides being "equal".

Root cause: Activity.DefaultIdFormat on .NET Framework 4.8 defaults to
ActivityIdFormat.Hierarchical, not W3C. When an ActivitySource starts an
activity under Hierarchical format, its Context struct's TraceId / SpanId
slots are unpopulated (default). On modern runtimes (.NET 6+) the default
is W3C and the context slots are populated correctly — which is why the
net10 runs passed locally and in the net_tests_{linux,windows} CI legs
but netfx_tests_windows failed.

Fix (test-side only): set

    Activity.DefaultIdFormat = ActivityIdFormat.W3C;
    Activity.ForceDefaultIdFormat = true;

in the static constructors of StreamsActivityCollector and
ProducerActivityScope — the two helper types every Streams*Spec test
touches at the top of each test body. Setting it in a static cctor
guarantees the format is applied before any ActivitySource in the
test process creates its first activity.

Important: the Akka.Streams library itself is NOT affected. This is a
test-process environmental setup that makes the netfx test runner
behave the same as the modern runtimes. The library code works
correctly under either ID format. No TFM change, no library source
change.

* Akka.Streams: downgrade fan-in linking API from public to internal

The fan-in trace-linking plumbing introduced earlier in this branch
exposed six members to the public API surface that don't need to be
public. The only callers are framework-internal stage implementations
(Batch.Logic, GroupedWeightedWithin.Logic, Merge.Logic,
MergePreferred.Logic, Concat.Logic), all of which live inside the
Akka.Streams assembly itself. Downgrading visibility keeps the
ergonomics the same for in-assembly callers (no code change on their
side, internal accessibility from within the defining assembly) while
shrinking the public API surface and reserving flexibility to evolve
the fan-in API shape later without a breaking change.

- GraphStageLogic.CurrentInletTraceContext<T>       protected -> internal
- GraphStageLogic.SetFanInTraceContext<T>           protected -> internal
- Connection.SlotContext                               public -> internal
- Connection.SlotLinks                                 public -> internal
- Connection.PendingPushPrimaryContext                 public -> internal
- Connection.PendingPushLinks                          public -> internal

StreamsDiagnostics (the framework-owned ActivitySource) stays public
and [InternalApi]-tagged — users opting their TracerProvider into the
new stream spans need the const string "Akka.Streams" to call
.AddSource(...) on their OTel pipeline.

Regenerated CoreAPISpec.ApproveStreams baselines (both DotNet.verified
and Net.verified variants) to drop the six lines that no longer appear
in the public surface. Full Streams trace spec suite (StreamsDiagnostics-
Spec, StreamsFanInSpec, StreamsFanOutSpec, StreamsRegressionSpec,
StreamsTraceRenderingSpec — 20 tests) still passes on net10.0 with no
changes to the calling code: InternalsVisibleTo("Akka.Streams.Tests")
lets the specs reach the now-internal helpers directly.

* Akka.Streams: cleanup pass on trace-context code — hot-path guards, string consts, helper dedup

Post-review cleanup of the trace-context propagation work. Three classes of change:

1. HasListeners() hot-path guards

GraphStageLogic.Push, GraphInterpreter.ProcessPush, and
ConcurrentAsyncCallback.InvokeWithPromise now short-circuit the
Activity.Current read and the StartActivity / GetStageName work when
nothing is listening to the "Akka.Streams" ActivitySource. Previously
Push would always read Activity.Current?.Context (one nullable
property access + field writes per element) and ProcessPush would
always call StreamsDiagnostics.GetStageName(stage) (reflection on
DeclaringType + IndexOf + Substring) before discovering that
StartActivity returns null. With the guard, the per-element cost on
non-traced streams drops from ~40-100 CPU cycles to essentially zero.

GraphStageLogicWithCallbackWrapper.InvokeCallbacks also moves the
Activity.Current capture inside the NotInitialized branch so the
Initialized and Stopped fast paths don't touch Activity.Current at all.

2. Stringly-typed constants

Span operation names ("akka.stream.stage", "akka.stream.ingress",
"akka.stream.ingress.queued") and tag keys ("stream.stage.type",
"stream.fan_in.links") are now internal const fields on
StreamsDiagnostics. Call sites in GraphInterpreter.cs and GraphStage.cs
reference them instead of raw strings.

3. Deduplication of pass-through fan-in pattern

Merge.Logic (fast path + slow-path DequeueAndDispatch) and Concat.Logic
used to repeat a four-line pattern inline:
    var ctx = CurrentInletTraceContext(inlet);
    var element = Grab(inlet);
    if (ctx.HasValue) SetFanInTraceContext(outlet, ctx.Value, null);
    Push(outlet, element);

Factored into a single internal helper GrabAndPushFanIn<TIn, TOut>
on GraphStageLogic, with the TIn : TOut constraint so it works for
Merge/Concat's covariant element types. MergePreferred stays inline
because it uses the Emit deferred-push path instead of direct Push,
and wrapping Emit cleanly would need its own helper for a modest win.

Also: static-constructor W3C activity format setup is now a single
StreamsActivityTestSetup.EnsureW3CActivityFormat() call, deduplicated
between StreamsActivityCollector and ProducerActivityScope.

No functional changes. All 73 affected tests (trace specs + Flow Merge
+ Flow Concat + Flow Batch + Flow GroupedWithin + MergePreferred behavior
specs) and the CoreAPISpec.ApproveStreams approval test pass on net10.0
after the refactor.

* Akka.Streams: cache stage names, dedup fan-in helpers, remove experimental version overrides

- Cache stage name + pre-formatted operation names per concrete
  GraphStageLogic type in a ConcurrentDictionary, eliminating per-element
  reflection (GetType/DeclaringType/Substring) and string interpolation
  on the traced hot path in ProcessPush and OnAsyncInput.

- Extract shared EmitFanInTraceContexts helper in StreamsDiagnostics,
  replacing near-identical EmitAggregateContextsIfAny (Batch) and
  EmitBufferContextsAsFanInLinks (GroupedWeightedWithin).

- Remove hardcoded Version/AssemblyVersion/FileVersion overrides from
  Akka.Streams.csproj that were left over from local experimental builds.

* Akka.Streams: add GrabAndEmitFanIn helper, lazy _bufferContexts, trim comments

- Add GrabAndEmitFanIn helper (Emit variant of GrabAndPushFanIn) and
  use it in MergePreferred's three call sites, eliminating the triplicated
  inline ctx/Grab/SetFanInTraceContext/Emit pattern.

- Make GroupedWeightedWithin's _bufferContexts lazily initialized (matching
  Batch's _aggregateContexts pattern) to avoid an unconditional List
  allocation per materialized stage when tracing is disabled.

- Trim verbose narrating comments in Merge, Push, and OnAsyncInput that
  duplicated the doc comments on the helper methods they call.

* docs: add stream tracing with OpenTelemetry documentation page

Adds a new documentation page covering Akka.Streams trace-context
propagation, including enabling tracing, fan-in semantics, Phobos
integration, and Jaeger screenshot examples.

* Akka.Streams: reduce no-listener trace context overhead on hot path

Guard Grab() cleanup and Push() context capture behind HasValue/HasListeners
checks so the no-listener path skips redundant nullable struct writes. Defer
ActivityContext struct copy in ProcessPush until after the HasValue gate.

* docs: fix markdownlint list style in stream-tracing page

* docs: fix Title Case headings in stream-tracing page

* Akka.Streams.Tests: extract span-draining helpers into StreamsActivityCollector

Move the poll-until-spans-arrive loops from individual test classes into
WaitForSpansAsync and WaitForLinkedSpanAsync on StreamsActivityCollector,
eliminating duplicated deadline/polling boilerplate across three spec files.
…y page (akkadotnet#8177)

The `allow-unregistered-types = false` setting added in v1.5.66 was only
documented in the serialization page. This adds it as a visible layer on
the network security page as well, without duplicating the full content.

Changes:
- remoting/security.md: expand Security Layers from 3 to 4, adding
  Serialization Safety; add a dedicated Serialization Security section
  between Untrusted Mode and VPNs explaining the fallback risk and the
  HOCON setting; improve prose throughout (mTLS benefits, programmatic
  validation intro, startup validation, TLS optional/recommended lists)
- serialization/serialization.md: rewrite the Disabling Default Serializer
  Fallback prose to drop inline-header bullet list; fix grammar in the
  polymorphic serializer section; update cross-reference link to point to
  the new section on the remoting security page

Closes the follow-up requested in akkadotnet#8173.
akkadotnet#8163)

* Ensure WriteMessagesAsync/SaveAsync is called asynchronously in AsyncWriteJournal/SnapshotStore.

* Fix persistence health check timing tests.

---------

Co-authored-by: Mark Dinh <mark.dinh@youlend.com>
Co-authored-by: Aaron Stannard <aaron@petabridge.com>
Co-authored-by: Aaron Stannard <aaron@aaronstannard.com>
…kkadotnet#8178)

On Windows hosts with multiple network adapters, Dns.GetHostEntryAsync
can return APIPA (169.254.x.x) addresses from DNS results when a
secondary NIC fails DHCP and registers the link-local address via
dynamic DNS.

Previously ResolveNameAsync unconditionally selected the last matching
address in the DNS result list. When the last entry was a link-local
address, the transport would bind to or attempt to connect to an
unreachable address, breaking cluster formation.

This fix:
- Filters out IPv4 link-local (169.254.0.0/16) and IPv6 link-local (fe80::/10) addresses
- Falls back to the original unfiltered behavior if all candidates are filtered, preserving backward compatibility
- Switches from LastOrDefault to FirstOrDefault for the filtered list, preferring the DNS server's primary result order
- Preserves loopback addresses (localhost still works normally)

Closes akkadotnet#8178
…ug logging

- FilterLinkLocalAddresses returns IEnumerable<IPAddress> instead of IPAddress[]
- Collapse 8 individual Fact tests into a single Theory with MemberData
- Add debug-level logging when link-local addresses are filtered from DNS results
Aaronontheweb and others added 4 commits April 24, 2026 20:44
* Fix multi-node adapter output race

* Skip xunit2 adapter tests in netfx incremental run
…RestartSpec (akkadotnet#8183)

* fix: use AwaitAssert for eventually-consistent cluster state in QuickRestartSpec

The bare assertions at lines 128-130 checked cluster membership and
unreachable state without polling for convergence. After a random sleep
(0-14s), gossip may not have fully propagated, leaving stale Unreachable
entries. The identical checks 15 lines earlier already used
Within(20s, AwaitAssert(...)) — this brings the post-sleep assertions
in line with that pattern.

Cluster state is eventually consistent by design; bare assertions on it
are a test bug, not a framework issue.

* Convert QuickRestartSpec to async and replace Thread.Sleep with Task.Delay

- All methods now return Task and use WithinAsync/AwaitAssertAsync/RunOnAsync
- Thread.Sleep replaced with await Task.Delay for the gating delay
- Terminate().Wait() replaced with await Terminate().WaitAsync()
- Removed unused System.Threading using
… channel-based drain-on-read pattern (akkadotnet#8184)

* fix(persistence): redesign MemoryJournal and MemorySnapshotStore with channel-based drain-on-read pattern

Replace ReaderWriterLockSlim + mutable collections with unbounded Channel + immutable collections to fix chronic race conditions exposed by Task.Yield() in AsyncWriteJournal.

Key changes:
- Writes enqueue to Channel<T> (non-blocking, never contends)
- Reads drain channel first, ensuring all pending writes are visible
- Use ImmutableList/ImmutableDictionary for thread-safe snapshots
- Remove all locks - channel drain is the synchronization point
- Make SnapshotEntry immutable (sealed class with constructor)
- SharedMemoryJournal overrides single Storage property instead of 4

This fixes the race where ReplayMessages could execute before WriteMessagesAsync completed due to the fire-and-forget pattern with Task.Yield() deferral.

* fix: update .NET Framework API approval for MemoryJournal/SnapshotStore redesign

* fix: add DrainLock to serialize concurrent read operations

AsyncWriteJournal can call read methods from multiple thread pool threads
concurrently due to the fire-and-forget pattern with Task.Yield(). Add a
lock around DrainPendingWrites() to ensure single-reader semantics are
maintained for the channel.

* fix: make JournalStorage and SnapshotStorage members internal

Hide implementation details from public API surface while preserving
the protected class visibility for subclass extension pattern.

* fix: add in-flight operation tracking to prevent write-read races

Use AroundReceive to track when write/delete messages arrive, and wait
for them to complete before draining pending ops. This fixes races where
Task.Yield() in the base class allows reads to proceed before writes
enqueue their operations.

- Add InFlightOps counter and ManualResetEventSlim for coordination
- AroundReceive increments counter for WriteMessages/DeleteMessagesTo
- Async write/delete methods decrement counter and signal completion
- DrainPendingOps waits for InFlightComplete before reading channel

* Revert "fix: add in-flight operation tracking to prevent write-read races"

This reverts commit 8bc14db.

* fix: correct test assertions for non-deterministic persist timing

PersistentActorFailureSpec tests for ThrowingActorOne were asserting
that "err" would not be persisted when an exception is thrown
immediately after calling Persist(). This is incorrect - the persistence
is non-deterministic because:

1. AsyncWriteJournal uses Task.Yield() inside ExecuteBatch
2. The exception is thrown before the journal acknowledgment
3. Whether "err" gets persisted depends on timing

Changed the assertions to filter out "err" and verify the remaining
events are present, making the test resilient to timing variation.

Also converted all tests in PersistentActorFailureSpec to use async/Task
patterns (ExpectMsgAsync, WatchAsync, ExpectTerminatedAsync) and added
ExpectMsgInOrderAsync helper to PersistenceSpec.

* fix: process MemorySnapshotStore deletes through channel to prevent race

The DeleteAsync methods were modifying Storage.Snapshots outside the
DrainLock, causing a race condition where concurrent operations could
lose updates. This is the same pattern fixed earlier in MemoryJournal.

Changes:
- Add ISnapshotOperation interface with WriteSnapshot, DeleteSnapshot,
  and DeleteSnapshotRange implementations
- Rename PendingWrites channel to PendingOperations to handle all ops
- Queue delete operations to channel instead of direct modification
- Process all operations atomically inside DrainPendingOperations lock

This ensures all snapshot modifications are serialized through the
channel drain, preventing lost updates from concurrent access.

* fix: stabilize flaky persistence tests by eliminating race conditions

AtLeastOnceDeliveryCrashSpec: Wait for CrashMessage to be persisted
before stopping supervisor - fixes race where stop could happen before
journal write completed, causing recovery to succeed instead of crash.
Removed LocalFact skip attribute since fix addresses the underlying issue.

SnapshotRecoveryLocalStoreSpec: Use unique GUID-based persistence IDs
and wait for RecoveryCompleted before sending commands - fixes test
isolation issues from stale data and race between actor creation and
command handling.
@Arkatufus
Arkatufus enabled auto-merge April 24, 2026 20:53
@Aaronontheweb
Aaronontheweb disabled auto-merge April 24, 2026 21:10
@Aaronontheweb
Aaronontheweb enabled auto-merge (rebase) April 24, 2026 21:10
… surface

Apply the same API changes introduced by cherry-picks to the .NET Framework
API approval files:
- ActorTaskScheduler protected ctor (akkadotnet#8158)
- MemoryJournal: Messages→Storage (JournalStorage), remove Update, Read param rename (akkadotnet#8184)
- SharedMemoryJournal: Messages→Storage
- MemorySnapshotStore: Snapshots→Storage (SnapshotStorage nested class) (akkadotnet#8184)
- SnapshotEntry: sealed with readonly constructor-initialized properties (akkadotnet#8184)
- StreamsDiagnostics class in Akka.Streams.Implementation (akkadotnet#8160)
@Aaronontheweb
Aaronontheweb disabled auto-merge April 24, 2026 23:25
@Aaronontheweb
Aaronontheweb merged commit ed4bd3b into akkadotnet:v1.5 Apr 24, 2026
8 of 11 checks passed
@Aaronontheweb
Aaronontheweb deleted the backport/1.5.66 branch April 24, 2026 23:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants