Skip to content

Repository files navigation

MultiLock

codecov

NuGet - Azure Blob StorageNuGet - SQL ServerNuGet - PostgreSQLNuGet - RedisNuGet - File SystemNuGet - In-MemoryNuGet - ConsulNuGet - ZooKeeper

A comprehensive .NET framework for implementing Leader Election and Distributed Semaphores patterns with support for multiple providers including Azure Blob Storage, SQL Server, PostgreSQL, Redis, File System, In-Memory, Consul, and ZooKeeper.

Requirements

  • .NET 8.0 or .NET 10.0
  • Supported platforms: Windows, Linux, macOS

Breaking Changes

v2.0.0

  • Distributed Semaphores are introduced as a new coordination primitive alongside Leader Election. This is an additive feature and does not change existing leader-election APIs. See Distributed Semaphores.

  • Consul: SessionLockDelay validation tightened. The permitted maximum for ConsulLeaderElectionOptions.SessionLockDelay changed from 60 minutes to 60 seconds, matching Consul's own upper bound for session lock-delay. A value greater than 60 seconds now throws an ArgumentException at startup instead of being accepted.

    // ❌ Throws ArgumentException at startup (when the provider is constructed) —// the maximum is now 60 seconds, was previously 60 minutes:varinvalid=newConsulLeaderElectionOptions{SessionLockDelay=TimeSpan.FromMinutes(5)};// ✅ Valid — must be between 0 and 60 seconds (default is 15 seconds):varvalid=newConsulLeaderElectionOptions{SessionLockDelay=TimeSpan.FromSeconds(15)};

    SessionLockDelay is an init-only property, so it is set through an object initializer (or configuration binding), not by reassignment.

    Migration: if you set SessionLockDelay above 60 seconds, lower it to 60 seconds or less. Configurations using the default (15 seconds) are unaffected.

Architecture Overview

┌────────────────────────────────────────────────────────────────────────────┐
│ APPLICATIONS │
│ │
│ ┌──────────────┐ ┌──────────────────┐ ┌──────────────────┐ │
│ │ Sample App │ │ Multi-Provider │ │ Your Application │ │
│ │ │ │ Demo │ │ │ │
│ └──────┬───────┘ └────────┬─────────┘ └────────┬─────────┘ │
│ │ │ │ │
│ └─────────────────────┼───────────────────────┘ │
│ │ │
└─────────────────────────────────┼──────────────────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────────────────────────────┐
│ CORE FRAMEWORK │
│ │
│ ┌─────────────────────────────────┐ ┌─────────────────────────────────┐ │
│ │ ILeaderElectionService │ │ ISemaphoreService │ │
│ │ • StartAsync() / StopAsync() │ │ • AcquireAsync() │ │
│ │ • IsLeader │ │ • TryAcquireAsync() │ │
│ │ • GetLeadershipChangesAsync() │ │ • GetStatusChangesAsync() │ │
│ └───────────────┬─────────────────┘ └───────────────┬─────────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────────────────────────┐ ┌─────────────────────────────────┐ │
│ │ ILeaderElectionProvider │ │ ISemaphoreProvider │ │
│ │ • TryAcquireLeadershipAsync() │ │ • TryAcquireAsync() │ │
│ │ • ReleaseLeadershipAsync() │ │ • ReleaseAsync() │ │
│ │ • UpdateHeartbeatAsync() │ │ • UpdateHeartbeatAsync() │ │
│ └───────────────┬─────────────────┘ └───────────────┬─────────────────┘ │
│ │ │ │
└──────────────────┼────────────────────────────────────┼────────────────────┘
│ │
└────────────────┬───────────────────┘
▼
┌────────────────────────────────────────────────────────────────────────────┐
│ PROVIDERS │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ InMemory │ │ FileSystem │ │ SQL Server │ │ PostgreSQL │ │
│ │ │ │ │ │ │ │ │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Azure Blob │ │ Redis │ │ Consul │ │ ZooKeeper │ │
│ │ Storage │ │ │ │ │ │ │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
└────────────────────────────────────────────────────────────────────────────┘

Features

  • Multiple Providers: Support for various storage backends
  • Leader Election: First-to-acquire becomes leader strategy with automatic failover
  • Distributed Semaphores: Control concurrent access with configurable slot limits
  • Heartbeat Monitoring: Automatic health monitoring and failover
  • Resilient Design: Handles transient and persistent failures gracefully
  • Thread-Safe: All operations are thread-safe
  • Configurable: Extensive configuration options for timeouts, retry policies, etc.
  • Dependency Injection: Full support for .NET dependency injection
  • Comprehensive Logging: Built-in logging and telemetry support

Supported Providers

ProviderPackageDescription
Azure Blob StorageMultiLock.AzureBlobStorageUses Azure Blob Storage for coordination
SQL ServerMultiLock.SqlServerUses SQL Server database for coordination
PostgreSQLMultiLock.PostgreSQLUses PostgreSQL database for coordination
RedisMultiLock.RedisUses Redis for coordination
File SystemMultiLock.FileSystemUses local file system (single machine)
In-MemoryMultiLock.InMemoryIn-memory provider for testing
ConsulMultiLock.ConsulUses HashiCorp Consul for coordination
ZooKeeperMultiLock.ZooKeeperUses Apache ZooKeeper for coordination

Quick Start

1. Install a Provider Package

# For SQL Server
dotnet add package MultiLock.SqlServer
# For Redis
dotnet add package MultiLock.Redis
# For PostgreSQL
dotnet add package MultiLock.PostgreSQL
# For In-Memory (testing)
dotnet add package MultiLock.InMemory

2. Configure Services

usingMultiLock;usingMultiLock.InMemory;varbuilder=WebApplication.CreateBuilder(args);// Add leader election with In-Memory providerbuilder.Services.AddLeaderElection<InMemoryLeaderElectionProvider>(options =>{options.ElectionGroup="my-service";options.HeartbeatInterval=TimeSpan.FromSeconds(30);options.HeartbeatTimeout=TimeSpan.FromSeconds(90);});varapp=builder.Build();

3. Use in Your Service

publicclassMyBackgroundService:BackgroundService{privatereadonlyILeaderElectionService_leaderElection;privatereadonlyILogger<MyBackgroundService>_logger;publicMyBackgroundService(ILeaderElectionServiceleaderElection,ILogger<MyBackgroundService>logger){_leaderElection=leaderElection;_logger=logger;}protectedoverrideasyncTaskExecuteAsync(CancellationTokenstoppingToken){// Start listening to leadership changes using AsyncEnumerablevarleadershipTask=Task.Run(async()=>{awaitforeach(varchangein_leaderElection.GetLeadershipChangesAsync(stoppingToken)){if(change.BecameLeader){_logger.LogInformation("🎉 Leadership acquired!");}elseif(change.LostLeadership){_logger.LogWarning("😞 Leadership lost!");}}},stoppingToken);while(!stoppingToken.IsCancellationRequested){if(_leaderElection.IsLeader){// Perform leader-only work_logger.LogInformation("🏆 Performing leader work...");awaitDoLeaderWork(stoppingToken);}else{// Perform follower work or wait_logger.LogInformation("👥 Waiting as follower...");awaitTask.Delay(TimeSpan.FromSeconds(10),stoppingToken);}}// Wait for leadership monitoring to completetry{awaitleadershipTask;}catch(OperationCanceledException){// Expected when stopping}}privateasyncTaskDoLeaderWork(CancellationTokencancellationToken){// Your leader-specific logic hereawaitTask.Delay(TimeSpan.FromSeconds(5),cancellationToken);}}

AsyncEnumerable API

The leader election service provides a modern AsyncEnumerable API for consuming leadership change events:

Basic Usage

// Subscribe to all leadership changesawaitforeach(varchangeinleaderElection.GetLeadershipChangesAsync(cancellationToken)){if(change.BecameLeader){Console.WriteLine("I am now the leader!");}elseif(change.LostLeadership){Console.WriteLine("I lost leadership!");}}

Filtering Events

// Only receive leadership acquired eventsawaitforeach(varchangeinleaderElection.GetLeadershipChangesAsync(LeadershipEventType.Acquired,cancellationToken)){Console.WriteLine("Leadership acquired!");}// Receive both acquired and lost eventsawaitforeach(varchangeinleaderElection.GetLeadershipChangesAsync(LeadershipEventType.Acquired|LeadershipEventType.Lost,cancellationToken)){// Handle events}

Extension Methods

The framework provides powerful LINQ-style extension methods:

// Take only the first leadership acquisitionawaitforeach(varchangeinleaderElection.GetLeadershipChangesAsync(cancellationToken).TakeUntilLeader(cancellationToken)){Console.WriteLine("Waiting for leadership...");}// Process events while we are the leaderawaitforeach(varchangeinleaderElection.GetLeadershipChangesAsync(cancellationToken).WhileLeader(cancellationToken)){Console.WriteLine("Still the leader!");}// Execute callbacks on leadership transitionsawaitleaderElection.GetLeadershipChangesAsync(cancellationToken).OnLeadershipTransition(onAcquired: e =>Console.WriteLine("Acquired!"),onLost: e =>Console.WriteLine("Lost!"),cancellationToken);// Combine multiple extension methodsawaitforeach(varchangeinleaderElection.GetLeadershipChangesAsync(cancellationToken).Where(e =>e.BecameLeader||e.LostLeadership,cancellationToken).DistinctUntilChanged(cancellationToken).Take(5,cancellationToken)){// Process filtered events}

Concurrency and Thread-Safety

Thread-Safety Guarantees

The MultiLock framework is designed to be fully thread-safe and handle concurrent operations gracefully:

Core Service Thread-Safety

  • All public methods are thread-safe: You can safely call StartAsync(), StopAsync(), IsLeaderAsync(), and other methods from multiple threads concurrently.
  • Internal state protection: All internal state modifications are protected by appropriate synchronization mechanisms (locks, atomic operations, etc.).
  • Safe disposal: The service implements both IDisposable and IAsyncDisposable with proper coordination to prevent race conditions during shutdown.
  • Callback coordination: Timer callbacks (heartbeat and election timers) are coordinated with disposal to prevent ObjectDisposedException and ensure clean shutdown.

Provider Thread-Safety

All provider implementations are thread-safe and support concurrent operations:

  • Concurrent leadership acquisition: Multiple participants can safely attempt to acquire leadership simultaneously - only one will succeed.
  • Concurrent heartbeat updates: Multiple threads can safely update heartbeats for the same participant.
  • Concurrent metadata updates: Metadata can be safely updated from multiple threads.
  • Concurrent queries: Methods like GetCurrentLeaderAsync() and IsLeaderAsync() can be called concurrently without issues.

Concurrency Scenarios

The framework has been extensively tested for various concurrent scenarios:

Multiple Participants Competing

When multiple participants attempt to acquire leadership simultaneously:

// Safe to run from multiple instances/threadsvartasks=Enumerable.Range(0,10).Select(async i =>{varservice=serviceProvider.GetRequiredService<ILeaderElectionService>();awaitservice.StartAsync();// Only one will become leader}).ToArray();awaitTask.WhenAll(tasks);

Exactly one participant will become the leader. All others will fail gracefully and continue monitoring for leadership opportunities.

Rapid Acquire/Release Cycles

The framework handles rapid leadership transitions without race conditions:

// Safe to rapidly acquire and release leadershipfor(inti=0;i<100;i++){awaitservice.StartAsync();awaitTask.Delay(10);awaitservice.StopAsync();}

Each cycle completes cleanly without resource leaks or state corruption.

Concurrent Heartbeat Updates

Multiple threads can safely update heartbeats:

// Safe concurrent heartbeat updatesvartasks=Enumerable.Range(0,50).Select(async i =>{awaitprovider.UpdateHeartbeatAsync("election-group","participant-id");}).ToArray();awaitTask.WhenAll(tasks);

All heartbeat updates are processed atomically. The last update wins.

Leader Failover Race Conditions

When a leader fails and multiple participants compete to take over:

// Current leader releases leadershipawaitcurrentLeader.StopAsync();// Multiple waiting participants compete// Only one will successfully acquire leadership

Clean failover without split-brain scenarios. Exactly one new leader is elected.

Best Practices for Concurrent Usage

  1. Use dependency injection: Register the service as a singleton to ensure a single instance per application.
services.AddSingleton<ILeaderElectionService,LeaderElectionService>();
  1. Avoid manual synchronization: The framework handles all necessary synchronization internally. Don't wrap calls in your own locks.

  2. Use async disposal: When shutting down, prefer DisposeAsync() over Dispose() for providers that support it (Consul, ZooKeeper).

awaitusingvarprovider=newConsulLeaderElectionProvider(options,logger);// Provider will be properly disposed asynchronously
  1. Handle cancellation properly: Always pass CancellationToken to async methods and handle OperationCanceledException.
try{awaitservice.StartAsync(cancellationToken);}catch(OperationCanceledException){// Clean shutdown requested}
  1. Monitor leadership changes: Use the GetLeadershipChangesAsync() API to react to leadership transitions rather than polling.
awaitforeach(varchangeinservice.GetLeadershipChangesAsync(cancellationToken)){if(change.BecameLeader){// Start leader-only work}elseif(change.LostLeadership){// Stop leader-only work}}

Provider-Specific Concurrency Considerations

Database Providers (PostgreSQL, SQL Server)

  • Use atomic database operations (INSERT ... ON CONFLICT, MERGE) to prevent race conditions
  • Connection pooling is handled automatically by the database client libraries
  • No risk of deadlocks - all operations use optimistic concurrency control

Redis Provider

  • Uses atomic Redis commands (SET with NX and EX options)
  • Connection multiplexing is handled by StackExchange.Redis
  • No connection pool exhaustion under high concurrency

File System Provider

  • Uses file locking for mutual exclusion
  • Note: Only suitable for single-machine scenarios
  • File locks are automatically released on process termination

Consul Provider

  • Uses Consul sessions for distributed locking
  • Session TTL ensures automatic cleanup on failure
  • Supports high concurrency across distributed systems

ZooKeeper Provider

  • Uses ZooKeeper ephemeral sequential nodes
  • Automatic cleanup when client disconnects
  • Handles network partitions gracefully

Provider-Specific Configuration

SQL Server Provider

builder.Services.AddSqlServerLeaderElection(connectionString:"Server=localhost;Database=MyApp;Trusted_Connection=true;",
options =>{options.ElectionGroup="my-service";options.HeartbeatInterval=TimeSpan.FromSeconds(30);options.HeartbeatTimeout=TimeSpan.FromSeconds(90);});

PostgreSQL Provider

builder.Services.AddPostgreSQLLeaderElection(connectionString:"Host=localhost;Database=MyApp;Username=user;Password=password;",
options =>{options.ElectionGroup="my-service";options.HeartbeatInterval=TimeSpan.FromSeconds(30);options.HeartbeatTimeout=TimeSpan.FromSeconds(90);});

Redis Provider

builder.Services.AddRedisLeaderElection(connectionString:"localhost:6379",
options =>{options.ElectionGroup="my-service";options.HeartbeatInterval=TimeSpan.FromSeconds(30);options.HeartbeatTimeout=TimeSpan.FromSeconds(90);});

Azure Blob Storage Provider

builder.Services.AddAzureBlobStorageLeaderElection(connectionString:"DefaultEndpointsProtocol=https;AccountName=...",
options =>{options.ElectionGroup="my-service";options.HeartbeatInterval=TimeSpan.FromSeconds(30);options.HeartbeatTimeout=TimeSpan.FromSeconds(90);});

Consul Provider

builder.Services.AddConsulLeaderElection(address:"http://localhost:8500",
options =>{options.ElectionGroup="my-service";options.HeartbeatInterval=TimeSpan.FromSeconds(30);options.HeartbeatTimeout=TimeSpan.FromSeconds(90);});

ZooKeeper Provider

builder.Services.AddZooKeeperLeaderElection(connectionString:"localhost:2181",
options =>{options.ElectionGroup="my-service";options.HeartbeatInterval=TimeSpan.FromSeconds(30);options.HeartbeatTimeout=TimeSpan.FromSeconds(90);});

Advanced Configuration Examples

PostgreSQL with Custom Schema and Table:

builder.Services.AddPostgreSQLLeaderElection(options =>{options.ConnectionString="Host=localhost;Database=myapp;Username=user;Password=pass";options.TableName="custom_leader_election";options.SchemaName="leader_election";options.AutoCreateTable=true;options.CommandTimeoutSeconds=60;}, leaderElectionOptions =>{leaderElectionOptions.ElectionGroup="my-service";leaderElectionOptions.ParticipantId=Environment.MachineName;leaderElectionOptions.HeartbeatInterval=TimeSpan.FromSeconds(15);leaderElectionOptions.HeartbeatTimeout=TimeSpan.FromSeconds(45);leaderElectionOptions.EnableDetailedLogging=true;});

ZooKeeper with Custom Session Settings:

builder.Services.AddZooKeeperLeaderElection(options =>{options.ConnectionString="zk1:2181,zk2:2181,zk3:2181";options.RootPath="/my-app/leader-election";options.SessionTimeout=TimeSpan.FromSeconds(30);options.ConnectionTimeout=TimeSpan.FromSeconds(10);options.MaxRetries=5;options.RetryDelay=TimeSpan.FromSeconds(2);options.AutoCreateRootPath=true;}, leaderElectionOptions =>{leaderElectionOptions.ElectionGroup="my-service";leaderElectionOptions.ParticipantId=$"{Environment.MachineName}-{Environment.ProcessId}";leaderElectionOptions.HeartbeatInterval=TimeSpan.FromSeconds(10);leaderElectionOptions.HeartbeatTimeout=TimeSpan.FromSeconds(30);});

Key Concepts

Leader Election Process

  1. Election: Multiple instances compete to become the leader
  2. Leadership: One instance becomes the leader and performs exclusive work
  3. Heartbeat: The leader sends periodic heartbeats to maintain leadership
  4. Failover: If the leader fails, a new election occurs automatically

Leadership Change Events

The service provides an AsyncEnumerable API for observing leadership changes:

  • Acquired: Emitted when the current instance becomes the leader
  • Lost: Emitted when the current instance loses leadership
  • Changed: Emitted on any leadership status change (including when another participant becomes leader)

Use GetLeadershipChangesAsync() to subscribe to these events and optionally filter by event type using LeadershipEventType flags.

Configuration Options

  • ElectionGroup: Logical group name for the election (multiple services can have different groups)
  • ParticipantId: Unique identifier for this instance (auto-generated if not specified)
  • HeartbeatInterval: How often the leader sends heartbeats
  • HeartbeatTimeout: How long to wait before considering a leader dead
  • ElectionInterval: How often followers attempt to become leader
  • LockTimeout: Maximum time to hold a lock during election
  • AutoStart: Whether to automatically start the election process

Distributed Semaphores

Distributed semaphores allow you to control concurrent access to a shared resource across multiple instances. Unlike leader election (which allows only one holder), semaphores allow a configurable number of concurrent holders.

Use Cases

  • Rate Limiting: Limit concurrent API calls to an external service
  • Resource Pooling: Control access to a limited pool of resources (database connections, licenses, etc.)
  • Throttling: Limit concurrent processing of expensive operations
  • Capacity Management: Ensure only N instances process work simultaneously

Quick Start

usingMultiLock;usingMultiLock.InMemory;varbuilder=WebApplication.CreateBuilder(args);// Add semaphore with In-Memory provider (max 5 concurrent holders)builder.Services.AddSemaphore<InMemorySemaphoreProvider>(options =>{options.SemaphoreName="api-rate-limiter";options.MaxCount=5;options.HeartbeatInterval=TimeSpan.FromSeconds(30);options.HeartbeatTimeout=TimeSpan.FromSeconds(90);});varapp=builder.Build();

Using the Semaphore Service

publicclassRateLimitedApiClient{privatereadonlyISemaphoreService_semaphore;privatereadonlyHttpClient_httpClient;publicRateLimitedApiClient(ISemaphoreServicesemaphore,HttpClienthttpClient){_semaphore=semaphore;_httpClient=httpClient;}publicasyncTask<string>CallExternalApiAsync(CancellationTokencancellationToken){// Block until a slot is available (or cancellationToken is triggered)await_semaphore.WaitForSlotAsync(cancellationToken);try{// We now hold a slot - make the API callreturnawait_httpClient.GetStringAsync("/api/data",cancellationToken);}finally{// Always release the slot, even if the call throwsawait_semaphore.ReleaseAsync(cancellationToken);}}publicasyncTask<string?>TryCallExternalApiAsync(CancellationTokencancellationToken){// Try to acquire without blocking; returns false if all slots are takenboolacquired=await_semaphore.TryAcquireAsync(cancellationToken);if(!acquired)returnnull;try{returnawait_httpClient.GetStringAsync("/api/data",cancellationToken);}finally{await_semaphore.ReleaseAsync(cancellationToken);}}publicasyncTask<string?>CallWithScopeAsync(CancellationTokencancellationToken){// AcquireScopeAsync returns a disposable scope when a slot is acquired (null if full),// so the slot is released automatically at the end of the using block - no try/finally.awaitusingSemaphoreAcquisition?scope=await_semaphore.AcquireScopeAsync(cancellationToken);if(scopeisnull)returnnull;returnawait_httpClient.GetStringAsync("/api/data",cancellationToken);}}

Monitoring Semaphore Status

// Check current status synchronously via the propertySemaphoreStatusstatus=semaphore.CurrentStatus;Console.WriteLine($"Holding: {status.IsHolding}");Console.WriteLine($"Available: {status.AvailableSlots}/{status.MaxCount}");// Or fetch the latest state including all holders asynchronouslySemaphoreInfo?info=awaitsemaphore.GetSemaphoreInfoAsync(cancellationToken);if(info!=null)Console.WriteLine($"[Async] Active holders: {info.CurrentCount}/{info.MaxCount}");// Subscribe to status changesawaitforeach(varchangeinsemaphore.GetStatusChangesAsync(cancellationToken)){if(change.AcquiredSlot){Console.WriteLine("Acquired a semaphore slot!");}elseif(change.LostSlot){Console.WriteLine("Lost semaphore slot!");}}

Provider-Specific Configuration

All providers support semaphores with the same API:

// PostgreSQLbuilder.Services.AddPostgreSqlSemaphore(connectionString:"Host=localhost;Database=MyApp;...",
options =>{options.SemaphoreName="my-semaphore";options.MaxCount=10;});// Redisbuilder.Services.AddRedisSemaphore(connectionString:"localhost:6379",
options =>{options.SemaphoreName="my-semaphore";options.MaxCount=10;});// SQL Serverbuilder.Services.AddSqlServerSemaphore(connectionString:"Server=localhost;Database=MyApp;...",
options =>{options.SemaphoreName="my-semaphore";options.MaxCount=10;});// Azure Blob Storagebuilder.Services.AddAzureBlobStorageSemaphore(connectionString:"DefaultEndpointsProtocol=https;...",
options =>{options.SemaphoreName="my-semaphore";options.MaxCount=10;});// Consulbuilder.Services.AddConsulSemaphore(address:"http://localhost:8500",
options =>{options.SemaphoreName="my-semaphore";options.MaxCount=10;});// ZooKeeperbuilder.Services.AddZooKeeperSemaphore(connectionString:"localhost:2181",
options =>{options.SemaphoreName="my-semaphore";options.MaxCount=10;});// File Systembuilder.Services.AddFileSystemSemaphore(directoryPath:"/var/locks",
options =>{options.SemaphoreName="my-semaphore";options.MaxCount=10;});

Semaphore Configuration Options

OptionDescriptionDefault
SemaphoreNameUnique name for the semaphoreRequired
MaxCountMaximum concurrent holdersRequired
HolderIdUnique identifier for this holderAuto-generated
HeartbeatIntervalHow often to send heartbeats10 seconds
HeartbeatTimeoutTime before a holder is considered dead30 seconds
AcquisitionIntervalHow often to retry acquiring a slot5 seconds
MaxRetryAttemptsRetry attempts for transient failures3
RetryBaseDelayBase delay between retries100ms
RetryMaxDelayMaximum delay between retries5 seconds
AutoStartStart acquiring on service starttrue
EnableDetailedLoggingEnable verbose loggingfalse

Semaphore vs Leader Election

FeatureLeader ElectionSemaphore
Concurrent holders1 (exclusive)N (configurable)
Use caseSingle leader tasksRate limiting, pooling
FailoverAutomatic re-electionSlot becomes available
APIIsLeader propertyIsHolding property

Samples

Check out the sample applications:

  • Basic Sample: samples/MultiLock.Sample/ - Single provider demonstration
  • Multi-Provider Demo: samples/MultiLock.MultiProvider/ - Multiple instances competing
  • Semaphore Sample: samples/MultiLock.SemaphoreSample/ - Rate-limited workers coordinated by a distributed semaphore
# Run the basic sample with different providerscd samples/MultiLock.Sample
dotnet run inmemory
dotnet run filesystem
dotnet run sqlserver "Server=localhost;Database=Test;Trusted_Connection=true;"
dotnet run postgresql "Host=localhost;Database=leaderelection;Username=user;Password=pass"
dotnet run redis "localhost:6379"
dotnet run consul "http://localhost:8500"
dotnet run zookeeper "localhost:2181"# Run the multi-provider democd samples/MultiLock.MultiProvider
dotnet run
# Run the semaphore sample: dotnet run [provider] [maxConcurrent]cd samples/MultiLock.SemaphoreSample
dotnet run inmemory 3

Testing

The project includes comprehensive unit tests and integration tests. Integration tests use Docker containers to test against real services.

# Run unit tests
dotnet test tests/MultiLock.Tests/
# Run integration tests (requires Docker)
docker-compose up -d
dotnet test tests/MultiLock.IntegrationTests/
docker-compose down

Testing with Docker Compose

The included docker-compose.yml file provides all the necessary services for testing:

  • PostgreSQL (port 5432) - For PostgreSQL provider testing
  • ZooKeeper (port 2181) - For ZooKeeper provider testing
  • Redis (port 6379) - For Redis provider testing
  • Consul (port 8500) - For Consul provider testing
  • Azurite (ports 10000-10002) - For Azure Blob Storage provider testing
  • SQL Server (port 1433) - For SQL Server provider testing
# Start all services
docker-compose up -d
# Check service health
docker-compose ps
# View logs for a specific service
docker-compose logs postgres
docker-compose logs zookeeper
# Stop all services
docker-compose down
# Stop and remove volumes (clean slate)
docker-compose down -v

Docker Support

The framework works seamlessly in containerized environments. See docker-compose.yml for examples of running with various backing services.

Contributing

Contributions are welcome! Please see our contributing guidelines and code of conduct.

License

This project is licensed under the MIT License - see the LICENSE file for details.

About

Leader election

Resources

Code of conduct

Contributing

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages