Přeskočit na hlavní obsah

Distributed

Overview

The distributed packages provide shared primitives for applications that run in more than one process or node.

ASOL.Core.Distributed is provider-agnostic. It contains the common distributed foundation, instance identity, coordination abstractions, lock renewal, shared invalidation versioning, and TTL marker contracts.

ASOL.Core.Distributed.Redis contains the Redis-backed implementations and the shared Redis connection provider. Redis-specific connection lifecycle, retry, timeout, and health check configuration live here instead of in feature-specific packages such as distributed caching.

The package name Distributed is intentional. Coordination is one functional area in the package, but the public surface is wider than locks: it also includes instance identity, shared invalidation/versioning, and TTL marker concepts. A Coordination package name would be narrower than the actual shared distributed primitives and would make future non-coordination primitives harder to place. Provider-specific implementation starts in ASOL.Core.Distributed.Redis, so the naming boundary is Distributed for the general foundation and Distributed.Redis for Redis-backed services.

Install

  • ASOL.Core.Distributed - provider-agnostic distributed primitives and abstractions.
  • ASOL.Core.Distributed.Redis - Redis connection provider and Redis-backed distributed coordination implementations.

Third-party libraries (NuGet)

Dependency injection

Register only the provider-agnostic foundation when the application needs instance identity or shared local services without Redis:

services.AddDistributedFoundation(configuration); // namespace: ASOL.Core.Distributed.Extensions

Register provider-agnostic coordination services without Redis implementations:

services.AddDistributedCoordination(configuration); // namespace: ASOL.Core.Distributed.Extensions

Register only the shared Redis connection provider:

services.AddRedisConnectionProvider(configuration); // namespace: ASOL.Core.Distributed.Redis.Extensions

Register Redis-backed distributed coordination:

services.AddRedisDistributedCoordination(configuration); // namespace: ASOL.Core.Distributed.Redis.Extensions

Register Redis readiness when the application depends on Redis for remote cache or distributed coordination:

services.AddHealthChecks()
.AddRedisConnectionHealthCheck(); // namespace: ASOL.Core.Distributed.Redis.Extensions

Do not register the Redis health check for MemoryCache-only distributed cache usage.

Public services and use cases

Use IInstanceIdentityProvider when code needs to identify the current process or node in a multi-instance deployment. Typical use cases are owner identifiers for distributed operations, diagnostics, telemetry enrichment, or any logic that needs to distinguish one running instance from another without hard-coding machine-specific values.

Use IDistributedLockService when only one application instance may execute a block of work at a time. This is the primary API for preventing race conditions in multi-instance applications, for example background jobs, scheduled synchronization, one-off migrations, import processing, cache rebuilds, or any workflow where concurrent execution by several nodes could produce duplicate work or inconsistent state.

Use IDistributedLockRenewalService when the protected operation can take longer than the initial lock lease. It keeps the acquired lock alive while the operation runs, so long-running work does not accidentally release the lock before it finishes. Short operations can usually acquire and release the lock directly; long-running operations should wrap the protected callback with renewal.

Use ISharedInvalidationVersionService when several application nodes must agree on the current version of a shared dependency, cache, projection, or computed data set. Readers can compare stored versions with the current shared version, while writers can bump the version after changing the underlying data. This is useful when local in-memory state exists on several nodes but invalidation must be coordinated globally.

Use IInvalidationStampService when lower-level code needs direct access to numeric invalidation stamps. Application code should usually prefer ISharedInvalidationVersionService, because it wraps the stamp together with the dependency identity and is easier to use consistently.

Use ITtlMarkerService when the application needs a distributed marker with automatic expiration. Common use cases are short-lived idempotency markers, cooldown windows, temporary processing flags, duplicate suppression, and "already scheduled" signals shared by all nodes.

Use IRedisConnectionProvider when a Redis-backed component needs direct access to the shared StackExchange.Redis connection or database. Application feature code should prefer the higher-level distributed services when they match the use case; use the provider directly only for Redis-specific integration code.

Use AddRedisConnectionHealthCheck when Redis is required for application readiness, for example when the application uses RemoteCache or Redis-backed coordination. Do not register it for configurations that use only local memory cache and do not require Redis at runtime.

Application code should usually depend on the interfaces and registration extension methods rather than concrete implementation classes. Concrete classes such as RedisDistributedLockService, RedisInvalidationStampService, RedisTtlMarkerService, RedisConnectionProvider, and RedisConnectionHealthCheck are public so they can participate in dependency injection, health checks, tests, and provider-specific composition.

Supporting public types model inputs and outputs for these services:

  • DistributedLockAcquireOptions controls lock lease duration, wait timeout, and retry delay for an acquire attempt.
  • DistributedLockAcquireResult tells the caller whether the lock was acquired and carries the DistributedLockHandle when it was.
  • DistributedLockHandle represents the acquired distributed lock, including owner token, key, acquisition time, and expiration.
  • SharedInvalidationDependency identifies the shared dependency whose version should be read or bumped.
  • SharedInvalidationVersion combines a dependency identity with its current numeric stamp.
  • CoordinationKeyBuilder builds stable provider keys from application code, primitive type, namespace, and scope segments. Application feature code rarely needs it directly; provider and infrastructure code use it to keep Redis key format consistent.

Configuration

InstanceIdentity identifies the current process or node. If InstanceId is not configured, the default provider builds one from machine and process information.

{
"InstanceIdentity": {
"InstanceId": "idm-node-01"
}
}

DistributedCoordination contains provider-agnostic coordination settings:

{
"DistributedCoordination": {
"ApplicationCode": "ASOLEU-IDM-AP-",
"DefaultLockLeaseDuration": "00:00:30",
"DefaultLockWaitTimeout": "00:00:00",
"DefaultLockRetryDelay": "00:00:00.100"
}
}

RedisConnection contains Redis connection settings shared by Redis-backed Core packages:

{
"RedisConnection": {
"ConnectionString": "localhost:6379",
"ConnectRetryCount": 5,
"InitConnectRetryDelay": 5,
"ConnectTimeoutMs": 500,
"SyncTimeoutMs": 5000,
"HealthCheckMode": "Enabled"
}
}

Usage

Instance identity

Use IInstanceIdentityProvider when a multi-instance application must include the current node identity in diagnostics, telemetry, ownership, or heartbeat payloads. The value is stable for the provider instance and can be configured explicitly or resolved from runtime environment.

General pattern:

public class NodeProbe(
IInstanceIdentityProvider instanceIdentityProvider,
INodeStatusReporter nodeStatusReporter)
{
public Task PingAsync(CancellationToken cancellationToken)
{
var instanceId = instanceIdentityProvider.InstanceId;

return nodeStatusReporter.ReportAsync(instanceId, cancellationToken);
}
}

Example use case:

public class NodeHeartbeatPublisher(
IInstanceIdentityProvider instanceIdentityProvider,
INodeHeartbeatClient heartbeatClient)
{
public Task PingAsync(CancellationToken cancellationToken)
{
return heartbeatClient.ReportAsync(
new NodeHeartbeat(instanceIdentityProvider.InstanceId, DateTimeOffset.UtcNow),
cancellationToken);
}
}

Direct Redis access

Use IRedisConnectionProvider only when a component needs direct Redis access. Application feature code should prefer higher-level distributed services when they fit the use case. Direct Redis access is appropriate for provider-specific infrastructure code, custom diagnostics, or Redis features that are not represented by the shared abstractions.

General pattern:

public class RedisProbe(IRedisConnectionProvider redisConnectionProvider)
{
public async Task PingAsync(CancellationToken cancellationToken)
{
var database = await redisConnectionProvider.GetDatabaseAsync(cancellationToken);

await database.PingAsync();
}
}

Example use case:

public class RedisDiagnostics(IRedisConnectionProvider redisConnectionProvider)
{
public async Task<TimeSpan> MeasureRedisLatencyAsync(CancellationToken cancellationToken)
{
var database = await redisConnectionProvider.GetDatabaseAsync(cancellationToken);

return await database.PingAsync();
}
}

Distributed locks

Use IDistributedLockService together with IDistributedLockRenewalService for operations that must be protected across application nodes. A typical real use case is running database schema migrations during application startup in a multi-node deployment, where only one instance may apply pending migrations and seed reference data.

General pattern:

public class DistributedOperationRunner(
IDistributedLockService lockService,
IDistributedLockRenewalService renewalService)
{
public async Task RunOnceAcrossNodesAsync(
Func<CancellationToken, Task> protectedOperation,
CancellationToken cancellationToken)
{
var result = await lockService.AcquireAsync(
"startup",
["schema-migration"],
new DistributedLockAcquireOptions
{
LeaseDuration = TimeSpan.FromSeconds(30),
WaitTimeout = TimeSpan.Zero,
RetryDelay = TimeSpan.FromMilliseconds(100)
},
cancellationToken);

if (!result.IsAcquired || result.Handle is null)
{
return;
}

try
{
await renewalService.ExecuteWithRenewalAsync(
result.Handle,
result.Handle.LeaseDuration,
TimeSpan.FromSeconds(10),
protectedOperation,
cancellationToken);
}
finally
{
await lockService.ReleaseAsync(result.Handle, cancellationToken);
}
}
}

Example use case:

private async Task RunProtectedMigrationsAsync(CancellationToken cancellationToken)
{
await schemaMigrator.ApplyPendingSchemaMigrationsAsync(cancellationToken);
await schemaMigrator.EnsureReferenceDataAsync(cancellationToken);
}

Shared invalidation versioning

Use ISharedInvalidationVersionService when several nodes cache or compute data from the same dependency and must agree on the current invalidation version. Readers include the current version in their local cache key; writers bump the version after changing the dependency.

General pattern:

public class ProjectionInvalidation(ISharedInvalidationVersionService invalidationVersionService)
{
public Task<SharedInvalidationVersion> GetCurrentVersionAsync(CancellationToken cancellationToken)
{
return invalidationVersionService.GetVersionAsync(
new SharedInvalidationDependency("authorization", ["permissions"]),
cancellationToken);
}
}

Example use case:

public async Task<string> BuildPermissionsCacheKeyAsync(
string roleId,
CancellationToken cancellationToken)
{
var version = await projectionInvalidation.GetCurrentVersionAsync(cancellationToken);

return SharedInvalidationVersionedCacheKeyBuilder.Build(
$"authorization:permissions:{roleId}",
[version]);
}

TTL markers

Use ITtlMarkerService when several nodes need to share a short-lived marker. This is useful for idempotency windows, duplicate suppression, cooldowns, or "already scheduled" signals.

General pattern:

public async Task<bool> TryStartOnceAsync(CancellationToken cancellationToken)
{
return await ttlMarkerService.TryCreateAsync(
"processing",
["daily-summary"],
TimeSpan.FromMinutes(10),
cancellationToken);
}

Example use case:

public async Task<bool> TryQueuePasswordResetEmailAsync(
string userId,
CancellationToken cancellationToken)
{
var created = await ttlMarkerService.TryCreateAsync(
"notifications",
["password-reset", userId],
TimeSpan.FromMinutes(5),
cancellationToken);

if (!created)
{
return false;
}

await passwordResetEmailQueue.EnqueueAsync(userId, cancellationToken);
return true;
}

Registration example

Use Redis-backed coordination and Redis readiness when the application depends on Redis for distributed coordination at runtime:

services.AddRedisDistributedCoordination(configuration);

services.AddPlatformHealthChecks<Startup>(configuration)
.AddRedisConnectionHealthCheck();