# scheduling Companion project for **@Scheduled, ShedLock and Distributed Cron: Scheduling That Survives Three Replicas** on [ankurm.com](https://ankurm.com). Three replicas of one Spring Boot application run against one PostgreSQL database, and the `job_execution` table records who ran what and when. Counting rows is the measurement. ## Verified stack | Component | Version | Source of the number | |---|---|---| | JDK | 25.0.4.1+1 (Temurin) | `java -version` | | Spring Boot | 4.1.1 | `maven-metadata.xml` on Maven Central | | Spring Framework | 7.0.9 | `spring-boot-dependencies-4.1.1.pom` | | ShedLock | 7.9.0 | `maven-metadata.xml`; **not managed by Boot**, so it is pinned in `pom.xml` | | PostgreSQL | 14.24 | the broker the transcripts ran against; `docker-compose.yml` uses 17 | ## Quickstart ```bash scripts/postgres.sh start # throwaway PostgreSQL 14 on :55432, no Docker, no root scripts/three-replicas.sh unlocked # 24 executions where 8 were due scripts/three-replicas.sh locked # 7 executions for 7 ticks mvn test # scheduler-pool and clock-skew evidence scripts/run-all.sh # regenerates every docs/output/ file ``` With Docker instead: ```bash docker compose up --build # unlocked PROFILE=locked docker compose up --build # locked ``` ## Profiles | Profile | What is active | Result | |---|---|---| | `unlocked` | `@SchedulerLock` on the job, and nothing else | every replica runs every tick | | `locked` | `LockConfiguration`: `@EnableSchedulerLock` + a `LockProvider` with `usingDbTime()` | one execution per tick | | `appclock` | the same, with a `LockProvider` that has no `usingDbTime()` | used by the clock-skew experiment | | `poolprobe` | three competing `@Scheduled` methods | used by the scheduler-pool tests | ## Documentation | Chapter | What it settles | |---|---| | [01 Three replicas, three executions](docs/01-three-replicas-three-executions.md) | The failure, and why `@SchedulerLock` alone does nothing | | [02 The lock](docs/02-the-lock.md) | How one row and one conditional `UPDATE` fix it, and what the two durations mean | | [03 One scheduler thread](docs/03-one-scheduler-thread.md) | `pool.size=1`: the same execution count, arriving in a burst | | [04 Clock skew](docs/04-clock-skew.md) | A 40-second-fast node taking a live lock, and `usingDbTime()` refusing it | | [05 When not to use a lock](docs/05-when-not-to-use-a-lock.md) | Idempotence, Kubernetes `CronJob`, Quartz, and a pre-ship checklist | ## Captured output | File | What it shows | |---|---| | [`three-replicas-unlocked.txt`](docs/output/three-replicas-unlocked.txt) | 8 + 8 + 8 executions, 9 overlapping pairs, an empty lock table | | [`three-replicas-locked.txt`](docs/output/three-replicas-locked.txt) | 7 executions, 0 overlaps, one lock row | | [`scheduler-pool-1.txt`](docs/output/scheduler-pool-1.txt) | 40 executions, longest gap 1995 ms, 35 of them in a burst | | [`scheduler-pool-4.txt`](docs/output/scheduler-pool-4.txt) | 41 executions, longest gap 201 ms, no burst | | [`clock-skew.txt`](docs/output/clock-skew.txt) | Two holders of one lock, and `usingDbTime()` preventing it | | [`tests.txt`](docs/output/tests.txt) | The test run behind the last three | ## A note on the database `scripts/postgres.sh` downloads the PostgreSQL 14 `.deb` packages and unpacks them into `target/pg` — no Docker, no root, no system-wide install. It exists because the transcripts had to be reproducible on a machine with neither. If you have Docker, `docker-compose.yml` is simpler.