You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
docs(ui): scenario picker — all four cutover topologies, spelled out
The four supported scenarios, now selectable on the Guide tab (persisted)
with per-scenario checklists, and as a decision matrix in the RUNBOOK:
1. Two clusters, no mirroring (plain switch) -> bound UNSET; top-up
chases new old-cluster data until the final drain.
2. Two clusters, mirror old->new (old primary) -> bound SET at the tee
flip; detect + one-click apply; badge on every pod; sync parity during
validation; GDPR/merge re-apply before sign-off.
3. Two clusters, mirror new->old (rollback net) -> same machinery as 2;
the detector is direction-agnostic (ClickHouse starts cold either way).
4. Single cluster, in-place upgrade -> bound UNSET; the
upgrade froze mongo drill collections; live-parallel path with
backpressure protecting the production ClickHouse.
The picker states the one decision that changes configuration — tee or no
tee — because a wrong bound setting is the one mistake the tool cannot
detect afterwards (unset with a tee = undetectable duplicates; set without
one = orphaned new data).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: docs/RUNBOOK.md
+18Lines changed: 18 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -84,6 +84,24 @@ incident.
84
84
drill → optionally `bench/seed-failures.ts` for a full failure-scenario drill
85
85
(breaker, DLQ, monitor, retry-failed).
86
86
87
+
## Choose your scenario first
88
+
89
+
The one decision that changes the configuration is whether a TEE mirrors
90
+
the same requests into both stacks. Everything else is shared machinery.
91
+
92
+
| # | Topology | LEDGER_CD_UPPER_BOUND | Ingestion switch | New data arriving in old Mongo | Sign-off |
93
+
|---|---|---|---|---|---|
94
+
| 1 | Two clusters, **no mirroring** (plain switch) |**UNSET**| Before the migration (cutover-first) or after the bulk (bulk-before-cutover + final drain) |**Migrated** — top-up passes chase it until the drain finds nothing | Verify + audits, DLQ = 0 |
95
+
| 2 | Two clusters, **mirror old → new** (old primary) |**SET** = tee flip | At customer sign-off |**Never migrated past the bound** — it is the tee's copy (different _id/cd; duplicates would be undetectable) | Verify + audits for pre-bound; dashboard comparison + sync parity for post-bound |
96
+
| 3 | Two clusters, **mirror new → old** (new primary, old = rollback net) |**SET** = the moment new became primary | Already happened at the flip | Same as 2 — post-flip old-side docs are mirror copies | Same as 2 |
97
+
| 4 |**Single cluster, in-place upgrade** (drill mongo → ClickHouse in background) |**UNSET**| The upgrade itself is the switch; old drill collections freeze | Transition tail drained by top-up; no tee → nothing to duplicate | Verify + audits, DLQ = 0 (live-parallel path; backpressure protects prod CH) |
98
+
99
+
Scenario is also selectable on the dashboard's **Migration Guide** tab —
100
+
it renders the per-scenario checklist and states the bound requirement.
101
+
For 2 and 3: use **Detect boundary** + **Apply this bound to the run**
102
+
(one click covers all pods), verify the `bounded · cd < …` badge on every
103
+
pod, and keep re-running sync parity during the validation window.
104
+
87
105
## Tee-mirror cutover (customer keeps the old architecture until sign-off)
88
106
89
107
For customers who require approval before switching: the old arch stays
<h2>Which cutover is this? <span class="hint">— pick once; the settings below are per-scenario, and the wrong bound setting is the one mistake this tool cannot detect afterwards</span></h2>
{ id: 'switch', name: '1 \u00b7 Two clusters, no mirroring',
816
+
bound: false,
817
+
html: '<p><b>Plain ingestion switch</b> (cutover-first, or bulk-before-cutover with a final drain).</p>' +
818
+
'<ul>' +
819
+
'<li><b>LEDGER_CD_UPPER_BOUND: LEAVE UNSET.</b> The migration must take everything, including data still arriving in the old cluster \u2014 top-up passes chase it until the final drain finds nothing new.</li>' +
820
+
'<li>Cutover-first: switch SDK ingestion to the new cluster, then run the migration (old drill data is frozen). Bulk-before-cutover: run the bulk first, switch ingestion, then let the final top-up pass drain the tail.</li>' +
821
+
'<li>Ignore the Tee boundary card \u2014 it is for mirrored setups only. Applying a bound here would ORPHAN newly arrived data.</li>' +
{ id: 'tee-old', name: '2 \u00b7 Mirror old \u2192 new',
825
+
bound: true,
826
+
html: '<p><b>Old cluster stays primary; nginx tees the same requests to the new stack</b>, which re-ingests them with its own logic (same event = different _id/cd on each side \u2014 nothing downstream can dedup across that seam).</p>' +
827
+
'<ul>' +
828
+
'<li>Flip the tee inside a ~60s old-ingestion pause (SDKs queue and retry \u2014 lossless). The pause creates a sharp boundary.</li>' +
829
+
'<li>Run <b>Detect boundary</b> (Overview tab) BEFORE the migration maps anything: a green gap = exact seam. Click <b>Apply this bound to the run</b> \u2014 one click covers every pod \u2014 or set <code>LEDGER_CD_UPPER_BOUND</code> in the deployment config.</li>' +
830
+
'<li><b>Every pod must show the</b> <code>bounded \u00b7 cd < \u2026</code> <b>badge.</b> A pod without it migrates post-flip data = undetectable duplicates.</li>' +
831
+
'<li>During validation, re-run the boundary card\u2019s sync parity regularly: nginx mirror drops silently when the secondary is down; flagged hours are bounded backfill windows.</li>' +
832
+
'<li>GDPR erasures / user merges done on the old side during validation must be re-applied on the new stack before sign-off.</li>' +
833
+
'</ul>' },
834
+
{ id: 'tee-new', name: '3 \u00b7 Mirror new \u2192 old',
835
+
bound: true,
836
+
html: '<p><b>New cluster is already primary; nginx mirrors back to the old stack</b> as the customer\u2019s rollback safety net during validation.</p>' +
837
+
'<ul>' +
838
+
'<li>Everything from scenario 2 applies unchanged \u2014 detection, bound, badge, sync parity. ClickHouse is the store that started cold in both directions, so the detector does not care which side is primary.</li>' +
839
+
'<li>The bound = the moment the new cluster became primary. Old-cluster docs after it are the mirror\u2019s copies \u2014 never migrate them.</li>' +
840
+
'<li>Post-flip data quality is validated by comparing dashboards between stacks, not by this tool: this migration owns only the pre-flip history.</li>' +
841
+
'</ul>' },
842
+
{ id: 'inplace', name: '4 \u00b7 Single cluster, in-place upgrade',
843
+
bound: false,
844
+
html: '<p><b>One cluster upgraded to the new architecture</b>; old drill_events collections stay in the same MongoDB and are migrated into ClickHouse in the background while live traffic already flows into the same table.</p>' +
845
+
'<ul>' +
846
+
'<li><b>LEDGER_CD_UPPER_BOUND: LEAVE UNSET.</b> The upgrade itself froze the Mongo drill collections (new events go to ClickHouse natively); if a transition tail trickled in, top-up drains it. There is no tee, so there is nothing to duplicate.</li>' +
847
+
'<li>Live rows and migrated rows share the table safely: live cd is post-upgrade, migrated cd is historical \u2014 verify and the audits are built on that boundary (this is the tested live-parallel path).</li>' +
848
+
'<li>Backpressure matters most here \u2014 migration ATTACHes into the same ClickHouse serving production; the built-in throttle yields before parts pressure reaches live inserts.</li>' +
849
+
'<li>Sign-off: Verify + Audit vs source + content audit, DLQ pending = 0. Retention TTL deletions on old data show as drift, not defects.</li>' +
850
+
'</ul>' },
851
+
];
852
+
function renderScenario() {
853
+
var sel = localStorage.getItem('mig-scenario') || '';
'<p><b>Bound setting for this scenario: ' + (cur.bound ? 'REQUIRED \u2014 <code>LEDGER_CD_UPPER_BOUND</code> set (or applied from the boundary card)' : 'must stay UNSET') + '</b></p></div>'
861
+
: '<div class="empty">Select the scenario \u2014 it decides whether the cd bound must be set (mirrored setups) or must stay unset (everything else).</div>';
862
+
}
863
+
function pickScenario(id) { localStorage.setItem('mig-scenario', id); renderScenario(); }
0 commit comments