6.5 KiB
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 covers the deferred ES path).
Track: backend (infra) · Depends on: Phase 6 (6.1 settlement is a reconciliation job here) · Unlocks: unattended ops; >1 API instance Before you start, read ../../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 § post-phase-4 and the topology in ../server/runtime-services.md § 5, 7.
Verified state today:
- Two in-process
PeriodicTimerhosted services only: booking-request expiry (1 min) + notification retention (24 h). NoIJobSchedulerinterface 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-processMemoryCache;IDistributedLock= per-keySemaphoreSlim(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 § post-phase-4 (items 4.1–4.3).
- ../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's refund
settlement reconciliation, and Phase 8'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
processmanual until trust is earned. Jobs are already idempotent by design. - 7.2 — Redis for
ICacheService+IDistributedLock(only when scaling past one instance). AddStackExchange.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 updatein CI, or a--migrateone-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
IJobSchedulerregistry 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 testgreen.
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.mdstartup wiring (scheduler) + the mocks-registry rows. Update ../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.