66using Akka . Hosting . HealthChecks ;
77using Microsoft . Extensions . DependencyInjection ;
88using Microsoft . Extensions . Diagnostics . HealthChecks ;
9+ using Microsoft . Extensions . Hosting ;
910using Xunit ;
1011using Xunit . Abstractions ;
1112
1213namespace Akka . Hosting . Tests . HealthChecks ;
1314
1415public class HealthChecksSpec : TestKit . TestKit
1516{
16- public HealthChecksSpec ( ITestOutputHelper output )
17- : base ( output : output ) { }
18-
17+ public HealthChecksSpec ( ITestOutputHelper output )
18+ : base ( output : output )
19+ {
20+ }
21+
1922 private class FooActor : UntypedActor
2023 {
2124 protected override void OnReceive ( object message )
@@ -28,61 +31,64 @@ protected override void ConfigureAkka(AkkaConfigurationBuilder builder, IService
2831 builder
2932 . WithActorSystemLivenessCheck ( ) // have to opt-in to the built-in health check
3033 . WithHealthCheck ( "FooActor alive" , async ( system , registry , cancellationToken ) =>
31- {
32- /*
33- * N.B. CancellationToken is set by the call to MSFT.EXT.DIAGNOSTICS.HEALTHCHECK,
34- * so that value could be "infinite" by default.
35- *
36- * Therefore, it might be a really, really good idea to guard this with a non-infinite
37- * timeout via a LinkedCancellationToken here.
38- */
39- try
4034 {
41- var fooActor = await registry . GetAsync < FooActor > ( cancellationToken ) ;
42-
35+ /*
36+ * N.B. CancellationToken is set by the call to MSFT.EXT.DIAGNOSTICS.HEALTHCHECK,
37+ * so that value could be "infinite" by default.
38+ *
39+ * Therefore, it might be a really, really good idea to guard this with a non-infinite
40+ * timeout via a LinkedCancellationToken here.
41+ */
4342 try
4443 {
45- var r = await fooActor . Ask < ActorIdentity > ( new Identify ( "foo" ) , cancellationToken : cancellationToken ) ;
46- if ( r . Subject . IsNobody ( ) )
47- return HealthCheckResult . Unhealthy ( "FooActor was alive but is now dead" ) ;
44+ var fooActor = await registry . GetAsync < FooActor > ( cancellationToken ) ;
45+
46+ try
47+ {
48+ var r = await fooActor . Ask < ActorIdentity > ( new Identify ( "foo" ) ,
49+ cancellationToken : cancellationToken ) ;
50+ if ( r . Subject . IsNobody ( ) )
51+ return HealthCheckResult . Unhealthy ( "FooActor was alive but is now dead" ) ;
52+ }
53+ catch ( Exception e )
54+ {
55+ return HealthCheckResult . Degraded ( "FooActor found but non-responsive" , e ) ;
56+ }
4857 }
49- catch ( Exception e )
58+ catch ( Exception e2 )
5059 {
51- return HealthCheckResult . Degraded ( "FooActor found but non-responsive " , e ) ;
60+ return HealthCheckResult . Unhealthy ( "FooActor not found in registry " , e2 ) ;
5261 }
53- }
54- catch ( Exception e2 )
55- {
56- return HealthCheckResult . Unhealthy ( "FooActor not found in registry" , e2 ) ;
57- }
5862
59- return HealthCheckResult . Healthy ( "fooActor found and responsive" ) ;
60- } ) ;
63+ return HealthCheckResult . Healthy ( "fooActor found and responsive" ) ;
64+ } ) ;
6165 }
6266
6367 [ Fact ]
6468 public async Task ShouldHaveDefaultHealthCheckRegistration ( )
6569 {
6670 // arrange
6771 var configurationBuilder = Host . Services . GetRequiredService < AkkaConfigurationBuilder > ( ) ;
68-
72+
6973 // act
70-
74+
7175 // assert
7276 Assert . Equal ( 2 , configurationBuilder . HealthChecks . Count ) ; // 1 built-in, 1 custom
73-
77+
7478 // find the built-in implementation
75- var actorSystemHealthCheckRegistration = configurationBuilder . HealthChecks . Values . Single ( c => c . HealthCheck is ActorSystemLivenessCheck ) ;
79+ var actorSystemHealthCheckRegistration =
80+ configurationBuilder . HealthChecks . Values . Single ( c => c . Factory ( Host . Services ) is ActorSystemLivenessCheck ) ;
7681 var akkaHealthCheckContext = new AkkaHealthCheckContext ( Sys )
7782 { Registration = actorSystemHealthCheckRegistration . ToHealthCheckRegistration ( ) } ;
78-
83+
7984 // invoke the actorSystem liveness check
80- var healthCheckResult = await actorSystemHealthCheckRegistration . HealthCheck . CheckHealthAsync ( akkaHealthCheckContext , CancellationToken . None ) ;
81-
85+ var healthCheck = actorSystemHealthCheckRegistration . Factory ( Host . Services ) ;
86+ var healthCheckResult = await healthCheck . CheckHealthAsync ( akkaHealthCheckContext , CancellationToken . None ) ;
87+
8288 // assert - system is alive, health check should be healthy
8389 Assert . Equal ( HealthStatus . Healthy , healthCheckResult . Status ) ;
8490 }
85-
91+
8692 [ Fact ]
8793 public async Task ShouldReturnAppropriateResults ( )
8894 {
@@ -91,11 +97,11 @@ public async Task ShouldReturnAppropriateResults()
9197
9298 // act
9399 var customActorHealthCheck =
94- configurationBuilder . HealthChecks . Values . Single ( c => c . HealthCheck is DelegateHealthCheck ) ;
95-
100+ configurationBuilder . HealthChecks . Values . Single ( c => c . Factory ( Host . Services ) is DelegateHealthCheck ) ;
101+
96102 var akkaHealthCheckContext = new AkkaHealthCheckContext ( Sys )
97103 { Registration = customActorHealthCheck . ToHealthCheckRegistration ( ) } ;
98-
104+
99105 // should fail - target actor is not alive
100106 await InvokeHealthCheck ( HealthStatus . Unhealthy ) ;
101107
@@ -105,7 +111,7 @@ public async Task ShouldReturnAppropriateResults()
105111
106112 // should succeed - target actor is around
107113 await InvokeHealthCheck ( HealthStatus . Healthy , 3000 ) ;
108-
114+
109115 // kill the target actor
110116 await WatchAsync ( fooActor ) ;
111117 fooActor . Tell ( PoisonPill . Instance ) ;
@@ -118,10 +124,99 @@ public async Task ShouldReturnAppropriateResults()
118124 async Task InvokeHealthCheck ( HealthStatus expectedStatus , int waitMilliseconds = 1 )
119125 {
120126 using var fastCts = new CancellationTokenSource ( TimeSpan . FromMilliseconds ( waitMilliseconds ) ) ;
121- var healthCheckResult = await customActorHealthCheck . HealthCheck . CheckHealthAsync ( akkaHealthCheckContext , fastCts . Token ) ;
122- if ( healthCheckResult . Description != null )
127+ var healthCheck = customActorHealthCheck . Factory ( Host . Services ) ;
128+ var healthCheckResult = await healthCheck . CheckHealthAsync ( akkaHealthCheckContext , fastCts . Token ) ;
129+ if ( healthCheckResult . Description != null )
123130 Output ? . WriteLine ( healthCheckResult . Description ) ;
124131 Assert . Equal ( expectedStatus , healthCheckResult . Status ) ;
125132 }
126133 }
134+
135+ [ Fact ]
136+ public async Task ShouldResolveDiHealthCheckFromContainer ( )
137+ {
138+ // arrange
139+ var configurationBuilder = Host . Services . GetRequiredService < AkkaConfigurationBuilder > ( ) ;
140+
141+ // act - add a DI-resolved health check to the existing configuration
142+ configurationBuilder . WithHealthCheck < TestDiHealthCheck > ( "test-di-health" ) ;
143+
144+ // assert
145+ Assert . Equal ( 3 , configurationBuilder . HealthChecks . Count ) ; // 2 from base configuration + 1 DI-resolved
146+
147+ // find the DI-resolved health check
148+ var diHealthCheckRegistration =
149+ configurationBuilder . HealthChecks . Values . Single ( c => c . Name == "test-di-health" ) ;
150+ var akkaHealthCheckContext = new AkkaHealthCheckContext ( Sys )
151+ {
152+ Registration = diHealthCheckRegistration . ToHealthCheckRegistration ( )
153+ } ;
154+
155+ // invoke the DI-resolved health check - TestDiHealthCheck is registered in ConfigureServices
156+ var healthCheck = diHealthCheckRegistration . Factory ( Host . Services ) ;
157+ var healthCheckResult = await healthCheck . CheckHealthAsync ( akkaHealthCheckContext , CancellationToken . None ) ;
158+
159+ // assert - health check should be healthy
160+ Assert . Equal ( HealthStatus . Healthy , healthCheckResult . Status ) ;
161+ Assert . Equal ( "Test DI health check is working with DI" , healthCheckResult . Description ) ;
162+ }
163+
164+ [ Fact ]
165+ public async Task ShouldResolveDiHealthCheckWithRegistrationTemplate ( )
166+ {
167+ // arrange
168+ var configurationBuilder = Host . Services . GetRequiredService < AkkaConfigurationBuilder > ( ) ;
169+
170+ // act - add a DI-resolved health check using the registration template pattern
171+ configurationBuilder . WithHealthCheck < TestDiHealthCheck > (
172+ "template-test" ,
173+ HealthStatus . Degraded ,
174+ [
175+ "custom" , "test"
176+ ] ) ;
177+
178+ // assert
179+ Assert . Equal ( 3 , configurationBuilder . HealthChecks . Count ) ; // 2 from base configuration + 1 DI-resolved
180+
181+ // find the DI-resolved health check
182+ var diHealthCheckRegistration = configurationBuilder . HealthChecks . Values . Single ( c => c . Name == "template-test" ) ;
183+ Assert . Equal ( HealthStatus . Degraded , diHealthCheckRegistration . FailureStatus ) ;
184+ Assert . Contains ( "custom" , diHealthCheckRegistration . Tags ) ;
185+ Assert . Contains ( "test" , diHealthCheckRegistration . Tags ) ;
186+
187+ var akkaHealthCheckContext = new AkkaHealthCheckContext ( Sys )
188+ { Registration = diHealthCheckRegistration . ToHealthCheckRegistration ( ) } ;
189+
190+ // invoke the health check
191+ var healthCheck = diHealthCheckRegistration . Factory ( Host . Services ) ;
192+ var healthCheckResult = await healthCheck . CheckHealthAsync ( akkaHealthCheckContext , CancellationToken . None ) ;
193+
194+ // assert
195+ Assert . Equal ( HealthStatus . Healthy , healthCheckResult . Status ) ;
196+ Assert . Equal ( "Test DI health check is working with DI" , healthCheckResult . Description ) ;
197+ }
198+
199+ /// <summary>
200+ /// Test health check class that requires DI (simulates ILogger dependency)
201+ /// </summary>
202+ private class TestDiHealthCheck : IAkkaHealthCheck
203+ {
204+ private readonly IServiceProvider ? _serviceProvider ;
205+
206+ public TestDiHealthCheck ( IServiceProvider serviceProvider )
207+ {
208+ // Constructor with DI dependency
209+ _serviceProvider = serviceProvider ;
210+ }
211+
212+ public Task < HealthCheckResult > CheckHealthAsync ( AkkaHealthCheckContext context ,
213+ CancellationToken cancellationToken = default )
214+ {
215+ // Verify that dependencies can be resolved (simulates ILogger usage)
216+ var hasServiceProvider = _serviceProvider != null ;
217+
218+ return Task . FromResult ( HealthCheckResult . Healthy ( "Test DI health check is working" +
219+ ( hasServiceProvider ? " with DI" : "" ) ) ) ;
220+ }
221+ }
127222}
0 commit comments