# Refinement Phase 7 — Unattended operation: scheduler, locking & multi-instance readiness > **Mission:** make the platform run itself. Today only two recurring jobs exist; the **weekly payout batch, > credential-expiry scan, EVV no-show sweep, and Moadian reconciliation are admin-click-only** while their > cadence config keys sit unread — so **nurses are not paid unless an operator clicks**. This phase adds a real > scheduler, and (only when you need more than one API instance) shared cache + lock, and splits migrations from > boot. > > **This is where "do I need Redis / Elasticsearch?" gets answered: no, not for a single-instance MVP.** Redis > becomes necessary the moment you run a **second** API instance (shared cache invalidation + cross-instance > money lock). Elasticsearch is **never** MVP — SQL search is real and correct > ([Phase 9](refinement-phase-9-observability-and-scale.md) covers the deferred ES path). > > **Track:** backend (infra) · **Depends on:** [Phase 6](refinement-phase-6-money-correctness.md) (6.1 > settlement is a reconciliation job here) · **Unlocks:** unattended ops; >1 API instance > **Before you start, read [../../phases/_shared/agent-operating-rules.md](../../phases/_shared/agent-operating-rules.md).** ## 1. Context This phase **is** the server audit's **post-phase-4**. Full evidence & fixes: **[../server/post-phase-backend-plan.md](../server/post-phase-backend-plan.md) § post-phase-4** and the topology in **[../server/runtime-services.md](../server/runtime-services.md)** § 5, 7. Verified state today: - Two in-process `PeriodicTimer` hosted services only: booking-request expiry (1 min) + notification retention (24 h). **No `IJobScheduler` interface actually exists** (the registry name is aspirational); **no Hangfire/Quartz** package is referenced. - The verification credential-expiry scan, EVV no-show sweep, weekly **payout batch generation**, and Moadian reconciliation are **admin-manual endpoints** whose seeded cadence keys (`verification_expiry_scan_cadence_hours`, `no_show_scan_cadence_hours`, `nurse_payout_interval_days`) are never read on a schedule. - `ICacheService` = in-process `MemoryCache`; `IDistributedLock` = per-key `SemaphoreSlim` (one process only). The DB uniques/state-machines are the money-correctness backstop either way, but cache + lock **silently degrade** the moment a second instance runs. - Migrations run on boot (`MigrateAsync`) → concurrent multi-node start-up races on DDL; the app login needs permanent DDL rights. ## 2. Required reading - **[../server/post-phase-backend-plan.md](../server/post-phase-backend-plan.md)** § post-phase-4 (items 4.1–4.3). - **[../server/runtime-services.md](../server/runtime-services.md)** § 5 (Redis), § 7 (job scheduler), Deployment notes 1 & 4. - The mocks-registry rows for `IJobScheduler`, `ICacheService`, `IDistributedLock`. ## 3. Scope — deliver plan items 4.1–4.3 - **7.1 — real job scheduler + register the deferred crons.** Adopt Hangfire (SQL Server storage — **no new infra**) or Quartz; re-home the two existing sweeps and add recurring jobs for: credential-expiry scan, EVV no-show sweep, **weekly payout-batch generation**, [Phase 6](refinement-phase-6-money-correctness.md)'s refund settlement reconciliation, and [Phase 8](refinement-phase-8-external-rails.md)'s Moadian poll — each reading its seeded cadence key. **Keep the admin manual triggers as overrides.** Money-*moving* payout processing can stay human-approved — schedule *generation*, keep `process` manual until trust is earned. Jobs are already idempotent by design. - **7.2 — Redis for `ICacheService` + `IDistributedLock` (only when scaling past one instance).** Add `StackExchange.Redis`; `RedisCacheService` (same key/TTL scheme) + `RedisDistributedLock` (SET NX PX + token-checked release, lease ≥ the longest money handler), config-selected registration. **Single-instance MVP does not need this** — document it as the gate for horizontal scaling; keep DB uniques authoritative. - **7.3 — separate migrations from boot** (multi-instance + least privilege). A deploy-time migration step (`dotnet ef database update` in CI, or a `--migrate` one-shot mode) + a boot-time schema *check* instead of an apply; seeders become idempotent deploy steps. (Local dev can keep boot-migrate for convenience — gate the behavior by environment.) ## 4. Mocks & seams - The two hosted services move behind the real scheduler; the `IJobScheduler` registry row becomes "recurring jobs (Hangfire/Quartz)". - Redis is introduced **only if 7.2 is in scope for your deployment** — otherwise the in-process cache/lock stay and this is documented as the scale-out gate. ## 5. Critical rules - **Jobs must stay idempotent** (they are) — a scheduler retry must never double-pay or double-post. - **Keep payout *processing* human-approved at first** — schedule generation, not money movement, until trust is earned. - **Don't add Redis "because."** It's required only for >1 instance; adding it single-instance is complexity without payoff. Same for ES (never MVP). - **The DB uniques remain the correctness backstop** even with a real distributed lock. ## 6. Definition of Done - [ ] A real scheduler runs the two existing sweeps + the four deferred crons (each reading its cadence key); admin manual triggers still work as overrides. - [ ] Payout *generation* is scheduled; *processing* remains an explicit admin action. - [ ] (If scaling) Redis-backed cache + lock behind a config switch, with the DB uniques still authoritative; otherwise the single-instance limitation is documented as the scale-out gate. - [ ] Migration-on-boot is environment-gated; a deploy-time migration path exists. `dotnet build`/`dotnet test` green. ## 7. How to test - Set a short cadence in config → observe the payout-batch-generation / expiry-scan / no-show jobs fire on schedule (logs) and produce the same result as the admin manual trigger. - (If Redis) run two API instances against one Redis → a cache invalidation on instance A is seen by B, and the money lock is held across both. - A one-shot `--migrate` (or CI step) applies migrations; a normal boot only *checks* schema. ## 8. Hand off & document - Update `server/CLAUDE.md` startup wiring (scheduler) + the mocks-registry rows. Update [../server/runtime-services.md](../server/runtime-services.md) if the deployment topology changes (scheduler storage, optional Redis). Save a memory note: Redis = scale-out gate, ES = never MVP, scheduler now runs the deferred crons.