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
478 changes: 478 additions & 0 deletions CONNECTIVITY_HEALTH_CHECKS_REPORT.md

Large diffs are not rendered by default.

29 changes: 24 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,12 +97,14 @@ var host = new HostBuilder()
```

The health checks will automatically:
- Verify the persistence plugin is configured correctly
- Test connectivity to the underlying storage (database, cloud storage, etc.)
- Report `Healthy` when the plugin is operational
- Report `Degraded` or `Unhealthy` (configurable) when issues are detected
- Verify connectivity to the underlying SQL database
- Test database responsiveness with a lightweight "SELECT 1" query
- Report `Healthy` when the database is accessible
- Report `Degraded` or `Unhealthy` (configurable) when the database is unreachable or unresponsive

Health checks are tagged with `akka`, `persistence`, and either `journal` or `snapshot-store` for filtering purposes.
Health checks are tagged with `akka`, `persistence`, and either `journal` or `snapshot-store` for filtering and organization purposes.

#### Exposing Health Checks via ASP.NET Core

For ASP.NET Core applications, you can expose these health checks via an endpoint:

Expand All @@ -129,6 +131,23 @@ app.MapHealthChecks("/healthz");
app.Run();
```

#### Customizing Health Check Tags

You can customize the tags applied to health checks by providing an `IEnumerable<string>` to the `WithHealthCheck()` method:

```csharp
journalBuilder: j => j.WithHealthCheck(
unHealthyStatus: HealthStatus.Degraded,
name: "sql-journal",
tags: new[] { "backend", "database", "sql" }),
snapshotBuilder: s => s.WithHealthCheck(
unHealthyStatus: HealthStatus.Degraded,
name: "sql-snapshot",
tags: new[] { "backend", "database", "sql" })
```

When tags are not specified, the default tags are used: `["akka", "persistence", "journal"]` for journals and `["akka", "persistence", "snapshot-store"]` for snapshot stores.

## The Classic Way, Using HOCON

These are the minimum HOCON configuration you need to start using Akka.Persistence.Sql:
Expand Down
5 changes: 5 additions & 0 deletions docs/articles/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ title: Features/Architecture
- Configuration
- Custom provider configurations are supported.
- Compatibility with existing Akka.Persistence plugins is implemented via `table-mapping` setting.
- Connectivity Health Checks (Akka.Hosting integration)
- Liveness checks that proactively verify database connectivity
- Performed via lightweight "SELECT 1" queries
- Configurable health status when database is unreachable
- Integrated with Microsoft.Extensions.Diagnostics.HealthChecks for ASP.NET Core applications

## Incomplete

Expand Down
118 changes: 118 additions & 0 deletions src/Akka.Persistence.Sql.Hosting.Tests/MySqlConnectivityCheckSpec.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
// -----------------------------------------------------------------------
// <copyright file="MySqlConnectivityCheckSpec.cs" company="Akka.NET Project">
// Copyright (C) 2013-2025 .NET Foundation <https://github.com/akkadotnet/akka.net>
// </copyright>
// -----------------------------------------------------------------------

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

namespace Akka.Persistence.Sql.Hosting.Tests
{
[SkipWindows]
public class MySqlConnectivityCheckSpec : IAsyncLifetime
{
private readonly MySqlContainer _container;
private readonly ITestOutputHelper _output;

public MySqlConnectivityCheckSpec(ITestOutputHelper output)
{
_output = output;
_container = new MySqlContainer();
}

public async Task InitializeAsync()
{
await _container.InitializeAsync();
}

public async Task DisposeAsync()
{
await _container.DisposeAsync();
}

[Fact]
public async Task Journal_Connectivity_Check_Should_Return_Healthy_When_Connected()
{
// Arrange
var check = new SqlJournalConnectivityCheck(
_container.ConnectionString,
_container.ProviderName,
"sql");

var context = new AkkaHealthCheckContext(null!);

// Act
var result = await check.CheckHealthAsync(context, CancellationToken.None);

// Assert
result.Status.Should().Be(HealthStatus.Healthy);
result.Description.Should().Contain("successful");
}

[Fact]
public async Task Journal_Connectivity_Check_Should_Return_Unhealthy_When_Disconnected()
{
// Arrange
var check = new SqlJournalConnectivityCheck(
"Server=invalid-host;User=invalid;Password=invalid",
_container.ProviderName,
"sql");

var context = new AkkaHealthCheckContext(null!);

// Act
var result = await check.CheckHealthAsync(context, CancellationToken.None);

// Assert
result.Status.Should().Be(HealthStatus.Unhealthy);
result.Exception.Should().NotBeNull();
}

[Fact]
public async Task Snapshot_Connectivity_Check_Should_Return_Healthy_When_Connected()
{
// Arrange
var check = new SqlSnapshotStoreConnectivityCheck(
_container.ConnectionString,
_container.ProviderName,
"sql");

var context = new AkkaHealthCheckContext(null!);

// Act
var result = await check.CheckHealthAsync(context, CancellationToken.None);

// Assert
result.Status.Should().Be(HealthStatus.Healthy);
result.Description.Should().Contain("successful");
}

[Fact]
public async Task Snapshot_Connectivity_Check_Should_Return_Unhealthy_When_Disconnected()
{
// Arrange
var check = new SqlSnapshotStoreConnectivityCheck(
"Server=invalid-host;User=invalid;Password=invalid",
_container.ProviderName,
"sql");

var context = new AkkaHealthCheckContext(null!);

// Act
var result = await check.CheckHealthAsync(context, CancellationToken.None);

// Assert
result.Status.Should().Be(HealthStatus.Unhealthy);
result.Exception.Should().NotBeNull();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
// -----------------------------------------------------------------------
// <copyright file="PostgreSqlConnectivityCheckSpec.cs" company="Akka.NET Project">
// Copyright (C) 2013-2025 .NET Foundation <https://github.com/akkadotnet/akka.net>
// </copyright>
// -----------------------------------------------------------------------

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

namespace Akka.Persistence.Sql.Hosting.Tests
{
[SkipWindows]
public class PostgreSqlConnectivityCheckSpec : IAsyncLifetime
{
private readonly PostgreSqlContainer _container;
private readonly ITestOutputHelper _output;

public PostgreSqlConnectivityCheckSpec(ITestOutputHelper output)
{
_output = output;
_container = new PostgreSqlContainer();
}

public async Task InitializeAsync()
{
await _container.InitializeAsync();
}

public async Task DisposeAsync()
{
await _container.DisposeAsync();
}

[Fact]
public async Task Journal_Connectivity_Check_Should_Return_Healthy_When_Connected()
{
// Arrange
var check = new SqlJournalConnectivityCheck(
_container.ConnectionString,
_container.ProviderName,
"sql");

var context = new AkkaHealthCheckContext(null!);

// Act
var result = await check.CheckHealthAsync(context, CancellationToken.None);

// Assert
result.Status.Should().Be(HealthStatus.Healthy);
result.Description.Should().Contain("successful");
}

[Fact]
public async Task Journal_Connectivity_Check_Should_Return_Unhealthy_When_Disconnected()
{
// Arrange
var check = new SqlJournalConnectivityCheck(
"Host=invalid-host;Username=invalid;Password=invalid",
_container.ProviderName,
"sql");

var context = new AkkaHealthCheckContext(null!);

// Act
var result = await check.CheckHealthAsync(context, CancellationToken.None);

// Assert
result.Status.Should().Be(HealthStatus.Unhealthy);
result.Exception.Should().NotBeNull();
}

[Fact]
public async Task Snapshot_Connectivity_Check_Should_Return_Healthy_When_Connected()
{
// Arrange
var check = new SqlSnapshotStoreConnectivityCheck(
_container.ConnectionString,
_container.ProviderName,
"sql");

var context = new AkkaHealthCheckContext(null!);

// Act
var result = await check.CheckHealthAsync(context, CancellationToken.None);

// Assert
result.Status.Should().Be(HealthStatus.Healthy);
result.Description.Should().Contain("successful");
}

[Fact]
public async Task Snapshot_Connectivity_Check_Should_Return_Unhealthy_When_Disconnected()
{
// Arrange
var check = new SqlSnapshotStoreConnectivityCheck(
"Host=invalid-host;Username=invalid;Password=invalid",
_container.ProviderName,
"sql");

var context = new AkkaHealthCheckContext(null!);

// Act
var result = await check.CheckHealthAsync(context, CancellationToken.None);

// Assert
result.Status.Should().Be(HealthStatus.Unhealthy);
result.Exception.Should().NotBeNull();
}
}
}
12 changes: 12 additions & 0 deletions src/Akka.Persistence.Sql.Hosting.Tests/Properties/AssemblyInfo.cs

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Needed this to ensure that we use the SqlFrameworkDiscoverer and skip Docker-based integration tests on Windows CI/CD

Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// -----------------------------------------------------------------------
// <copyright file="AssemblyInfo.cs" company="Akka.NET Project">
// Copyright (C) 2013-2025 .NET Foundation <https://github.com/akkadotnet/akka.net>
// </copyright>
// -----------------------------------------------------------------------

using Xunit;

[assembly:
TestFramework(
"Akka.Persistence.Sql.Tests.Common.Internal.Xunit.SqlTestFramework",
"Akka.Persistence.Sql.Tests.Common")]
Loading