A demonstration and reference implementation of distributed locking for background jobs in .NET applications, ensuring that scheduled jobs run only once across multiple application instances.
In distributed systems with multiple application instances, background jobs can execute simultaneously across different instances, leading to:
- Duplicate processing
- Resource conflicts
- Data inconsistency
- Performance degradation
This system provides a robust solution using database-based distributed locking.
βββββββββββββββββββ βββββββββββββββββββ
β Instance One β β Instance Two β
β β β β
β βββββββββββββββ β β βββββββββββββββ β
β β Hangfire β β β β Hangfire β β
β β Jobs β β β β Jobs β β
β βββββββββββββββ β β βββββββββββββββ β
β βββββββββββββββ β β βββββββββββββββ β
β β Coravel β β β β Coravel β β
β β Jobs β β β β Jobs β β
β βββββββββββββββ β β βββββββββββββββ β
βββββββββββββββββββ βββββββββββββββββββ
β β
βββββββββββββ¬ββββββββββββ
β
βββββββββββββββββββββββ
β Shared Database β
β β
β βββββββββββββββββββ β
β β QueueLocks β β
β β (Composite β β
β β Primary Key) β β
β βββββββββββββββββββ β
β βββββββββββββββββββ β
β β JobLogs β β
β βββββββββββββββββββ β
βββββββββββββββββββββββ
βββ Application.InstanceOne/ # Demo instance 1
βββ Application.InstanceTwo/ # Demo instance 2
βββ Application.MigrationApp/ # Database migrations
βββ Application.Services/ # Business logic & job services
β βββ JobServices/ # Background job implementations
β β βββ HeartbeatHangFireJob.cs # Hangfire job with distributed lock
β β βββ HeartbeatCoravelJob.cs # Coravel job with distributed lock
β βββ EnqueueService.cs # Job enqueueing service
β βββ HangfireHostedService.cs # Hangfire lifecycle management
βββ Domain/ # Domain entities and contracts
β βββ Entities/
β β βββ QueueLocks/ # Distributed lock entity
β β βββ JobLogs/ # Job execution logging
β βββ Repositories/ # Repository interfaces
βββ Infrastructure/ # Data access implementation
βββ Repositories/ # EF Core & Dapper repositories
βββ Configurations/ # Entity configurations
βββ Migrations/ # Database migrations
public class QueueLock
{
public string QueueName { get; set; } // Queue identifier
public string JobName { get; set; } // Job identifier
public DateTime CreatedTime { get; set; } // Lock timestamp
}
// Composite Primary Key: (QueueName, JobName)public class JobLog
{
public long Id { get; set; }
public Guid AppId { get; set; } // Instance identifier
public string JobName { get; set; }
public JobLogStatus Status { get; set; } // Processing, Completed, Exited
public string Remark { get; set; } // Execution details
public DateTime CreatedTime { get; set; }
public DateTime UpdatedTime { get; set; }
}- Attempt Lock Creation: Insert record with unique constraint
- Handle Conflicts: Catch unique constraint violations
- Check Existing Locks: Verify if lock exists and check timeout
- Timeout Cleanup: Remove stale locks (>5 minutes)
- Execute Job: Process business logic
- Release Lock: Delete lock record on completion/failure
- .NET 8.0 SDK
- SQL Server (LocalDB or full instance)
- Visual Studio 2022 or VS Code
- Target Framework: .NET 8.0
- Hangfire Version: 1.8.14
- Coravel Version: 5.0.3
- Entity Framework Core: 8.0.0
- Dapper: 2.1.35
- Serilog: 4.0.1
- Windows 10/11, Windows Server 2019/2022
- Linux (Ubuntu 20.04+, RHEL 8+, Alpine 3.17+)
- macOS 12.0+ (Monterey)
- Docker containers (linux/amd64, linux/arm64)
-
Clone Repository
git clone <repository-url> cd background-job-distributed-lock-demo
-
Database Setup
# Update connection strings in appsettings.json files # Run migrations dotnet run --project Application.MigrationApp
-
Run Multiple Instances
# Terminal 1 - Instance One (Port 5001) dotnet run --project Application.InstanceOne # Terminal 2 - Instance Two (Port 5002) dotnet run --project Application.InstanceTwo
-
Access Applications
- Instance One:
https://localhost:5001 - Instance Two:
https://localhost:5002 - Hangfire Dashboard:
https://localhost:5001/hangfire
- Instance One:
# Trigger Hangfire job from Instance One
curl -X POST https://localhost:5001/enqueue-hangfire-heartbeat-jb
# Trigger Coravel job from Instance Two
curl -X POST https://localhost:5002/enqueue-coravel-heartbeat-jb- Hangfire v1.8.14: Configured to run hourly via
Cron.Hourlywith SQL Server persistence - Coravel v5.0.3: Configured to run hourly via
.Hourly()with in-memory scheduling
- Check logs for lock acquisition/release messages
- Monitor
QueueLockstable for active locks - Review
JobLogstable for execution history
- Database-based atomic lock acquisition
- Automatic timeout and cleanup (5-minute default)
- Graceful handling of concurrent access attempts
- Hangfire v1.8.14: Enterprise-grade background job processing with SQL Server storage
- Coravel v5.0.3: Lightweight .NET job scheduling with in-memory queuing
- Job execution tracking with status
- Instance identification for debugging
- Detailed error and conflict reporting
- Handles database connection failures
- Manages application shutdown scenarios
- Prevents orphaned locks with timeout mechanism
- Dapper for high-performance database operations
- Minimal lock duration
- Efficient unique constraint utilization
{
"ConnectionStrings": {
"DbConnection": "Server=(localdb)\\mssqllocaldb;Database=BackgroundJobDistributedLock;Trusted_Connection=true;"
}
}public static class QueueName
{
public const string HangFireQueue = "hangfire-queue";
public const string CoravelQueue = "coravel-queue";
}private const int QueueLockMaximumLifeTimeInMinutes = 5;| Column | Type | Description |
|---|---|---|
| QueueName | nvarchar(100) | Queue identifier (PK) |
| JobName | nvarchar(100) | Job identifier (PK) |
| CreatedTime | datetime2 | Lock creation timestamp |
| Column | Type | Description |
|---|---|---|
| Id | bigint | Primary key |
| AppId | uniqueidentifier | Application instance ID |
| JobName | nvarchar(100) | Job identifier |
| Status | nvarchar(35) | Processing status |
| Remark | nvarchar(max) | Execution details |
| CreatedTime | datetime2 | Log creation time |
| UpdatedTime | datetime2 | Last update time |
- Start Both Instances: Run Instance One and Instance Two simultaneously
- Trigger Same Job: Execute the same job type from both instances
- Observe Logs: Only one instance should process the job
- Check Database: Verify lock creation and cleanup in
QueueLockstable - Review Job Logs: Examine execution history in
JobLogstable
- Connection String: Ensure SQL Server is accessible
- Port Conflicts: Modify ports in
launchSettings.jsonif needed - Lock Timeouts: Adjust
QueueLockMaximumLifeTimeInMinutesfor longer jobs - Migration Errors: Ensure database permissions for schema changes
- Enable detailed logging in
serilogs.json - Monitor Hangfire dashboard for job status
- Query database directly to inspect lock states
- Check application logs for constraint violation messages
- Optimistic Locking: Attempt lock creation first
- Unique Constraints: Database enforces atomicity
- Timeout Mechanism: Prevents indefinite locks
- Cleanup Strategy: Automatic and manual lock removal
- Dapper Usage: High-performance data access for critical operations
- Connection Management: Proper disposal and error handling
- Minimal Lock Duration: Quick lock acquisition and release
- Index Optimization: Composite primary key for efficient lookups
- Fork the repository
- Create a feature branch
- Implement changes with tests
- Submit a pull request
This project is licensed under the MIT License - see the LICENSE file for details.