-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Fix .NET 10 CLR shutdown hook breaking change #7964
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
a4f3762
Fix .NET 10 CLR shutdown hook breaking change
Arkatufus f8f329e
fix global.json
Arkatufus b6f3fc7
Fix warning as errors
Arkatufus 74026d4
Merge branch 'dev' into fix-net-10-CLR-hook
Aaronontheweb 914cc91
Install .NET SDK 8.0
Arkatufus dc54820
Add very targetted .NET 10 CI/CD run
Arkatufus a27acf1
Fix CI/CD script
Arkatufus b392bd2
Merge branch 'dev' into fix-net-10-CLR-hook
Arkatufus edec9f8
Merge branch 'dev' into fix-net-10-CLR-hook
Aaronontheweb 0125baf
Code fix
Arkatufus 5bf21b7
Revert .NET 10 CI/CD integration (will be moved to a new PR)
Arkatufus c07799b
whitespace cleanup
Arkatufus f42665b
Merge branch 'dev' into fix-net-10-CLR-hook
Aaronontheweb 57ed9d7
Merge branch 'dev' into fix-net-10-CLR-hook
Aaronontheweb File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
295 changes: 295 additions & 0 deletions
295
src/core/Akka.Tests/Actor/TerminationSignalHandlerSpec.cs
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,295 @@ | ||
| //----------------------------------------------------------------------- | ||
| // <copyright file="TerminationSignalHandlerSpec.cs" company="Akka.NET Project"> | ||
| // Copyright (C) 2009-2022 Lightbend Inc. <http://www.lightbend.com> | ||
| // Copyright (C) 2013-2025 .NET Foundation <https://github.com/akkadotnet/akka.net> | ||
| // </copyright> | ||
| //----------------------------------------------------------------------- | ||
|
|
||
| using System; | ||
| using System.Collections.Generic; | ||
| using System.Collections.Immutable; | ||
| using System.Threading.Tasks; | ||
| using Akka.Actor; | ||
| using Akka.Configuration; | ||
| using Akka.TestKit; | ||
| using Akka.TestKit.Extensions; | ||
| using Akka.Util.Internal; | ||
| using FluentAssertions; | ||
| using Xunit; | ||
| using Xunit.Abstractions; | ||
| using static Akka.Actor.CoordinatedShutdown; | ||
|
|
||
| namespace Akka.Tests.Actor; | ||
|
|
||
| /// <summary> | ||
| /// Tests for the CLR termination signal handling in <see cref="CoordinatedShutdown"/>. | ||
| /// </summary> | ||
| public class TerminationSignalHandlerSpec : AkkaSpec | ||
| { | ||
| public TerminationSignalHandlerSpec(ITestOutputHelper output) : base(output) | ||
| { | ||
| } | ||
|
|
||
| public ExtendedActorSystem ExtSys => Sys.AsInstanceOf<ExtendedActorSystem>(); | ||
|
|
||
| private static readonly Phase EmptyPhase = new(ImmutableHashSet<string>.Empty, TimeSpan.FromSeconds(10), true); | ||
|
|
||
| /// <summary> | ||
| /// Test double for <see cref="ITerminationSignalHandler"/> that allows simulating termination signals. | ||
| /// </summary> | ||
| private class TestTerminationSignalHandler : ITerminationSignalHandler | ||
| { | ||
| public Action RegisteredCallback { get; private set; } | ||
| public bool IsDisposed { get; private set; } | ||
| public int RegisterCallCount { get; private set; } | ||
|
|
||
| public void Register(Action onTerminationSignal) | ||
| { | ||
| RegisterCallCount++; | ||
| RegisteredCallback = onTerminationSignal; | ||
| } | ||
|
|
||
| public void SimulateTerminationSignal() | ||
| { | ||
| RegisteredCallback?.Invoke(); | ||
| } | ||
|
|
||
| public void Dispose() | ||
| { | ||
| IsDisposed = true; | ||
| } | ||
| } | ||
|
|
||
| [Fact(DisplayName = "CoordinatedShutdown should register handler when run-by-clr-shutdown-hook is enabled")] | ||
| public void CoordinatedShutdown_should_register_handler_when_enabled() | ||
| { | ||
| // Arrange | ||
| var phases = new Dictionary<string, Phase> { { "a", EmptyPhase } }; | ||
| var coord = new CoordinatedShutdown(ExtSys, phases); | ||
| var testHandler = new TestTerminationSignalHandler(); | ||
| var conf = ConfigurationFactory.ParseString("run-by-clr-shutdown-hook = on"); | ||
|
|
||
| // Act | ||
| CoordinatedShutdown.InitClrHook(Sys, conf, coord, testHandler); | ||
|
|
||
| // Assert | ||
| testHandler.RegisterCallCount.Should().Be(1); | ||
| testHandler.RegisteredCallback.Should().NotBeNull(); | ||
| } | ||
|
|
||
| [Fact(DisplayName = "CoordinatedShutdown should not register handler when run-by-clr-shutdown-hook is disabled")] | ||
| public void CoordinatedShutdown_should_not_register_handler_when_disabled() | ||
| { | ||
| // Arrange | ||
| var phases = new Dictionary<string, Phase> { { "a", EmptyPhase } }; | ||
| var coord = new CoordinatedShutdown(ExtSys, phases); | ||
| var testHandler = new TestTerminationSignalHandler(); | ||
| var conf = ConfigurationFactory.ParseString("run-by-clr-shutdown-hook = off"); | ||
|
|
||
| // Act | ||
| CoordinatedShutdown.InitClrHook(Sys, conf, coord, testHandler); | ||
|
|
||
| // Assert | ||
| testHandler.RegisterCallCount.Should().Be(0); | ||
| testHandler.RegisteredCallback.Should().BeNull(); | ||
| } | ||
|
|
||
| [Fact(DisplayName = "CoordinatedShutdown should run shutdown tasks when termination signal is received")] | ||
| public async Task CoordinatedShutdown_should_run_when_termination_signal_received() | ||
| { | ||
| // Arrange | ||
| var sys = ActorSystem.Create( | ||
| "TerminationSignalTest", | ||
| ConfigurationFactory.ParseString(@" | ||
| akka.coordinated-shutdown.terminate-actor-system = on | ||
| akka.coordinated-shutdown.run-by-clr-shutdown-hook = on | ||
| akka.coordinated-shutdown.run-by-actor-system-terminate = off")); | ||
|
|
||
| try | ||
| { | ||
| var testHandler = new TestTerminationSignalHandler(); | ||
| var coord = CoordinatedShutdown.Get(sys); | ||
|
|
||
| var taskExecuted = new TaskCompletionSource<bool>(); | ||
| coord.AddTask(PhaseBeforeServiceUnbind, "test-task", () => | ||
| { | ||
| taskExecuted.SetResult(true); | ||
| return Task.FromResult(Done.Instance); | ||
| }); | ||
|
|
||
| // Re-initialize with test handler | ||
| var conf = sys.Settings.Config.GetConfig("akka.coordinated-shutdown"); | ||
| CoordinatedShutdown.InitClrHook(sys, conf, coord, testHandler); | ||
|
|
||
| // Act - simulate termination signal | ||
| testHandler.SimulateTerminationSignal(); | ||
|
|
||
| // Assert | ||
| var result = await taskExecuted.Task.AwaitWithTimeout(TimeSpan.FromSeconds(10)); | ||
| result.Should().BeTrue(); | ||
| coord.ShutdownReason.Should().Be(ClrExitReason.Instance); | ||
| } | ||
| finally | ||
| { | ||
| await sys.Terminate(); | ||
| } | ||
| } | ||
|
|
||
| [Fact(DisplayName = "CoordinatedShutdown should set _runningClrHook flag during CLR shutdown")] | ||
| public async Task CoordinatedShutdown_should_set_running_flag_during_clr_shutdown() | ||
| { | ||
| // Arrange | ||
| var sys = ActorSystem.Create( | ||
| "RunningFlagTest", | ||
| ConfigurationFactory.ParseString(@" | ||
| akka.coordinated-shutdown.terminate-actor-system = on | ||
| akka.coordinated-shutdown.run-by-clr-shutdown-hook = on | ||
| akka.coordinated-shutdown.run-by-actor-system-terminate = off")); | ||
|
|
||
| try | ||
| { | ||
| var testHandler = new TestTerminationSignalHandler(); | ||
| var coord = CoordinatedShutdown.Get(sys); | ||
|
|
||
| var flagObserved = new TaskCompletionSource<bool>(); | ||
| coord.AddTask(PhaseBeforeServiceUnbind, "flag-check-task", () => | ||
| { | ||
| // The _runningClrHook flag should be set by now | ||
| // We can't directly access the private field, but we can verify | ||
| // the shutdown is running with ClrExitReason | ||
| flagObserved.SetResult(coord.ShutdownReason == ClrExitReason.Instance); | ||
| return Task.FromResult(Done.Instance); | ||
| }); | ||
|
|
||
| var conf = sys.Settings.Config.GetConfig("akka.coordinated-shutdown"); | ||
| CoordinatedShutdown.InitClrHook(sys, conf, coord, testHandler); | ||
|
|
||
| // Act | ||
| testHandler.SimulateTerminationSignal(); | ||
|
|
||
| // Assert | ||
| var result = await flagObserved.Task.AwaitWithTimeout(TimeSpan.FromSeconds(10)); | ||
| result.Should().BeTrue(); | ||
| } | ||
| finally | ||
| { | ||
| await sys.Terminate(); | ||
| } | ||
| } | ||
|
|
||
| [Fact(DisplayName = "CoordinatedShutdown should dispose handler when ActorSystem terminates normally")] | ||
| public async Task CoordinatedShutdown_should_dispose_handler_on_normal_termination() | ||
| { | ||
| // Arrange | ||
| var sys = ActorSystem.Create( | ||
| "DisposeTest", | ||
| ConfigurationFactory.ParseString(@" | ||
| akka.coordinated-shutdown.terminate-actor-system = on | ||
| akka.coordinated-shutdown.run-by-clr-shutdown-hook = on | ||
| akka.coordinated-shutdown.run-by-actor-system-terminate = on")); | ||
|
|
||
| var testHandler = new TestTerminationSignalHandler(); | ||
| var coord = CoordinatedShutdown.Get(sys); | ||
| var conf = sys.Settings.Config.GetConfig("akka.coordinated-shutdown"); | ||
| CoordinatedShutdown.InitClrHook(sys, conf, coord, testHandler); | ||
|
|
||
| // Act - terminate system normally (not via signal) | ||
| await sys.Terminate(); | ||
|
|
||
| // Give continuation time to run | ||
| await Task.Delay(100); | ||
|
|
||
| // Assert | ||
| testHandler.IsDisposed.Should().BeTrue(); | ||
| } | ||
|
|
||
| [Fact(DisplayName = "CoordinatedShutdown CLR hooks should only execute once even if signal fires multiple times")] | ||
| public async Task CoordinatedShutdown_clr_hooks_should_only_execute_once() | ||
| { | ||
| // Arrange | ||
| var sys = ActorSystem.Create( | ||
| "IdempotencyTest", | ||
| ConfigurationFactory.ParseString(@" | ||
| akka.coordinated-shutdown.terminate-actor-system = on | ||
| akka.coordinated-shutdown.run-by-clr-shutdown-hook = on | ||
| akka.coordinated-shutdown.run-by-actor-system-terminate = off")); | ||
|
|
||
| try | ||
| { | ||
| var testHandler = new TestTerminationSignalHandler(); | ||
| var coord = CoordinatedShutdown.Get(sys); | ||
|
|
||
| var executionCount = 0; | ||
| coord.AddTask(PhaseBeforeServiceUnbind, "count-task", () => | ||
| { | ||
| executionCount++; | ||
| return Task.FromResult(Done.Instance); | ||
| }); | ||
|
|
||
| var conf = sys.Settings.Config.GetConfig("akka.coordinated-shutdown"); | ||
| CoordinatedShutdown.InitClrHook(sys, conf, coord, testHandler); | ||
|
|
||
| // Act - simulate multiple termination signals | ||
| testHandler.SimulateTerminationSignal(); | ||
| testHandler.SimulateTerminationSignal(); | ||
| testHandler.SimulateTerminationSignal(); | ||
|
|
||
| // Wait for shutdown to complete | ||
| await sys.WhenTerminated.AwaitWithTimeout(TimeSpan.FromSeconds(10)); | ||
|
|
||
| // Assert - task should only have executed once | ||
| executionCount.Should().Be(1); | ||
| } | ||
| finally | ||
| { | ||
| if (!sys.WhenTerminated.IsCompleted) | ||
| await sys.Terminate(); | ||
| } | ||
| } | ||
|
|
||
| [Fact(DisplayName = "CoordinatedShutdown should handle exceptions in shutdown tasks gracefully")] | ||
| public async Task CoordinatedShutdown_should_handle_task_exceptions_gracefully() | ||
| { | ||
| // Arrange | ||
| var sys = ActorSystem.Create( | ||
| "ExceptionTest", | ||
| ConfigurationFactory.ParseString(@" | ||
| akka.coordinated-shutdown.terminate-actor-system = on | ||
| akka.coordinated-shutdown.run-by-clr-shutdown-hook = on | ||
| akka.coordinated-shutdown.run-by-actor-system-terminate = off")); | ||
|
|
||
| try | ||
| { | ||
| var testHandler = new TestTerminationSignalHandler(); | ||
| var coord = CoordinatedShutdown.Get(sys); | ||
|
|
||
| var secondTaskExecuted = new TaskCompletionSource<bool>(); | ||
|
|
||
| coord.AddTask(PhaseBeforeServiceUnbind, "failing-task", () => | ||
| { | ||
| throw new Exception("Test exception"); | ||
| }); | ||
|
|
||
| coord.AddTask(PhaseServiceUnbind, "second-task", () => | ||
| { | ||
| secondTaskExecuted.SetResult(true); | ||
| return Task.FromResult(Done.Instance); | ||
| }); | ||
|
|
||
| var conf = sys.Settings.Config.GetConfig("akka.coordinated-shutdown"); | ||
| CoordinatedShutdown.InitClrHook(sys, conf, coord, testHandler); | ||
|
|
||
| // Act | ||
| testHandler.SimulateTerminationSignal(); | ||
|
|
||
| // Assert - second task should still execute despite first task throwing | ||
| var result = await secondTaskExecuted.Task.AwaitWithTimeout(TimeSpan.FromSeconds(10)); | ||
| result.Should().BeTrue(); | ||
| } | ||
| finally | ||
| { | ||
| if (!sys.WhenTerminated.IsCompleted) | ||
| await sys.Terminate(); | ||
| } | ||
| } | ||
| } |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -20,13 +20,20 @@ | |
| <PackageReference Include="xunit.runner.visualstudio" Version="$(XunitVersion)" /> | ||
| <PackageReference Include="FluentAssertions" Version="$(FluentAssertionsVersion)" /> | ||
| <PackageReference Include="FsCheck.Xunit" Version="$(FsCheckVersion)" /> | ||
| <PackageReference Include="System.Net.Sockets" Version="4.3.0" /> | ||
| <PackageReference Include="System.Runtime.Extensions" Version="4.3.1" /> | ||
| <PackageReference Include="Xunit.SkippableFact" Version="1.4.13" /> | ||
| </ItemGroup> | ||
|
|
||
| <!-- Exclude System.Linq.Async for .NET 10+ as it conflicts with built-in BCL methods --> | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good call |
||
| <ItemGroup Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net10.0'))"> | ||
| <PackageReference Include="System.Linq.Async" Version="6.0.1" ExcludeAssets="all" PrivateAssets="all" /> | ||
| </ItemGroup> | ||
|
|
||
| <PropertyGroup Condition=" '$(TargetFramework)' == '$(NetTestVersion)'"> | ||
| <DefineConstants>$(DefineConstants);CORECLR</DefineConstants> | ||
| </PropertyGroup> | ||
|
|
||
| <PropertyGroup Condition=" '$(TargetFramework)' == '$(NetTenTestVersion)'"> | ||
| <DefineConstants>$(DefineConstants);CORECLR</DefineConstants> | ||
| </PropertyGroup> | ||
|
|
||
| </Project> | ||
Oops, something went wrong.
Oops, something went wrong.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM