104 lines
6.5 KiB
Markdown
104 lines
6.5 KiB
Markdown
# 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.
|