Files
spring-async-demo/scheduling/README.md
Ankur Mhatre 6af6e4a3f1 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().
2026-09-01 23:28:06 +05:30

71 lines
3.5 KiB
Markdown

# 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.