Skip to content

Commit ec63b03

Browse files
Merge branch 'dev' into feature/sql-connectivity-health-checks
2 parents 46cd742 + bb138a3 commit ec63b03

File tree

8 files changed

+581
-67
lines changed

8 files changed

+581
-67
lines changed

README.md

Lines changed: 14 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ If you're migrating from legacy `Akka.Persistence.Sql.Common` based plugins, you
1212
- [Akka.Persistence.Sql](#akkapersistencesql)
1313
- [Getting Started](#getting-started)
1414
* [The Easy Way, Using `Akka.Hosting`](#the-easy-way-using-akkahosting)
15-
+ [Health Checks (Akka.Hosting v1.5.51+)](#health-checks-akkahosting-v1551)
15+
+ [Health Checks](#health-checks)
1616
* [The Classic Way, Using HOCON](#the-classic-way-using-hocon)
1717
* [Supported Database Providers](#supported-database-providers)
1818
+ [Tested Database Providers](#tested-database-providers)
@@ -76,27 +76,22 @@ This includes setting the connection string and provider name again, if necessar
7676
Please consult the Linq2Db documentation for more details on configuring a valid DataOptions object.
7777
Note that `MappingSchema` and `RetryPolicy` will always be overridden by Akka.Persistence.Sql.
7878

79-
### Health Checks (Akka.Hosting v1.5.51+)
79+
### Health Checks
8080

81-
Starting with Akka.Hosting v1.5.51, you can add health checks for your persistence plugins to verify that journals and snapshot stores are properly initialized and accessible. These health checks integrate with `Microsoft.Extensions.Diagnostics.HealthChecks` and can be used with ASP.NET Core health check endpoints.
81+
Starting with Akka.Persistence.Sql v1.5.51 or later, you can add health checks for your persistence plugins to verify that journals and snapshot stores are properly initialized and accessible. These health checks integrate with `Microsoft.Extensions.Diagnostics.HealthChecks` and can be used with ASP.NET Core health check endpoints.
8282

83-
To configure health checks, use the `.WithHealthCheck()` method when setting up your journal and snapshot store:
83+
To configure health checks, use the `journalBuilder` and `snapshotBuilder` parameters with the `.WithHealthCheck()` method:
8484

8585
```csharp
8686
var host = new HostBuilder()
8787
.ConfigureServices((context, services) => {
8888
services.AddAkka("my-system-name", (builder, provider) =>
8989
{
90-
builder
91-
.WithSqlPersistence(
92-
connectionString: _myConnectionString,
93-
providerName: ProviderName.SqlServer2019,
94-
journal: j => j.WithHealthCheck(
95-
unHealthyStatus: HealthStatus.Degraded,
96-
name: "sql-journal"),
97-
snapshot: s => s.WithHealthCheck(
98-
unHealthyStatus: HealthStatus.Degraded,
99-
name: "sql-snapshot"));
90+
builder.WithSqlPersistence(
91+
connectionString: _myConnectionString,
92+
providerName: ProviderName.SqlServer2019,
93+
journalBuilder: journal => journal.WithHealthCheck(HealthStatus.Degraded),
94+
snapshotBuilder: snapshot => snapshot.WithHealthCheck(HealthStatus.Degraded));
10095
});
10196
});
10297
```
@@ -119,12 +114,11 @@ builder.Services.AddHealthChecks();
119114

120115
builder.Services.AddAkka("my-system-name", (configBuilder, provider) =>
121116
{
122-
configBuilder
123-
.WithSqlPersistence(
124-
connectionString: _myConnectionString,
125-
providerName: ProviderName.SqlServer2019,
126-
journal: j => j.WithHealthCheck(),
127-
snapshot: s => s.WithHealthCheck());
117+
configBuilder.WithSqlPersistence(
118+
connectionString: _myConnectionString,
119+
providerName: ProviderName.SqlServer2019,
120+
journalBuilder: journal => journal.WithHealthCheck(),
121+
snapshotBuilder: snapshot => snapshot.WithHealthCheck());
128122
});
129123

130124
var app = builder.Build();

RELEASE_NOTES.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,18 @@
1+
#### 1.5.53 October 14th 2025 ####
2+
3+
**Critical Bug Fix**
4+
5+
This release fixes a critical regression introduced in v1.5.51.1 where `IWriteEventAdapter` and `IReadEventAdapter` instances were not being applied when loading events using `BySequenceNr` queries. This caused event tagging and other event adapter functionality to fail in production scenarios.
6+
7+
* **[Fix EventAdapter regression from v1.5.51.1](https://github.com/akkadotnet/Akka.Persistence.Sql/issues/552)** - Event adapters configured via `WithSqlPersistence()` now work correctly when the method is called multiple times (e.g., once for default persistence with adapters, then again for sharding configuration). Fixed by upgrading to Akka.NET v1.5.53 which includes the fix from [Akka.Hosting#669](https://github.com/akkadotnet/Akka.Hosting/pull/669).
8+
* [Add runtime reproduction test for event adapter regression](https://github.com/akkadotnet/Akka.Persistence.Sql/pull/554)
9+
* [Bump AkkaVersion and AkkaHostingVersion to 1.5.53](https://github.com/akkadotnet/akka.net/releases/tag/1.5.53)
10+
11+
#### 1.5.51.1 October 2nd 2025 ####
12+
13+
* [Fix health check registration bug in Akka.Hosting extensions](https://github.com/akkadotnet/Akka.Persistence.Sql/pull/549)
14+
* [Add Akka.Hosting health check documentation to README](https://github.com/akkadotnet/Akka.Persistence.Sql/pull/548)
15+
116
#### 1.5.51 October 1st 2025 ####
217

318
* [Bump AkkaVersion and AkkaHostingVersion to 1.5.51](https://github.com/akkadotnet/akka.net/releases/tag/1.5.51)
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
// -----------------------------------------------------------------------
2+
// <copyright file="BaselineJournalBuilderSpec.cs" company="Akka.NET Project">
3+
// Copyright (C) 2013-2023 .NET Foundation <https://github.com/akkadotnet/akka.net>
4+
// </copyright>
5+
// -----------------------------------------------------------------------
6+
7+
using Akka.Actor;
8+
using Akka.Event;
9+
using Akka.Hosting;
10+
using Akka.Persistence.Hosting;
11+
using Akka.Persistence.Query;
12+
using Akka.Persistence.Sql.Query;
13+
using Akka.Persistence.Sql.Tests.Common.Containers;
14+
using Akka.Persistence.TCK.Query;
15+
using Akka.Streams;
16+
using Akka.Streams.TestKit;
17+
using FluentAssertions;
18+
using FluentAssertions.Extensions;
19+
using LinqToDB;
20+
using Xunit;
21+
using Xunit.Abstractions;
22+
23+
namespace Akka.Persistence.Sql.Hosting.Tests
24+
{
25+
/// <summary>
26+
/// Baseline test to validate current journalBuilder functionality before refactoring
27+
/// </summary>
28+
public class BaselineJournalBuilderSpec : Akka.Hosting.TestKit.TestKit, IClassFixture<SqliteContainer>
29+
{
30+
private const string PId = "baseline-test";
31+
private readonly SqliteContainer _fixture;
32+
33+
public BaselineJournalBuilderSpec(ITestOutputHelper output, SqliteContainer fixture)
34+
: base(nameof(BaselineJournalBuilderSpec), output)
35+
{
36+
_fixture = fixture;
37+
38+
if (!_fixture.InitializeDbAsync().Wait(10.Seconds()))
39+
throw new Exception("Failed to clean up database in 10 seconds");
40+
}
41+
42+
protected override void ConfigureAkka(AkkaConfigurationBuilder builder, IServiceProvider provider)
43+
{
44+
// Test the refactored pattern to ensure basic persistence works
45+
builder.WithSqlPersistence(
46+
connectionString: _fixture.ConnectionString,
47+
providerName: _fixture.ProviderName);
48+
49+
builder.StartActors((system, registry) =>
50+
{
51+
var actor = system.ActorOf(Props.Create(() => new TestPersistentActor(PId)));
52+
registry.Register<TestPersistentActor>(actor);
53+
});
54+
}
55+
56+
[Fact]
57+
public async Task Refactored_hosting_should_support_basic_persistence()
58+
{
59+
// Arrange
60+
var actor = ActorRegistry.Get<TestPersistentActor>();
61+
62+
// Act - persist an event
63+
actor.Tell("test-event");
64+
await ExpectMsgAsync<string>("ACK", 3.Seconds());
65+
66+
// Verify the event was persisted
67+
var readJournal = Sys.ReadJournalFor<SqlReadJournal>("akka.persistence.query.journal.sql");
68+
var source = readJournal.CurrentEventsByPersistenceId(PId, 0, long.MaxValue);
69+
var probe = source.RunWith(this.SinkProbe<EventEnvelope>(), Sys.Materializer());
70+
71+
probe.Request(1);
72+
var envelope = await probe.ExpectNextAsync(3.Seconds());
73+
envelope.PersistenceId.Should().Be(PId);
74+
envelope.Event.Should().Be("test-event");
75+
await probe.ExpectCompleteAsync();
76+
}
77+
78+
private class TestPersistentActor : ReceivePersistentActor
79+
{
80+
public TestPersistentActor(string persistenceId)
81+
{
82+
PersistenceId = persistenceId;
83+
84+
Command<string>(str =>
85+
{
86+
var sender = Sender;
87+
Persist(str, _ => sender.Tell("ACK"));
88+
});
89+
}
90+
91+
public override string PersistenceId { get; }
92+
}
93+
}
94+
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
// -----------------------------------------------------------------------
2+
// <copyright file="HealthCheckSpec.cs" company="Akka.NET Project">
3+
// Copyright (C) 2013-2023 .NET Foundation <https://github.com/akkadotnet/akka.net>
4+
// </copyright>
5+
// -----------------------------------------------------------------------
6+
7+
using System;
8+
using System.Linq;
9+
using System.Threading;
10+
using System.Threading.Tasks;
11+
using Akka.Hosting;
12+
using Akka.Hosting.HealthChecks;
13+
using Akka.Persistence.Journal;
14+
using Akka.Persistence.Sql.Tests.Common.Containers;
15+
using FluentAssertions;
16+
using FluentAssertions.Extensions;
17+
using Microsoft.Extensions.DependencyInjection;
18+
using Microsoft.Extensions.Diagnostics.HealthChecks;
19+
using Microsoft.Extensions.Hosting;
20+
using Xunit;
21+
using Xunit.Abstractions;
22+
23+
namespace Akka.Persistence.Sql.Hosting.Tests
24+
{
25+
/// <summary>
26+
/// Validates that health checks are properly registered after the refactoring.
27+
/// </summary>
28+
public class HealthCheckSpec : Akka.Hosting.TestKit.TestKit, IClassFixture<SqliteContainer>
29+
{
30+
private readonly SqliteContainer _fixture;
31+
32+
public HealthCheckSpec(ITestOutputHelper output, SqliteContainer fixture)
33+
: base(nameof(HealthCheckSpec), output)
34+
{
35+
_fixture = fixture;
36+
37+
if (!_fixture.InitializeDbAsync().Wait(10.Seconds()))
38+
throw new Exception("Failed to clean up database in 10 seconds");
39+
}
40+
41+
protected override void ConfigureServices(HostBuilderContext context, IServiceCollection services)
42+
{
43+
base.ConfigureServices(context, services);
44+
services.AddHealthChecks();
45+
}
46+
47+
protected override void ConfigureAkka(AkkaConfigurationBuilder builder, IServiceProvider provider)
48+
{
49+
// Use the refactored WithSqlPersistence with health check registration
50+
builder.WithSqlPersistence(
51+
connectionString: _fixture.ConnectionString,
52+
providerName: _fixture.ProviderName,
53+
journalBuilder: journal =>
54+
{
55+
journal.WithHealthCheck(HealthStatus.Degraded);
56+
},
57+
snapshotBuilder: snapshot =>
58+
{
59+
snapshot.WithHealthCheck(HealthStatus.Degraded);
60+
});
61+
}
62+
63+
[Fact]
64+
public async Task Health_checks_should_be_registered_and_healthy()
65+
{
66+
// Arrange
67+
var healthCheckService = Host.Services.GetRequiredService<HealthCheckService>();
68+
69+
// Act - run all health checks
70+
var healthReport = await healthCheckService.CheckHealthAsync(CancellationToken.None);
71+
72+
// Assert - verify that health checks are registered and healthy
73+
healthReport.Entries.Should().NotBeEmpty("health checks should be registered");
74+
75+
// Debug: print all registered health checks (ALL of them, not just SQL)
76+
Output?.WriteLine($"Total health checks registered: {healthReport.Entries.Count}");
77+
foreach (var entry in healthReport.Entries)
78+
{
79+
Output?.WriteLine($" - {entry.Key}: {entry.Value.Status}");
80+
}
81+
82+
// We should have exactly 2 health checks: journal and snapshot
83+
// Look for any Akka.Persistence-related health checks
84+
var persistenceHealthChecks = healthReport.Entries
85+
.Where(e => e.Key.Contains("Akka.Persistence", StringComparison.OrdinalIgnoreCase))
86+
.ToList();
87+
88+
persistenceHealthChecks.Should().HaveCount(2,
89+
"because we registered health checks for both journal and snapshot store");
90+
91+
// Verify journal health check exists and is healthy
92+
var journalHealthCheck = persistenceHealthChecks
93+
.FirstOrDefault(e => e.Key.Contains("journal", StringComparison.OrdinalIgnoreCase));
94+
95+
journalHealthCheck.Should().NotBeNull("journal health check should be registered");
96+
journalHealthCheck.Value.Status.Should().Be(HealthStatus.Healthy,
97+
"SQL journal should be properly initialized");
98+
99+
// Verify snapshot health check exists and is healthy
100+
var snapshotHealthCheck = persistenceHealthChecks
101+
.FirstOrDefault(e => e.Key.Contains("snapshot", StringComparison.OrdinalIgnoreCase));
102+
103+
snapshotHealthCheck.Should().NotBeNull("snapshot health check should be registered");
104+
snapshotHealthCheck.Value.Status.Should().Be(HealthStatus.Healthy,
105+
"SQL snapshot store should be properly initialized");
106+
107+
// Verify overall health status
108+
healthReport.Status.Should().Be(HealthStatus.Healthy,
109+
"because all health checks should pass");
110+
}
111+
}
112+
}

0 commit comments

Comments
 (0)