Excalibur.Dispatch.LeaderElection.Abstractions 3.0.0-alpha.19

This is a prerelease version of Excalibur.Dispatch.LeaderElection.Abstractions.
dotnet add package Excalibur.Dispatch.LeaderElection.Abstractions --version 3.0.0-alpha.19
                    
NuGet\Install-Package Excalibur.Dispatch.LeaderElection.Abstractions -Version 3.0.0-alpha.19
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Excalibur.Dispatch.LeaderElection.Abstractions" Version="3.0.0-alpha.19" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Excalibur.Dispatch.LeaderElection.Abstractions" Version="3.0.0-alpha.19" />
                    
Directory.Packages.props
<PackageReference Include="Excalibur.Dispatch.LeaderElection.Abstractions" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Excalibur.Dispatch.LeaderElection.Abstractions --version 3.0.0-alpha.19
                    
#r "nuget: Excalibur.Dispatch.LeaderElection.Abstractions, 3.0.0-alpha.19"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Excalibur.Dispatch.LeaderElection.Abstractions@3.0.0-alpha.19
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Excalibur.Dispatch.LeaderElection.Abstractions&version=3.0.0-alpha.19&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=Excalibur.Dispatch.LeaderElection.Abstractions&version=3.0.0-alpha.19&prerelease
                    
Install as a Cake Tool

Excalibur.Dispatch.LeaderElection.Abstractions

Core abstractions for distributed leader election in .NET 9 applications.

Overview

This package provides the foundational interfaces and types for implementing leader election patterns in distributed systems. It contains no implementation - install a provider package (Consul, Kubernetes, or InMemory) for concrete functionality.

Key Features:

  • Provider-agnostic - Define your logic once, swap providers without code changes
  • Event-driven - React to leadership changes via events
  • Health-aware - Optional health-based leadership with automatic step-down
  • AOT Compatible - Full Native AOT support for trimmed, ahead-of-time compiled deployments
  • Zero Dependencies - No external dependencies beyond .NET 9 base libraries

Installation

dotnet add package Excalibur.Dispatch.LeaderElection.Abstractions

This package alone provides no functionality. You must also install a provider:

Choose a Provider

Provider Package Use Case AOT Support
Consul Excalibur.Dispatch.LeaderElection.Consul Production deployments with HashiCorp Consul ❌ No
Kubernetes Excalibur.Dispatch.LeaderElection.Kubernetes Cloud-native apps running in Kubernetes clusters ❌ No
InMemory Excalibur.Dispatch.LeaderElection.InMemory Unit/integration testing, development ✅ Yes

Example: Install Consul provider

dotnet add package Excalibur.Dispatch.LeaderElection.Abstractions
dotnet add package Excalibur.Dispatch.LeaderElection.Consul

Core Interfaces

ILeaderElection

The primary interface for participating in leader election.

public interface ILeaderElection
{
    // Events
    event EventHandler<LeaderElectionEventArgs>? OnBecameLeader;
    event EventHandler<LeaderElectionEventArgs>? OnLostLeadership;
    event EventHandler<LeaderChangedEventArgs>? OnLeaderChanged;

    // Properties
    string CandidateId { get; }
    bool IsLeader { get; }
    string? CurrentLeaderId { get; }

    // Methods
    Task StartAsync(CancellationToken cancellationToken = default);
    Task StopAsync(CancellationToken cancellationToken = default);
}

Usage Pattern:

public class MyService : BackgroundService
{
    private readonly ILeaderElection _election;

    public MyService(ILeaderElectionFactory factory)
    {
        _election = factory.CreateElection("my-resource");
        _election.OnBecameLeader += OnBecameLeader;
        _election.OnLostLeadership += OnLostLeadership;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        await _election.StartAsync(stoppingToken);

        while (!stoppingToken.IsCancellationRequested)
        {
            if (_election.IsLeader)
            {
                // Perform leader-only work
                await DoLeaderWorkAsync();
            }
            else
            {
                // Perform follower work (or idle)
                await Task.Delay(1000, stoppingToken);
            }
        }

        await _election.StopAsync();
    }

    private void OnBecameLeader(object? sender, LeaderElectionEventArgs e)
    {
        Console.WriteLine($"This instance ({e.CandidateId}) became the leader!");
        // Initialize leader-specific resources
    }

    private void OnLostLeadership(object? sender, LeaderElectionEventArgs e)
    {
        Console.WriteLine($"This instance ({e.CandidateId}) lost leadership");
        // Clean up leader-specific resources
    }

    private async Task DoLeaderWorkAsync()
    {
        // Example: Process pending jobs, update aggregates, etc.
        await Task.Delay(100);
    }
}

IHealthBasedLeaderElection

Extends leader election with health awareness - leaders can step down when unhealthy.

public interface IHealthBasedLeaderElection : ILeaderElection
{
    Task UpdateHealthAsync(bool isHealthy, IDictionary<string, string>? metadata = null);
    Task<IEnumerable<CandidateHealth>> GetCandidateHealthAsync(CancellationToken cancellationToken = default);
}

Usage Pattern:

public class HealthMonitoredService : BackgroundService
{
    private readonly IHealthBasedLeaderElection _election;

    public HealthMonitoredService(ILeaderElectionFactory factory)
    {
        _election = factory.CreateHealthBasedElection("health-aware-resource");
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        await _election.StartAsync(stoppingToken);

        while (!stoppingToken.IsCancellationRequested)
        {
            // Continuously monitor health
            bool isHealthy = await CheckApplicationHealthAsync();

            // Update health status with metadata
            await _election.UpdateHealthAsync(isHealthy, new Dictionary<string, string>
            {
                ["cpu_usage"] = "65%",
                ["memory_usage"] = "80%",
                ["disk_space"] = "healthy"
            });

            // If unhealthy and leader, will automatically step down
            if (!isHealthy && _election.IsLeader)
            {
                Console.WriteLine("Unhealthy leader - stepping down automatically");
            }

            await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
        }
    }

    private async Task<bool> CheckApplicationHealthAsync()
    {
        // Implement health checks
        // - CPU usage < 90%
        // - Memory usage < 90%
        // - Disk space available
        // - Database connectivity
        return true; // Example
    }
}

ILeaderElectionFactory

Factory for creating leader election instances.

public interface ILeaderElectionFactory
{
    ILeaderElection CreateElection(string resourceName, string? candidateId = null);
    IHealthBasedLeaderElection CreateHealthBasedElection(string resourceName, string? candidateId = null);
}

Dependency Injection:

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        // Provider registers ILeaderElectionFactory
        services.AddConsulLeaderElection(/* options */);

        // Inject factory to create elections
        services.AddSingleton(sp =>
        {
            var factory = sp.GetRequiredService<ILeaderElectionFactory>();
            return factory.CreateElection("my-resource-lock");
        });
    }
}

Configuration

LeaderElectionOptions

Base configuration for all leader election implementations.

public class LeaderElectionOptions
{
    // Core timing configuration
    public TimeSpan LeaseDuration { get; set; } = TimeSpan.FromSeconds(15);
    public TimeSpan RenewInterval { get; set; } = TimeSpan.FromSeconds(5);
    public TimeSpan RetryInterval { get; set; } = TimeSpan.FromSeconds(2);
    public TimeSpan GracePeriod { get; set; } = TimeSpan.FromSeconds(5);

    // Identity
    public string InstanceId { get; set; } = Environment.MachineName;
    public IDictionary<string, string> CandidateMetadata { get; }

    // Health-based election settings
    public bool EnableHealthChecks { get; set; } = true;
    public double MinimumHealthScore { get; set; } = 0.8;
    public bool StepDownWhenUnhealthy { get; set; } = true;
}

Configuration Example:

services.AddConsulLeaderElection(options =>
{
    // Lease expires after 30 seconds of no renewal
    options.LeaseDuration = TimeSpan.FromSeconds(30);

    // Renew lease every 10 seconds
    options.RenewInterval = TimeSpan.FromSeconds(10);

    // Retry acquiring leadership every 5 seconds
    options.RetryInterval = TimeSpan.FromSeconds(5);

    // Grace period before declaring leader dead
    options.GracePeriod = TimeSpan.FromSeconds(5);

    // Unique identifier for this instance
    options.InstanceId = $"{Environment.MachineName}-{Guid.NewGuid()}";

    // Enable health-based leadership
    options.EnableHealthChecks = true;
    options.MinimumHealthScore = 0.75; // 75% health minimum
    options.StepDownWhenUnhealthy = true; // Auto step-down when unhealthy

    // Metadata visible to other candidates
    options.CandidateMetadata["version"] = "1.0.0";
    options.CandidateMetadata["region"] = "us-east-1";
});

Event Handling

Leadership Events

public class LeaderAwareService
{
    private readonly ILeaderElection _election;

    public LeaderAwareService(ILeaderElectionFactory factory)
    {
        _election = factory.CreateElection("my-resource");

        // Subscribe to all events
        _election.OnBecameLeader += OnBecameLeader;
        _election.OnLostLeadership += OnLostLeadership;
        _election.OnLeaderChanged += OnLeaderChanged;
    }

    private void OnBecameLeader(object? sender, LeaderElectionEventArgs e)
    {
        // This instance acquired leadership
        Console.WriteLine($"✓ Became leader: {e.CandidateId} at {e.Timestamp}");

        // Initialize leader-only resources
        // - Start scheduled jobs
        // - Acquire exclusive locks
        // - Begin processing work queues
    }

    private void OnLostLeadership(object? sender, LeaderElectionEventArgs e)
    {
        // This instance lost leadership (lease expired, stepped down, etc.)
        Console.WriteLine($"✗ Lost leadership: {e.CandidateId} at {e.Timestamp}");

        // Clean up leader-only resources
        // - Stop scheduled jobs
        // - Release exclusive locks
        // - Drain work queues gracefully
    }

    private void OnLeaderChanged(object? sender, LeaderChangedEventArgs e)
    {
        // Any candidate detected a leadership change
        Console.WriteLine($"Leader changed: {e.OldLeaderId ?? "none"} → {e.NewLeaderId ?? "none"}");

        // Update routing, caches, or external monitoring
    }
}

Common Patterns

Pattern 1: Single Leader for Singleton Work

Ensure only one instance performs a task (e.g., scheduled job, cleanup, aggregation).

public class SingletonJobService : BackgroundService
{
    private readonly ILeaderElection _election;

    public SingletonJobService(ILeaderElectionFactory factory)
    {
        _election = factory.CreateElection("singleton-job");
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        await _election.StartAsync(stoppingToken);

        while (!stoppingToken.IsCancellationRequested)
        {
            if (_election.IsLeader)
            {
                // Only the leader processes this job
                await ProcessJobAsync();
            }

            await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
        }

        await _election.StopAsync();
    }

    private async Task ProcessJobAsync()
    {
        Console.WriteLine("Leader processing job...");
        // Job logic here
    }
}

Pattern 2: Active-Passive Failover

Primary instance handles traffic; standby takes over on failure.

public class ActivePassiveService : BackgroundService
{
    private readonly ILeaderElection _election;
    private CancellationTokenSource? _activeWorkCts;

    public ActivePassiveService(ILeaderElectionFactory factory)
    {
        _election = factory.CreateElection("active-passive");
        _election.OnBecameLeader += async (s, e) => await ActivateAsync();
        _election.OnLostLeadership += async (s, e) => await DeactivateAsync();
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        await _election.StartAsync(stoppingToken);

        // Keep election alive
        while (!stoppingToken.IsCancellationRequested)
        {
            await Task.Delay(1000, stoppingToken);
        }

        await _election.StopAsync();
    }

    private async Task ActivateAsync()
    {
        Console.WriteLine("Activating as leader");

        _activeWorkCts = new CancellationTokenSource();

        // Start leader work
        _ = Task.Run(async () =>
        {
            while (!_activeWorkCts.Token.IsCancellationRequested)
            {
                // Active processing
                await Task.Delay(100, _activeWorkCts.Token);
            }
        });
    }

    private async Task DeactivateAsync()
    {
        Console.WriteLine("Deactivating (no longer leader)");

        // Stop leader work gracefully
        _activeWorkCts?.Cancel();
        await Task.Delay(100); // Allow work to drain

        _activeWorkCts?.Dispose();
        _activeWorkCts = null;
    }
}

Pattern 3: Health-Based Leadership with Step-Down

Leader monitors its own health and steps down if unhealthy.

public class HealthAwareLeaderService : BackgroundService
{
    private readonly IHealthBasedLeaderElection _election;

    public HealthAwareLeaderService(ILeaderElectionFactory factory)
    {
        _election = factory.CreateHealthBasedElection("health-leader");
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        await _election.StartAsync(stoppingToken);

        while (!stoppingToken.IsCancellationRequested)
        {
            // Check system health
            var health = await MeasureHealthAsync();

            // Update election with health status
            await _election.UpdateHealthAsync(
                isHealthy: health.Score >= 0.75,
                metadata: health.Metadata
            );

            if (_election.IsLeader)
            {
                // Do leader work
                await DoLeaderWorkAsync();
            }

            await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
        }

        await _election.StopAsync();
    }

    private async Task<(double Score, Dictionary<string, string> Metadata)> MeasureHealthAsync()
    {
        // Example health calculation
        var cpuUsage = 0.6; // 60%
        var memoryUsage = 0.7; // 70%
        var diskHealthy = true;

        var score = ((1.0 - cpuUsage) + (1.0 - memoryUsage) + (diskHealthy ? 1.0 : 0.0)) / 3.0;

        var metadata = new Dictionary<string, string>
        {
            ["cpu"] = $"{cpuUsage * 100:F0}%",
            ["memory"] = $"{memoryUsage * 100:F0}%",
            ["disk"] = diskHealthy ? "healthy" : "unhealthy"
        };

        return (score, metadata);
    }

    private async Task DoLeaderWorkAsync()
    {
        // Leader-only processing
        await Task.Delay(100);
    }
}

Thread Safety

All implementations of ILeaderElection and IHealthBasedLeaderElection must be thread-safe for concurrent access to properties (IsLeader, CurrentLeaderId) and event subscriptions.

Safe Usage:

// Safe: Multiple threads can check leadership
if (_election.IsLeader)
{
    // This is safe - leadership state is consistent
}

// Safe: Event subscriptions are thread-safe
_election.OnBecameLeader += Handler;

Testing

For unit and integration testing, use the InMemory provider which supports AOT and has zero external dependencies.

dotnet add package Excalibur.Dispatch.LeaderElection.InMemory

Test Example:

[Fact]
public async Task LeaderElection_ShouldElectSingleLeader()
{
    // Arrange
    var services = new ServiceCollection();
    services.AddInMemoryLeaderElection();
    var provider = services.BuildServiceProvider();

    var factory = provider.GetRequiredService<ILeaderElectionFactory>();
    var election1 = factory.CreateElection("test-resource");
    var election2 = factory.CreateElection("test-resource");

    // Act
    await election1.StartAsync();
    await election2.StartAsync();
    await Task.Delay(100); // Allow election to settle

    // Assert
    var leaders = new[] { election1.IsLeader, election2.IsLeader };
    Assert.Single(leaders.Where(x => x)); // Exactly one leader
}

See Also


License

This package is part of the Excalibur.Dispatch framework and is licensed under multiple licenses. See the project root for license details.


Support

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (5)

Showing the top 5 NuGet packages that depend on Excalibur.Dispatch.LeaderElection.Abstractions:

Package Downloads
Excalibur.Data.Postgres

Postgres database provider implementation for Excalibur data access layer.

Excalibur.LeaderElection

Core leader election utilities for the Excalibur framework. Provides distributed coordination capabilities with pluggable providers (SqlServer, Redis, Consul, Kubernetes, InMemory).

Excalibur.Testing.Conformance

Conformance test kits for Excalibur infrastructure implementations. Provides reusable abstract test suites for IEventStore, IOutboxStore, ISagaStore, ISnapshotStore, and other provider contracts.

Excalibur.LeaderElection.InMemory

In-memory leader election implementation for the Excalibur framework. Suitable for single-process scenarios, testing, and development.

Excalibur.Data.MongoDB

MongoDB database provider implementation for Excalibur data access layer.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
3.0.0-alpha.19 36 2/26/2026