Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 14 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ If you're migrating from legacy `Akka.Persistence.Sql.Common` based plugins, you
- [Akka.Persistence.Sql](#akkapersistencesql)
- [Getting Started](#getting-started)
* [The Easy Way, Using `Akka.Hosting`](#the-easy-way-using-akkahosting)
+ [Health Checks (Akka.Hosting v1.5.51+)](#health-checks-akkahosting-v1551)
+ [Health Checks](#health-checks)
* [The Classic Way, Using HOCON](#the-classic-way-using-hocon)
* [Supported Database Providers](#supported-database-providers)
+ [Tested Database Providers](#tested-database-providers)
Expand Down Expand Up @@ -76,27 +76,22 @@ This includes setting the connection string and provider name again, if necessar
Please consult the Linq2Db documentation for more details on configuring a valid DataOptions object.
Note that `MappingSchema` and `RetryPolicy` will always be overridden by Akka.Persistence.Sql.

### Health Checks (Akka.Hosting v1.5.51+)
### Health Checks

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.
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.

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

```csharp
var host = new HostBuilder()
.ConfigureServices((context, services) => {
services.AddAkka("my-system-name", (builder, provider) =>
{
builder
.WithSqlPersistence(
connectionString: _myConnectionString,
providerName: ProviderName.SqlServer2019,
journal: j => j.WithHealthCheck(
unHealthyStatus: HealthStatus.Degraded,
name: "sql-journal"),
snapshot: s => s.WithHealthCheck(
unHealthyStatus: HealthStatus.Degraded,
name: "sql-snapshot"));
builder.WithSqlPersistence(
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cleaned up the documentation some

connectionString: _myConnectionString,
providerName: ProviderName.SqlServer2019,
journalBuilder: journal => journal.WithHealthCheck(HealthStatus.Degraded),
snapshotBuilder: snapshot => snapshot.WithHealthCheck(HealthStatus.Degraded));
});
});
```
Expand All @@ -119,12 +114,11 @@ builder.Services.AddHealthChecks();

builder.Services.AddAkka("my-system-name", (configBuilder, provider) =>
{
configBuilder
.WithSqlPersistence(
connectionString: _myConnectionString,
providerName: ProviderName.SqlServer2019,
journal: j => j.WithHealthCheck(),
snapshot: s => s.WithHealthCheck());
configBuilder.WithSqlPersistence(
connectionString: _myConnectionString,
providerName: ProviderName.SqlServer2019,
journalBuilder: journal => journal.WithHealthCheck(),
snapshotBuilder: snapshot => snapshot.WithHealthCheck());
});

var app = builder.Build();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// -----------------------------------------------------------------------
// <copyright file="BaselineJournalBuilderSpec.cs" company="Akka.NET Project">
// Copyright (C) 2013-2023 .NET Foundation <https://github.com/akkadotnet/akka.net>
// </copyright>
// -----------------------------------------------------------------------

using Akka.Actor;
using Akka.Event;
using Akka.Hosting;
using Akka.Persistence.Hosting;
using Akka.Persistence.Query;
using Akka.Persistence.Sql.Query;
using Akka.Persistence.Sql.Tests.Common.Containers;
using Akka.Persistence.TCK.Query;
using Akka.Streams;
using Akka.Streams.TestKit;
using FluentAssertions;
using FluentAssertions.Extensions;
using LinqToDB;
using Xunit;
using Xunit.Abstractions;

namespace Akka.Persistence.Sql.Hosting.Tests
{
/// <summary>
/// Baseline test to validate current journalBuilder functionality before refactoring
/// </summary>
public class BaselineJournalBuilderSpec : Akka.Hosting.TestKit.TestKit, IClassFixture<SqliteContainer>
{
private const string PId = "baseline-test";
private readonly SqliteContainer _fixture;

public BaselineJournalBuilderSpec(ITestOutputHelper output, SqliteContainer fixture)
: base(nameof(BaselineJournalBuilderSpec), output)
{
_fixture = fixture;

if (!_fixture.InitializeDbAsync().Wait(10.Seconds()))
throw new Exception("Failed to clean up database in 10 seconds");
}

protected override void ConfigureAkka(AkkaConfigurationBuilder builder, IServiceProvider provider)
{
// Test the refactored pattern to ensure basic persistence works
builder.WithSqlPersistence(
connectionString: _fixture.ConnectionString,
providerName: _fixture.ProviderName);

builder.StartActors((system, registry) =>
{
var actor = system.ActorOf(Props.Create(() => new TestPersistentActor(PId)));
registry.Register<TestPersistentActor>(actor);
});
}

[Fact]
public async Task Refactored_hosting_should_support_basic_persistence()
{
// Arrange
var actor = ActorRegistry.Get<TestPersistentActor>();

// Act - persist an event
actor.Tell("test-event");
await ExpectMsgAsync<string>("ACK", 3.Seconds());

// Verify the event was persisted
var readJournal = Sys.ReadJournalFor<SqlReadJournal>("akka.persistence.query.journal.sql");
var source = readJournal.CurrentEventsByPersistenceId(PId, 0, long.MaxValue);
var probe = source.RunWith(this.SinkProbe<EventEnvelope>(), Sys.Materializer());

probe.Request(1);
var envelope = await probe.ExpectNextAsync(3.Seconds());
envelope.PersistenceId.Should().Be(PId);
envelope.Event.Should().Be("test-event");
await probe.ExpectCompleteAsync();
}

private class TestPersistentActor : ReceivePersistentActor
{
public TestPersistentActor(string persistenceId)
{
PersistenceId = persistenceId;

Command<string>(str =>
{
var sender = Sender;
Persist(str, _ => sender.Tell("ACK"));
});
}

public override string PersistenceId { get; }
}
}
}
112 changes: 112 additions & 0 deletions src/Akka.Persistence.Sql.Hosting.Tests/HealthCheckSpec.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
// -----------------------------------------------------------------------
// <copyright file="HealthCheckSpec.cs" company="Akka.NET Project">
// Copyright (C) 2013-2023 .NET Foundation <https://github.com/akkadotnet/akka.net>
// </copyright>
// -----------------------------------------------------------------------

using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Akka.Hosting;
using Akka.Hosting.HealthChecks;
using Akka.Persistence.Journal;
using Akka.Persistence.Sql.Tests.Common.Containers;
using FluentAssertions;
using FluentAssertions.Extensions;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Hosting;
using Xunit;
using Xunit.Abstractions;

namespace Akka.Persistence.Sql.Hosting.Tests
{
/// <summary>
/// Validates that health checks are properly registered after the refactoring.
/// </summary>
public class HealthCheckSpec : Akka.Hosting.TestKit.TestKit, IClassFixture<SqliteContainer>
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Validates that health checks actually get registered and run

{
private readonly SqliteContainer _fixture;

public HealthCheckSpec(ITestOutputHelper output, SqliteContainer fixture)
: base(nameof(HealthCheckSpec), output)
{
_fixture = fixture;

if (!_fixture.InitializeDbAsync().Wait(10.Seconds()))
throw new Exception("Failed to clean up database in 10 seconds");
}

protected override void ConfigureServices(HostBuilderContext context, IServiceCollection services)
{
base.ConfigureServices(context, services);
services.AddHealthChecks();
}

protected override void ConfigureAkka(AkkaConfigurationBuilder builder, IServiceProvider provider)
{
// Use the refactored WithSqlPersistence with health check registration
builder.WithSqlPersistence(
connectionString: _fixture.ConnectionString,
providerName: _fixture.ProviderName,
journalBuilder: journal =>
{
journal.WithHealthCheck(HealthStatus.Degraded);
},
snapshotBuilder: snapshot =>
{
snapshot.WithHealthCheck(HealthStatus.Degraded);
});
}

[Fact]
public async Task Health_checks_should_be_registered_and_healthy()
{
// Arrange
var healthCheckService = Host.Services.GetRequiredService<HealthCheckService>();

// Act - run all health checks
var healthReport = await healthCheckService.CheckHealthAsync(CancellationToken.None);

// Assert - verify that health checks are registered and healthy
healthReport.Entries.Should().NotBeEmpty("health checks should be registered");

// Debug: print all registered health checks (ALL of them, not just SQL)
Output?.WriteLine($"Total health checks registered: {healthReport.Entries.Count}");
foreach (var entry in healthReport.Entries)
{
Output?.WriteLine($" - {entry.Key}: {entry.Value.Status}");
}

// We should have exactly 2 health checks: journal and snapshot
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Validate that BOTH health checks (journal + snapshot store) run: this is what helped me find akkadotnet/Akka.Hosting#666

// Look for any Akka.Persistence-related health checks
var persistenceHealthChecks = healthReport.Entries
.Where(e => e.Key.Contains("Akka.Persistence", StringComparison.OrdinalIgnoreCase))
.ToList();

persistenceHealthChecks.Should().HaveCount(2,
"because we registered health checks for both journal and snapshot store");

// Verify journal health check exists and is healthy
var journalHealthCheck = persistenceHealthChecks
.FirstOrDefault(e => e.Key.Contains("journal", StringComparison.OrdinalIgnoreCase));

journalHealthCheck.Should().NotBeNull("journal health check should be registered");
journalHealthCheck.Value.Status.Should().Be(HealthStatus.Healthy,
"SQL journal should be properly initialized");

// Verify snapshot health check exists and is healthy
var snapshotHealthCheck = persistenceHealthChecks
.FirstOrDefault(e => e.Key.Contains("snapshot", StringComparison.OrdinalIgnoreCase));

snapshotHealthCheck.Should().NotBeNull("snapshot health check should be registered");
snapshotHealthCheck.Value.Status.Should().Be(HealthStatus.Healthy,
"SQL snapshot store should be properly initialized");

// Verify overall health status
healthReport.Status.Should().Be(HealthStatus.Healthy,
"because all health checks should pass");
}
}
}
Loading