Class RedisLockHandler
- Namespace
- Quartz.Extensions.Redis
- Assembly
- Quartz.Extensions.Redis.dll
A Redis-based ILockHandler that uses distributed locks
(SET NX PX) instead of database row locks.
public sealed class RedisLockHandler : ILockHandler
- Inheritance
-
RedisLockHandler
- Implements
- Inherited Members
Remarks
This lock handler is designed for clustered Quartz.NET setups where jobs are stored
in a relational database but lock contention on the QRTZ_LOCKS table causes
deadlocks or performance issues under heavy scheduling load.
The implementation uses a two-tier locking strategy: a local SemaphoreSlim
prevents redundant Redis round-trips within the same process, and a Redis
SET key value NX PX timeout command provides the cross-node distributed lock.
Configure via properties:
quartz.jobStore.lockHandler.type = Quartz.Extensions.Redis.RedisLockHandler, Quartz.Extensions.Redis
quartz.jobStore.lockHandler.redisConfiguration = localhost:6379
Properties
KeyPrefix
Gets or sets the prefix for Redis lock keys.
public string KeyPrefix { get; }
Property Value
Remarks
Defaults to "quartz:lock:". The full key format is
{KeyPrefix}{SchedulerName}:{lockName}.
LockRetryInterval
Gets or sets the polling interval between SET NX retry attempts.
[TimeSpanParseRule(TimeSpanParseRule.Milliseconds)]
public TimeSpan LockRetryInterval { get; }
Property Value
Remarks
Defaults to 100 milliseconds.
LockTimeToLive
Gets or sets the lock time-to-live.
[TimeSpanParseRule(TimeSpanParseRule.Milliseconds)]
public TimeSpan LockTimeToLive { get; }
Property Value
Remarks
Defaults to 30 seconds. The lock automatically expires after this duration, allowing recovery when a node crashes while holding a lock.
RedisConfiguration
Gets or sets the StackExchange.Redis configuration string.
public string RedisConfiguration { get; }
Property Value
Remarks
Defaults to "localhost:6379".
RequiresConnection
Whether this lock handler requires a database connection for its lock management operations.
public bool RequiresConnection { get; }
Property Value
- See Also
SchedulerName
Gets the scheduler name used to namespace Redis lock keys.
public string? SchedulerName { get; }
Property Value
Remarks
Told to the handler by the job store through Initialize(LockHandlerContext).
Methods
AcquireLock(Guid, ConnectionAndTransactionHolder?, SchedulerLock, CancellationToken)
Grants a lock on the identified resource to the calling context, waiting until it is available.
public ValueTask<bool> AcquireLock(Guid requestorId, ConnectionAndTransactionHolder? conn, SchedulerLock lockKind, CancellationToken cancellationToken = default)
Parameters
requestorIdGuidIdentifies the calling context, so that a re-entrant acquire can be told from a competing one.
connConnectionAndTransactionHolderThe unit of work to take the lock on. It is null when the store has not opened a connection yet, which it delays for a handler whose RequiresConnection is false.
lockKindSchedulerLockWhich of the two locks to take.
cancellationTokenCancellationTokenThe cancellation instruction.
Returns
- ValueTask<bool>
true if this call took the lock and its caller must release it; false if
requestorIdalready held it and must not.
Remarks
The answer is a release obligation rather than a report of success. true
says this call took the lock and its caller is the one that has to give it back;
false says requestorId already held it — a re-entrant
acquire — so the outer caller's single release is what frees it, and releasing here would
drop a lock that operation is still relying on. A handler that instead counts its holds may
answer true to a re-entrant acquire, because there the inner release is
the one that decrements; either way the caller releases exactly when it was told
true.
Re-entry is the only thing false may mean. A handler that could not
take the lock says so by throwing: LockException when the lock was refused,
and OperationCanceledException when
cancellationToken fired. Answering false on
cancellation is not a harmless approximation — the job store reads it as "already held, do
not release" and goes on to run the guarded operation with no lock at all, which is the whole
point of taking one.
The store's own ordering must not be relied on to cover for a handler that gets this wrong. A handler answering false to RequiresConnection is followed immediately by a connection open on the same token, so a mistake here is masked today by that open throwing first. That is an accident of statement order rather than a guarantee, and it does not hold wherever the store reaches the lock in a different sequence.
A handler that gives up leaves nothing behind: an abandoned wait must not consume a handover meant for the next waiter, and a partially taken lock is released before the exception escapes, so that the next caller can still be served.
Exceptions
- OperationCanceledException
cancellationTokenfired before the lock was taken.- LockException
The lock could not be obtained.
Initialize(LockHandlerContext)
Called once by the job store before the lock handler is used, telling the handler which scheduler it locks for. The default implementation does nothing, which suits a handler that does not key its locks by scheduler identity.
public void Initialize(LockHandlerContext context)
Parameters
contextLockHandlerContext
ReleaseLock(Guid, SchedulerLock, CancellationToken)
Release the lock on the identified resource if it is held by the calling thread.
public ValueTask ReleaseLock(Guid requestorId, SchedulerLock lockKind, CancellationToken cancellationToken = default)
Parameters
requestorIdGuidlockKindSchedulerLockcancellationTokenCancellationToken
Returns
Shutdown(CancellationToken)
Called once by the job store, after the store has released its own resources, telling the handler to release whatever it opened. The default implementation does nothing, which suits a handler that holds nothing of its own.
public ValueTask Shutdown(CancellationToken cancellationToken = default)
Parameters
cancellationTokenCancellationTokenThe cancellation instruction.
Returns
Remarks
Named for Shutdown(CancellationToken) rather than for IAsyncDisposable, because it is the store's shutdown that reaches it and a handler is never used again afterwards. It is a default interface member, so a handler written against 4.0's first shape need not implement it — one that holds nothing has nothing to say here.
It runs after the lock this handler guards has stopped being asked for, so an implementation may assume no acquire is in flight. Throwing is not fatal: the store logs it and finishes shutting down, because a scheduler that cannot complete its shutdown is worse than a connection that outlives it.