Cutover: stopping the old server is what removes the guard, so add one
Step 2 stops stalwart.service and step 4 points the fork at a store path. Between those two moments nothing protects the original, and the rehearsal had already shown that one open by the fork costs the rollback for good. What protects it until then turns out to be the running server itself: RocksDB refuses a second opener with "While lock file: LOCK: Resource temporarily unavailable". So the intuition that a service shutdown prevents the mistake is backwards — the shutdown is what enables it. Measured, in probe_guard.py, in the three states that matter: held by the running server, the fork is refused and the rollback is intact; stopped but read-only, the fork is refused while rotating its own log and the Enterprise build still starts on it afterwards; stopped and writable, the fork opens, adds its column family, and upstream never starts again. So step 3 now makes the original read-only as soon as the copy is taken, which turns a discipline problem into a one-line one, and the answered section carries the table.
This commit is contained in:
@@ -87,6 +87,15 @@ unprivileged process hold port 25.
|
||||
3. Copy `/opt/stalwart/data` to the fork's store path. A copy, not a move:
|
||||
the original is the rollback. Budget the disk for two full copies, and
|
||||
take it from the stopped server, never from under a running one.
|
||||
|
||||
**Then make the original read-only, before the fork exists on this host**
|
||||
(`chmod -R a-w`, or `chattr +i` on the directory, or keep it on a
|
||||
read-only bind mount). Until this moment the running server's own RocksDB
|
||||
lock was what stopped anything else opening that store; stopping it takes
|
||||
that away exactly when a store path is about to be typed. A read-only
|
||||
original refuses the fork harmlessly and still starts under the
|
||||
Enterprise build — measured, `tools/fork/cutover-rehearsal/probe_guard.py`.
|
||||
Undo it only if you are rolling back.
|
||||
4. Install the fork at its own path with its own config, pointing at the
|
||||
copied store. Most settings travel inside the store — they live in the
|
||||
registry — so the config file is mainly the store path and the hostname.
|
||||
@@ -234,6 +243,19 @@ database with `create_missing_column_families(true)`, so it creates `_` on
|
||||
first open. Upstream has no descriptor for it, and RocksDB will not open a
|
||||
database holding a column family it was not told about.
|
||||
|
||||
The mistake is worth guarding mechanically rather than carefully, because
|
||||
the guard that exists today is removed by step 2. Three states, measured
|
||||
(`probe_guard.py`):
|
||||
|
||||
| The original store is | The fork | The rollback |
|
||||
|---|---|---|
|
||||
| held by the running server | refused by RocksDB's lock | intact |
|
||||
| stopped, read-only | refused while rotating its own log | intact |
|
||||
| stopped, writable | **opens, and adds `_`** | **gone** |
|
||||
|
||||
So the window of exposure opens the moment `stalwart.service` stops and
|
||||
closes when the original is made read-only. Keep it short.
|
||||
|
||||
Three consequences:
|
||||
|
||||
- The failure is a hard one, at startup, exit code 1, **before any data is
|
||||
|
||||
Reference in New Issue
Block a user