Backport dev changes to v1.5 for 1.5.66 release - #8186
Merged
Conversation
…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.
…3435) (akkadotnet#8119) Co-authored-by: Gregorius Soedharmo <arkatufus@yahoo.com>
…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
* 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
enabled auto-merge
April 24, 2026 20:53
Aaronontheweb
disabled auto-merge
April 24, 2026 21:10
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)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Cherry-picks all unported commits from
devtov1.5for the 1.5.66 release. Includes 33 cherry-picks + release notes commit.Key Changes
Akka.Streams: OpenTelemetry Trace Context Propagation (#8160)
System.Diagnostics.Activitytrace context end-to-end through stream graphsAkka.Streams: Non-Blocking Materialized-Value TCS (#8161)
TaskCompletionSourcewithRunContinuationsAsynchronouslyAkka.Persistence
Akka.Core
New Features
allow-unregistered-typesserialization settingDocs & Benchmarks
Test Stability
Conflict Resolutions
.verified.txtfiles: took dev's version (latest API surface)MemoryJournal.cs/MemorySnapshotStore.cs: took dev's version (whole-file redesign)CurrentEventsByTagSpec.cs: removed staleFluentAssertionsusing (already removed on v1.5)ClusterLogSpec.cs: took dev's EventFilter-based test, added missingEnsureEventBusListenerReadyAsynchelperTest plan
dotnet build -c Release)