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.
- .NET 8.0 or .NET 10.0
- Supported platforms: Windows, Linux, macOS
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:
SessionLockDelayvalidation tightened. The permitted maximum forConsulLeaderElectionOptions.SessionLockDelaychanged from 60 minutes to 60 seconds, matching Consul's own upper bound for session lock-delay. A value greater than 60 seconds now throws anArgumentExceptionat 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)};
SessionLockDelayis aninit-only property, so it is set through an object initializer (or configuration binding), not by reassignment.Migration: if you set
SessionLockDelayabove 60 seconds, lower it to 60 seconds or less. Configurations using the default (15 seconds) are unaffected.
┌────────────────────────────────────────────────────────────────────────────┐
│ 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 │ │ │ │ │ │ │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
└────────────────────────────────────────────────────────────────────────────┘
- 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
| Provider | Package | Description |
|---|---|---|
| Azure Blob Storage | MultiLock.AzureBlobStorage | Uses Azure Blob Storage for coordination |
| SQL Server | MultiLock.SqlServer | Uses SQL Server database for coordination |
| PostgreSQL | MultiLock.PostgreSQL | Uses PostgreSQL database for coordination |
| Redis | MultiLock.Redis | Uses Redis for coordination |
| File System | MultiLock.FileSystem | Uses local file system (single machine) |
| In-Memory | MultiLock.InMemory | In-memory provider for testing |
| Consul | MultiLock.Consul | Uses HashiCorp Consul for coordination |
| ZooKeeper | MultiLock.ZooKeeper | Uses Apache ZooKeeper for coordination |
# 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.InMemoryusingMultiLock;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();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);}}The leader election service provides a modern AsyncEnumerable API for consuming leadership change events:
// 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!");}}// 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}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}The MultiLock framework is designed to be fully thread-safe and handle concurrent operations gracefully:
- 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
IDisposableandIAsyncDisposablewith proper coordination to prevent race conditions during shutdown. - Callback coordination: Timer callbacks (heartbeat and election timers) are coordinated with disposal to prevent
ObjectDisposedExceptionand ensure clean shutdown.
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()andIsLeaderAsync()can be called concurrently without issues.
The framework has been extensively tested for various concurrent scenarios:
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.
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.
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.
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 leadershipClean failover without split-brain scenarios. Exactly one new leader is elected.
- Use dependency injection: Register the service as a singleton to ensure a single instance per application.
services.AddSingleton<ILeaderElectionService,LeaderElectionService>();Avoid manual synchronization: The framework handles all necessary synchronization internally. Don't wrap calls in your own locks.
Use async disposal: When shutting down, prefer
DisposeAsync()overDispose()for providers that support it (Consul, ZooKeeper).
awaitusingvarprovider=newConsulLeaderElectionProvider(options,logger);// Provider will be properly disposed asynchronously- Handle cancellation properly: Always pass
CancellationTokento async methods and handleOperationCanceledException.
try{awaitservice.StartAsync(cancellationToken);}catch(OperationCanceledException){// Clean shutdown requested}- 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}}- 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
- Uses atomic Redis commands (SET with NX and EX options)
- Connection multiplexing is handled by StackExchange.Redis
- No connection pool exhaustion under high concurrency
- Uses file locking for mutual exclusion
- Note: Only suitable for single-machine scenarios
- File locks are automatically released on process termination
- Uses Consul sessions for distributed locking
- Session TTL ensures automatic cleanup on failure
- Supports high concurrency across distributed systems
- Uses ZooKeeper ephemeral sequential nodes
- Automatic cleanup when client disconnects
- Handles network partitions gracefully
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);});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);});builder.Services.AddRedisLeaderElection(connectionString:"localhost:6379",
options =>{options.ElectionGroup="my-service";options.HeartbeatInterval=TimeSpan.FromSeconds(30);options.HeartbeatTimeout=TimeSpan.FromSeconds(90);});builder.Services.AddAzureBlobStorageLeaderElection(connectionString:"DefaultEndpointsProtocol=https;AccountName=...",
options =>{options.ElectionGroup="my-service";options.HeartbeatInterval=TimeSpan.FromSeconds(30);options.HeartbeatTimeout=TimeSpan.FromSeconds(90);});builder.Services.AddConsulLeaderElection(address:"http://localhost:8500",
options =>{options.ElectionGroup="my-service";options.HeartbeatInterval=TimeSpan.FromSeconds(30);options.HeartbeatTimeout=TimeSpan.FromSeconds(90);});builder.Services.AddZooKeeperLeaderElection(connectionString:"localhost:2181",
options =>{options.ElectionGroup="my-service";options.HeartbeatInterval=TimeSpan.FromSeconds(30);options.HeartbeatTimeout=TimeSpan.FromSeconds(90);});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);});- Election: Multiple instances compete to become the leader
- Leadership: One instance becomes the leader and performs exclusive work
- Heartbeat: The leader sends periodic heartbeats to maintain leadership
- Failover: If the leader fails, a new election occurs automatically
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.
- 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 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.
- 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
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();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);}}// 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!");}}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;});| Option | Description | Default |
|---|---|---|
SemaphoreName | Unique name for the semaphore | Required |
MaxCount | Maximum concurrent holders | Required |
HolderId | Unique identifier for this holder | Auto-generated |
HeartbeatInterval | How often to send heartbeats | 10 seconds |
HeartbeatTimeout | Time before a holder is considered dead | 30 seconds |
AcquisitionInterval | How often to retry acquiring a slot | 5 seconds |
MaxRetryAttempts | Retry attempts for transient failures | 3 |
RetryBaseDelay | Base delay between retries | 100ms |
RetryMaxDelay | Maximum delay between retries | 5 seconds |
AutoStart | Start acquiring on service start | true |
EnableDetailedLogging | Enable verbose logging | false |
| Feature | Leader Election | Semaphore |
|---|---|---|
| Concurrent holders | 1 (exclusive) | N (configurable) |
| Use case | Single leader tasks | Rate limiting, pooling |
| Failover | Automatic re-election | Slot becomes available |
| API | IsLeader property | IsHolding property |
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 3The 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 downThe 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 -vThe framework works seamlessly in containerized environments. See docker-compose.yml for examples of running with various backing services.
Contributions are welcome! Please see our contributing guidelines and code of conduct.
This project is licensed under the MIT License - see the LICENSE file for details.