mirror of
https://github.com/MAKS-IT-COM/maksit-hamode.git
synced 2026-06-30 22:36:42 +02:00
8.4 KiB
8.4 KiB
MaksIT.HAMode
Reusable high-availability runtime coordination library for MaksIT services.
Packages
MaksIT.HAMode(single NuGet package)MaksIT.HAMode.Extensions.ServiceCollectionExtensionsMaksIT.HAMode.Abstractionsnamespace:IRuntimeInstanceIdIRuntimeLeaseServiceIRuntimeLeaseConnectionStringProviderIRuntimeLeaseRedisConnectionProviderIRuntimeLeaseEtcdConnectionProviderRuntimeInstanceIdProvider
MaksIT.HAMode.PostgreSql.RuntimeLeaseServiceNpgsqlMaksIT.HAMode.Redis.RuntimeLeaseServiceRedisMaksIT.HAMode.Etcd.RuntimeLeaseServiceEtcd
Target framework
net10.0
Design goals
- Keep runtime coordination reusable across products.
- Keep application configuration ownership in host projects.
- Keep lease persistence implementation swappable and SOLID-compliant.
PostgreSQL lease table contract
RuntimeLeaseServiceNpgsql expects:
- table:
public.app_runtime_leases - columns:
lease_name(text, PK)holder_id(text)version(bigint)acquired_at_utc(timestamptz)expires_at_utc(timestamptz)
Usage examples
Install package
<ItemGroup>
<PackageReference Include="MaksIT.HAMode" Version="1.0.1" />
</ItemGroup>
Shared runtime instance id
using MaksIT.HAMode.Extensions;
builder.Services.AddHAModeRuntimeInstanceId();
PostgreSQL backend
using MaksIT.HAMode.Abstractions;
using MaksIT.HAMode.Extensions;
public sealed class MyPgLeaseConnectionProvider(IConfiguration cfg) : IRuntimeLeaseConnectionStringProvider {
public string ConnectionString => cfg["Configuration:Engine:ConnectionString"]!;
}
builder.Services.AddHAModePostgreSql<MyPgLeaseConnectionProvider>();
Redis backend
using MaksIT.HAMode.Abstractions;
using MaksIT.HAMode.Extensions;
public sealed class MyRedisLeaseConnectionProvider(IConfiguration cfg) : IRuntimeLeaseRedisConnectionProvider {
public string Configuration => cfg["Configuration:Redis:ConnectionString"]!;
public string KeyPrefix => "my-app/runtime-leases:";
}
builder.Services.AddHAModeRedis<MyRedisLeaseConnectionProvider>();
etcd backend
using MaksIT.HAMode.Abstractions;
using MaksIT.HAMode.Extensions;
public sealed class MyEtcdLeaseConnectionProvider(IConfiguration cfg) : IRuntimeLeaseEtcdConnectionProvider {
public string Endpoints => cfg["Configuration:Etcd:Endpoints"]!; // ex: http://etcd:2379
public string? Username => cfg["Configuration:Etcd:Username"];
public string? Password => cfg["Configuration:Etcd:Password"];
public string KeyPrefix => "my-app/runtime-leases/";
}
builder.Services.AddHAModeEtcd<MyEtcdLeaseConnectionProvider>();
Runtime acquire/release flow
using MaksIT.HAMode.Abstractions;
public sealed class BootstrapHostedService(
IRuntimeLeaseService leaseService,
IRuntimeInstanceId runtimeInstance,
ILogger<BootstrapHostedService> logger
) : IHostedService {
public async Task StartAsync(CancellationToken cancellationToken) {
var holder = runtimeInstance.InstanceId;
var acquired = await leaseService.TryAcquireAsync(
leaseName: "my-app-bootstrap",
holderId: holder,
ttl: TimeSpan.FromSeconds(30),
cancellationToken: cancellationToken);
if (!acquired.IsSuccess || !acquired.Value) {
logger.LogInformation("Another replica owns bootstrap lease.");
return;
}
try {
// Run single-replica bootstrap logic here.
}
finally {
await leaseService.ReleaseAsync("my-app-bootstrap", holder, CancellationToken.None);
}
}
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}
Integration in MaksIT.Vault
1) Add package reference
In MaksIT.Vault.Engine.csproj:
<ItemGroup>
<PackageReference Include="MaksIT.HAMode" Version="1.0.1" />
</ItemGroup>
Switching backend uses a different service registration (no package change needed).
2) Keep Vault interfaces stable with adapters
// Vault Engine contract remains unchanged for consumers.
using SharedRuntimeLeaseService = MaksIT.HAMode.Abstractions.IRuntimeLeaseService;
namespace MaksIT.Vault.Engine.Infrastructure;
public interface IRuntimeLeaseService : SharedRuntimeLeaseService;
// Vault host contract remains unchanged for controllers/hosted services.
namespace MaksIT.Vault.Engine.RuntimeCoordination;
public interface IRuntimeInstanceId : MaksIT.HAMode.Abstractions.IRuntimeInstanceId;
3) Adapter for connection/config ownership
using MaksIT.HAMode.Abstractions;
using SharedRuntimeLeaseServiceNpgsql = MaksIT.HAMode.PostgreSql.RuntimeLeaseServiceNpgsql;
public sealed class RuntimeLeaseServiceNpgsql(
IVaultEngineConfiguration config,
ILogger<SharedRuntimeLeaseServiceNpgsql> logger
) : IRuntimeLeaseService {
private sealed class VaultLeaseConnection(IVaultEngineConfiguration cfg) : IRuntimeLeaseConnectionStringProvider {
public string ConnectionString => cfg.ConnectionString;
}
private readonly SharedRuntimeLeaseServiceNpgsql _inner = new(new VaultLeaseConnection(config), logger);
public Task<MaksIT.Results.Result<bool>> TryAcquireAsync(string leaseName, string holderId, TimeSpan ttl, CancellationToken ct = default) =>
_inner.TryAcquireAsync(leaseName, holderId, ttl, ct);
public Task<MaksIT.Results.Result> ReleaseAsync(string leaseName, string holderId, CancellationToken ct = default) =>
_inner.ReleaseAsync(leaseName, holderId, ct);
}
4) DI registration in Program.cs
builder.Services.AddSingleton<MaksIT.Vault.Engine.RuntimeCoordination.IRuntimeInstanceId, RuntimeInstanceIdProvider>();
builder.Services.AddSingleton<MaksIT.Vault.Engine.Infrastructure.IRuntimeLeaseService, RuntimeLeaseServiceNpgsql>();
Integration in MaksIT.CertsUI
The same pattern applies to MaksIT.CertsUI.Engine.
1) Add package reference
In MaksIT.CertsUI.Engine.csproj:
<ItemGroup>
<PackageReference Include="MaksIT.HAMode" Version="1.0.1" />
</ItemGroup>
2) Keep CertsUI interfaces stable with adapters
using SharedRuntimeLeaseService = MaksIT.HAMode.Abstractions.IRuntimeLeaseService;
namespace MaksIT.CertsUI.Engine.Infrastructure;
public interface IRuntimeLeaseService : SharedRuntimeLeaseService;
namespace MaksIT.CertsUI.Engine.RuntimeCoordination;
public interface IRuntimeInstanceId : MaksIT.HAMode.Abstractions.IRuntimeInstanceId;
3) Adapter for CertsUI configuration
using MaksIT.HAMode.Abstractions;
using SharedRuntimeLeaseServiceNpgsql = MaksIT.HAMode.PostgreSql.RuntimeLeaseServiceNpgsql;
public sealed class RuntimeLeaseServiceNpgsql(
ICertsEngineConfiguration config,
ILogger<SharedRuntimeLeaseServiceNpgsql> logger
) : IRuntimeLeaseService {
private sealed class CertsLeaseConnection(ICertsEngineConfiguration cfg) : IRuntimeLeaseConnectionStringProvider {
public string ConnectionString => cfg.ConnectionString;
}
private readonly SharedRuntimeLeaseServiceNpgsql _inner = new(new CertsLeaseConnection(config), logger);
public Task<MaksIT.Results.Result<bool>> TryAcquireAsync(string leaseName, string holderId, TimeSpan ttl, CancellationToken ct = default) =>
_inner.TryAcquireAsync(leaseName, holderId, ttl, ct);
public Task<MaksIT.Results.Result> ReleaseAsync(string leaseName, string holderId, CancellationToken ct = default) =>
_inner.ReleaseAsync(leaseName, holderId, ct);
}
4) DI registration in Program.cs
builder.Services.AddSingleton<MaksIT.CertsUI.Engine.RuntimeCoordination.IRuntimeInstanceId, RuntimeInstanceIdProvider>();
builder.Services.AddSingleton<MaksIT.CertsUI.Engine.Infrastructure.IRuntimeLeaseService, RuntimeLeaseServiceNpgsql>();
Backend switching strategy
Use the same app-level interface (IRuntimeLeaseService) and swap only adapter implementation:
- PostgreSQL:
MaksIT.HAMode.PostgreSql.RuntimeLeaseServiceNpgsql - Redis:
MaksIT.HAMode.Redis.RuntimeLeaseServiceRedis - etcd:
MaksIT.HAMode.Etcd.RuntimeLeaseServiceEtcd
This keeps hosted services and domain workflows unchanged in both maksit-vault and maksit-certs-ui.
Local pack
dotnet pack .\src\MaksIT.HAMode.slnx -c Release
The command emits a single MaksIT.HAMode .nupkg and .snupkg.