Repository navigation
feat(omnigraph): add the 0.13 plan gate and the nightly stop window - #6355
Merged
Merged
Conversation
At storage format 14 the stack creates no cluster-apply Job and suspends the optimize and cleanup CronJobs, because both are refused beside a serving 0.12+ server. That left a format-14 environment with no pre-deploy check on a schema or policy change and no compaction or version cleanup. This adds, gated on the served format like the lock handoff: - a read-only `cluster plan` Job in the apply Job's slot, which the Deployment depends on, so a change the entrypoint's apply would refuse fails the update while the old pod is still serving; - one `omnigraph-maintenance` CronJob in place of optimize and cleanup. It scales the server to 0 for a nightly window, under its own IRSA ServiceAccount with a Role limited to the unlock Lease, the pod list and the server Deployment's scale; - the `ol.mit.edu/migration-armed` annotation on the Deployment, which the window reads so it does not scale the server back up under an armed migration; - a council probe schedule that skips the window's hour. Both commands are in the omnigraph-server image (mitodl/agent-kit#480). Environments on format 9 get a second ServiceAccount on the IRSA role and no other change. Claude-Session: https://claude.ai/code/session_01W4TjT25fD1W4TS1uRUY1hC
Contributor
There was a problem hiding this comment.
🟡 Changes recommended
Replica field ownership, configuration validation, wiring coverage, and alert guidance need correction before rollout.
4 open findings
What changed in this PR
Adds format-14 Omnigraph deployment safeguards and nightly maintenance.
Changes:
- Adds a pre-deploy plan gate and stop-window CronJob.
- Adds RBAC, migration coordination, probe scheduling, and monitoring.
- Adds tests and operational runbooks.
| File | Description |
|---|---|
tests/ol_infrastructure/applications/omnigraph/test_storage_migration_job.py |
Updates maintenance writer fixtures. |
tests/ol_infrastructure/applications/omnigraph/test_maintenance_window.py |
Tests gate and maintenance resources. |
src/ol_infrastructure/infrastructure/grafana_alerting/metric_rules/witan.py |
Monitors maintenance staleness. |
src/ol_infrastructure/applications/omnigraph/storage_migration.py |
Supports variable maintenance writers. |
src/ol_infrastructure/applications/omnigraph/maintenance.py |
Implements the stop window. |
src/ol_infrastructure/applications/omnigraph/lock_handoff.py |
Implements the plan gate. |
src/ol_infrastructure/applications/omnigraph/data_tier.py |
Wires format-dependent resources. |
src/ol_infrastructure/applications/omnigraph/council_probe.py |
Skips the maintenance hour. |
src/ol_infrastructure/applications/omnigraph/__main__.py |
Adds configuration and service accounts. |
docs/omnigraph-store-maintenance-runbook.md |
Documents maintenance and recovery. |
docs/omnigraph-storage-format-upgrade-runbook.md |
Updates format-14 cutover guidance. |
🧠 Review effort: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
…gating The maintenance window writes `spec.replicas` through the scale subresource, which makes its client a second field manager of that field. Server-side apply then refuses a Pulumi update that changes the count, and the update that does is the one arming a migration. The Deployment now carries `pulumi.com/patchForce` wherever a window exists. Nothing ran the format and migration branches of `create_data_tier`, so a plan gate created during a migration, or one missing at format 14, would have passed every test. `test_data_tier_wiring.py` runs the whole function under Pulumi mocks for format 9, format 14 and an armed migration from each. Also: `omnigraph:cleanup_iso_weekday: 0` was read as unset and became Sunday without reaching the range check, and the daily staleness alert's description now says what `omnigraph-maintenance` is and where its recovery steps are. Claude-Session: https://claude.ai/code/session_01W4TjT25fD1W4TS1uRUY1hC
This was referenced Oct 13, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


What are the relevant tickets?
N/A (witan task tk-ol-infrastructure-omnigraph-stack-for-0-12-clust-2dee94, part of the omnigraph 0.13 upgrade. Spec: mitodl/agent-kit#461 D4 and D5)
Description (What does it do?)
Since #6351, a stack at storage format 14 creates no cluster-apply Job and suspends the optimize and cleanup CronJobs, because both are refused with
state_lock_heldbeside a serving 0.12+ server. That left a format-14 environment with no pre-deploy check on a schema or policy change, and no compaction or version cleanup.This adds the two replacements, gated on
internal_schema_version >= 14like the lock handoff. Both run commands baked into the omnigraph-server image by mitodl/agent-kit#480.create_plan_gateinlock_handoff.py) in the apply Job's slot. It runsomnigraph_plan_gate.pywith the new image against the served root, and the Deployment depends on it. The entrypoint applies only afterRecreatehas stopped the old pod, so without the gate a change the apply refuses is first seen by a pod that cannot start. Not created while a migration is armed, for the reason the apply Job is not.omnigraph-maintenanceCronJob (create_maintenance_windowinmaintenance.py) in place of optimize and cleanup, which are deleted at format 14. It runsomnigraph_maintenance.py: scale the server to 0, take over itsservelock,optimizeevery graph (andcleanupon Sundays), scale back up. 03:20 UTC,omnigraph:maintenance_scheduleto move it,omnigraph:cleanup_iso_weekdayfor the cleanup day.omnigraph-maintenance, added to the existing IRSA role for the same S3 access. Its Role allowsget/updateon theomnigraph-unlockLease,liston pods,geton theomnigraph-serverDeployment andget/updateon itsscale. Bound to the server's ServiceAccount, those verbs would let the server scale itself.ol.mit.edu/migration-armedwhile a migration is armed. The window skips on it and does not scale the server back up if it appears mid-window.*/15 0-2,4-23 * * *. The probe has no Kubernetes credential to read a window marker with, so it skips the hour, and its 04:00 run is the check that the window ended. Settingmaintenance_schedulewithoutcouncil_probe_schedulefails the preview.pulumi.com/patchForceat format 14 (see "Unverified").omnigraph-maintenanceis added to the daily staleness rule inmetric_rules/witan.py.The window is a nightly data-tier outage for every witan user. The time budget from 03:20: no new command after 25 minutes, one command stopped after 10, Job deadline at 36, so it ends before the CI indexer's 04:00 run. I sized that against Production's current sweeps (
kube_job_status_completion_time - kube_job_status_start_time, 30 days to 2026-10-13, beside a live 0.11 server): optimize 55s to 163s, cleanup 329s to 1552s with the last three runs the longest. So a Sunday window in Production may not reach every graph. The command rotates where the cleanup pass starts each week so no graph is starved, the unreached graphs fail the Job, and nothing is left locked. These need re-measuring on 0.13 in CI and QA before Production's cutover, and the budget constants may have to move.On format 9 (CI, QA and Production today) this creates the
omnigraph-maintenanceServiceAccount and adds it to the IRSA role's trust policy. Nothing else should change, and the preview on this PR is the check.Not in this PR, and still needed before a cutover:
WitanGraphQuarantined*ongraph blocked during startup, and an alert on 0 available replicas outside the window. Neither can be validated in Loki until an environment runs 0.13.Unverified:
create_data_tierhas not been through apulumi preview.test_data_tier_wiring.pyruns the whole function under Pulumi mocks for each format, which checks what is created and not how the provider diffs it.spec.replicasthrough the scale subresource, so a later Pulumi update that changesreplicas(arming a migration) could be refused with a server-side-apply field conflict. The Deployment therefore carriespulumi.com/patchForceat format 14. I have not observed the conflict or the forced apply. Checkkubectl get deploy omnigraph-server --show-managed-fieldsafter the first window in CI.pulumi up. The Deployment and the sweeps gain adepends_onedge to the new ServiceAccount, and Pulumi refuses a--targeted apply that leaves such an edge outside the target list.How can this be tested?
uv run pytest tests/ol_infrastructure/applications/omnigraph: 251 passed.test_data_tier_wiring.pyrunscreate_data_tierunder Pulumi mocks for format 9, format 14 and an armed migration from each.test_maintenance_window.pycovers the Role's rules and its subject, the CronJob's command, env, deadline and grace period against what the image reads, the time budget including a late start, and the gate's env and mounts.pre-commit runpasses on the changed files.mypyon the omnigraph application reports the same twotoken_sync.pyerrors asmain.annotations={}on the Deployment that would have shown as an update on every stack (fixed, nowNone), and the items under "Unverified".file://root. Nothing has run against a real cluster or S3. The first run is CI's cutover.https://claude.ai/code/session_01W4TjT25fD1W4TS1uRUY1hC