When you run multiple replicas of your application — for example in a Kubernetes deployment — every instance ofDocumentation Index
Fetch the complete documentation index at: https://mintlify.com/Indroic/HexCore/llms.txt
Use this file to discover all available pages before exploring further.
DynamicScheduler will tick at the same time and could enqueue the same cron job twice. HexCore’s ILockProvider interface solves this by letting you acquire an atomic, TTL-backed distributed lock before each job is submitted. This guide explains both built-in implementations — RedisLockProvider and PostgresLockProvider — and shows how to wire them into DynamicScheduler.
The ILockProvider Interface
Both providers implement the same two-method contract from hexcore.domain.cqrs.cron:
Atomically tries to take the lock. Returns
True if this replica now owns it, False if another replica got there first. The ttl_seconds acts as a safety net — the lock expires automatically even if release_lock is never called (e.g. on a crash).Deletes the lock immediately so the next scheduled tick can proceed without waiting for the TTL. Safe to call even if the lock no longer exists.
RedisLockProvider
RedisLockProvider uses a single Redis command — SET key "locked" NX EX ttl — which is atomic on the Redis server side. It requires a redis.asyncio.Redis client instance:
How it works internally
NX— only write if the key does not already exist (atomic compare-and-set)EX ttl_seconds— auto-expire ensures no deadlock if a replica crashes before callingrelease_lock
Full example
src/infrastructure/scheduler.py
PostgresLockProvider
Use
PostgresLockProvider when your stack already includes PostgreSQL and you want zero additional infrastructure. No Redis required — locks are stored in a dedicated table that HexCore creates for you.PostgresLockProvider uses an asyncpg pool or connection and relies on a hexcore_cron_locks table with an ON CONFLICT DO UPDATE WHERE expires_at < NOW() upsert pattern — fully atomic in a single SQL statement:
setup() — create the locks table
Custom table name
If you need the lock table in a specific schema or want a different name, passtable_name:
How the upsert lock works
- If the row does not exist → insert and return the row → lock acquired
- If the row exists and has expired → update and return the row → lock acquired
- If the row exists and has not expired →
WHEREclause blocks the update → no row returned → lock not acquired
Full example
src/infrastructure/scheduler.py
Choosing a Provider
| Concern | RedisLockProvider | PostgresLockProvider |
|---|---|---|
| Infrastructure | Requires Redis | PostgreSQL only |
| Atomicity mechanism | SET NX EX | SQL ON CONFLICT ... WHERE |
| TTL enforcement | Native Redis TTL | Checked on next acquire |
| Crash safety | Auto-expires via TTL | Auto-expires via WHERE expires_at < NOW() |
| Setup required | None | await lock_provider.setup() |
Injecting a Lock Provider into DynamicScheduler
Both providers share the same injection point:
lock_provider is set, the scheduler calls acquire_lock before enqueuing each job. If the lock cannot be acquired, the job is silently skipped on that tick — another replica already claimed it. The lock is released automatically after the job is enqueued (or on error).