Add the scheduling module
Three replicas of one application against one PostgreSQL database, proving duplicate @Scheduled execution and then removing it with ShedLock: 24 executions where 8 were due, then 7 for 7 ticks. Also measured: @SchedulerLock without @EnableSchedulerLock does nothing and warns about nothing; spring.task.scheduling.pool.size=1 does not starve a fixedRate job but delays it and fires 35 of 40 executions in a burst; and a node whose clock is 40 seconds fast takes a live lock unless the provider uses usingDbTime().
This commit is contained in:
46
scheduling/docs/05-when-not-to-use-a-lock.md
Normal file
46
scheduling/docs/05-when-not-to-use-a-lock.md
Normal file
@@ -0,0 +1,46 @@
|
||||
prev: [Clock skew](04-clock-skew.md) · [README](../README.md)
|
||||
|
||||
# 5. When not to use a lock
|
||||
|
||||
ShedLock is small, it has one dependency on your database, and it solves the stated problem. It
|
||||
is still worth asking whether the problem should exist.
|
||||
|
||||
**Make the job idempotent instead.** A lock is a way of avoiding a second execution. Idempotence
|
||||
is a way of not caring about one. If the job's work can be keyed — "mark invoices dated
|
||||
2026-09-01 as sent, where they are not already marked" — then three replicas running it produce
|
||||
one outcome and you have removed a distributed-systems dependency rather than adding one. This is
|
||||
almost always the better engineering, and it is almost never what the article you searched for
|
||||
suggests.
|
||||
|
||||
**A lock is not a guarantee of exactly-once.** It is a guarantee of at-most-one-per-lock-window,
|
||||
which is different. If the holder dies halfway, the job did not complete and nothing retries it —
|
||||
ShedLock has no notion of failure, only of expiry. Pairing it with a job table that records
|
||||
completion is what makes "exactly once" true, and at that point the job table is doing most of
|
||||
the work.
|
||||
|
||||
**Consider the scheduler you already have.** A Kubernetes `CronJob` runs one pod per schedule and
|
||||
needs no lock, no library and no table; it costs you a pod start per run and the schedule lives
|
||||
outside the application. Quartz in clustered mode owns misfire policy, persistence and recovery,
|
||||
at the price of eleven tables and a great deal of configuration. ShedLock sits between them: it
|
||||
adds a lock to the scheduler you already have, and deliberately adds nothing else.
|
||||
|
||||
**A lock provider needs its store to be consistent.** The JDBC provider is safe because a
|
||||
conditional `UPDATE` on one row is atomic in every relational database. The Redis provider is
|
||||
safe on a single instance and, as its own documentation notes, is subject to the well-known
|
||||
argument about locks over Redis replication. If your store is eventually consistent, your lock
|
||||
is too.
|
||||
|
||||
## A short checklist for the version you ship
|
||||
|
||||
- `@EnableSchedulerLock` present, and a `LockProvider` bean present — without both, the
|
||||
annotation does nothing ([chapter 1](01-three-replicas-three-executions.md))
|
||||
- `usingDbTime()` on ([chapter 4](04-clock-skew.md))
|
||||
- `lockAtMostFor` longer than the worst-case run time, and you have accepted the gap it implies
|
||||
after a crash
|
||||
- `lockAtLeastFor` non-zero for any job shorter than the interval between two replicas' timers
|
||||
- `spring.task.scheduling.pool.size` raised above 1 ([chapter 3](03-one-scheduler-thread.md))
|
||||
- the lock names are unique per job — two jobs sharing a name share a lock, and one of them stops
|
||||
running
|
||||
- the method carrying `@SchedulerLock` is called from outside its own class, and is not `final`
|
||||
|
||||
[README](../README.md)
|
||||
Reference in New Issue
Block a user