MaksIT.HAMode
1.0.2
See the version list below for details.
dotnet add package MaksIT.HAMode --version 1.0.2
NuGet\Install-Package MaksIT.HAMode -Version 1.0.2
<PackageReference Include="MaksIT.HAMode" Version="1.0.2" />
<PackageVersion Include="MaksIT.HAMode" Version="1.0.2" />
<PackageReference Include="MaksIT.HAMode" />
paket add MaksIT.HAMode --version 1.0.2
#r "nuget: MaksIT.HAMode, 1.0.2"
#:package MaksIT.HAMode@1.0.2
#addin nuget:?package=MaksIT.HAMode&version=1.0.2
#tool nuget:?package=MaksIT.HAMode&version=1.0.2
MaksIT.HAMode
Reusable high-availability runtime coordination library for MaksIT services.
Packages
MaksIT.HAMode(single NuGet package)MaksIT.HAMode.Extensions.ServiceCollectionExtensionsMaksIT.HAMode.Abstractionsnamespace:IRuntimeInstanceIdIRuntimeLeaseServiceIRuntimeLeaseConnectionProvider(root marker interface)IRuntimeLeaseConnectionStringProviderIRuntimeLeaseRedisConnectionProviderIRuntimeLeaseEtcdConnectionProviderRuntimeInstanceIdProvider
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: configurable via
IRuntimeLeaseConnectionStringProvider.SchemaandIRuntimeLeaseConnectionStringProvider.Table- defaults:
public.app_runtime_leases
- defaults:
- columns:
lease_name(text, PK)holder_id(text)version(bigint)acquired_at_utc(timestamptz)expires_at_utc(timestamptz)
If the configured table does not exist, PostgreSQL lease operations return an explicit internal error explaining which table is missing.
Usage examples
Install package
<ItemGroup>
<PackageReference Include="MaksIT.HAMode" />
</ItemGroup>
Shared runtime instance id
using MaksIT.HAMode.Extensions;
builder.Services.AddHAModeRuntimeInstanceId();
PostgreSQL backend
using MaksIT.HAMode.Abstractions;
using MaksIT.HAMode.Extensions;
// Host project contract.
public interface IMyPgLeaseConfiguration : IRuntimeLeaseConnectionStringProvider;
// Host project concrete configuration.
public sealed class MyPgLeaseConfiguration : IMyPgLeaseConfiguration {
public required string ConnectionString { get; init; }
public string Schema { get; init; } = "ha";
public string Table { get; init; } = "runtime_leases";
}
IMyPgLeaseConfiguration pgConfiguration = new MyPgLeaseConfiguration {
ConnectionString = "<your-connection-string>"
};
builder.Services.AddHAModePostgreSql(pgConfiguration);
If you already manage a pooled PostgreSQL client in the host, pass the shared NpgsqlDataSource:
var dataSource = new NpgsqlDataSourceBuilder("<your-connection-string>").Build();
builder.Services.AddHAModePostgreSql(pgConfiguration, dataSource);
Redis backend
using MaksIT.HAMode.Abstractions;
using MaksIT.HAMode.Extensions;
// Host project contract.
public interface IMyRedisLeaseConfiguration : IRuntimeLeaseRedisConnectionProvider;
// Host project concrete configuration.
public sealed class MyRedisLeaseConfiguration : IMyRedisLeaseConfiguration {
public required string Configuration { get; init; }
public string KeyPrefix { get; init; } = "my-app/runtime-leases:";
}
IMyRedisLeaseConfiguration redisConfiguration = new MyRedisLeaseConfiguration {
Configuration = "<your-redis-connection-string>"
};
builder.Services.AddHAModeRedis(redisConfiguration);
If you already manage a shared Redis client in the host, pass the same IConnectionMultiplexer:
var multiplexer = await ConnectionMultiplexer.ConnectAsync("<your-redis-connection-string>");
builder.Services.AddHAModeRedis(redisConfiguration, multiplexer);
Redis is schema-less, so there is no table bootstrap requirement; lease keys are isolated by KeyPrefix.
etcd backend
using MaksIT.HAMode.Abstractions;
using MaksIT.HAMode.Extensions;
// Host project contract.
public interface IMyEtcdLeaseConfiguration : IRuntimeLeaseEtcdConnectionProvider;
// Host project concrete configuration.
public sealed class MyEtcdLeaseConfiguration : IMyEtcdLeaseConfiguration {
public required string Endpoints { get; init; } // ex: http://etcd:2379
public string? Username { get; init; }
public string? Password { get; init; }
public string KeyPrefix { get; init; } = "my-app/runtime-leases/";
}
IMyEtcdLeaseConfiguration etcdConfiguration = new MyEtcdLeaseConfiguration {
Endpoints = "http://etcd:2379",
Username = null,
Password = null
};
builder.Services.AddHAModeEtcd(etcdConfiguration);
If you already manage a shared etcd client in the host, pass the same EtcdClient:
var etcdClient = new EtcdClient("http://etcd:2379");
builder.Services.AddHAModeEtcd(etcdConfiguration, etcdClient);
etcd is key-space based, so there is no table bootstrap requirement; lease keys are isolated by KeyPrefix.
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" />
</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" />
</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.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- dotnet-etcd (>= 8.1.0)
- Google.Protobuf (>= 3.35.1)
- MaksIT.Results (>= 2.0.2)
- Microsoft.Extensions.Logging.Abstractions (>= 11.0.0-preview.5.26302.115)
- Npgsql (>= 10.0.3)
- StackExchange.Redis (>= 3.0.47-preview)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
See CHANGELOG.md in the package and repository releases.