Prove the capstone through safe failure drills: crash recovery, blocked locks, standby lag/loss, accidental change and PITR, backup corruption detection, configuration failure, load pressure, failover, and a final evidence-based design defense.
Run Failure, Corruption, Restore, Failover, and Performance Drills—and Defend the Design
Prove the capstone through safe failure drills: crash recovery, blocked locks, standby lag/loss, accidental change and PITR, backup corruption detection, configuration failure, load pressure, failover, and a final evidence-based design defense.
Learning outcomes
The capstone is not complete when the architecture diagram looks reasonable. It is complete when controlled failures produce expected detection, recovery, and correctness evidence—and when the team can state where the design still fails. Every drill below targets only the disposable Chapter 24 cluster, standby, restore copy, or backup copy. Never translate “failure injection” into corrupting an unmanaged production data directory.
Execute safe crash, lock, replica-lag, accidental-change, backup-corruption, configuration, and load-pressure drills.
Record detection time, recovery time, data correctness, RPO impact, and residual risk for each failure.
Use PostgreSQL logs/catalog/statistics and business invariants together instead of declaring success from process state alone.
Defend the design's explicit tradeoffs and identify unmet SLOs without hiding them.
Clean up the disposable topology only after evidence and rollback artifacts have been reviewed.
1. Use one drill record for every incident
SET ROLE servicehub_cap_owner;CREATE TABLE IF NOT EXISTS ops.ch24_drill_log ( drill_id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, drill_name text NOT NULL, started_at timestamptz NOT NULL, detected_at timestamptz, recovered_at timestamptz, expected_result text NOT NULL, observed_result text, data_correct boolean, rpo_seconds numeric, rto_seconds numeric, notes text);RESET ROLE;
Do not fabricate values. Start each drill, capture wall-clock timestamps from the lab, and write observed facts afterward. A failed drill is useful engineering evidence when the gap is documented and repaired.
2. Drill A — immediate process loss and crash recovery
pg_ctl stop -m immediate simulates abrupt
server-process loss without clean shutdown. WAL crash recovery
should return the cluster to a consistent transactional state;
it does not recover missing/corrupted storage.
date -upg_ctl -D "$CH24_PRIMARY" stop -m immediate# Observe the process is down, then restart.pg_ctl -D "$CH24_PRIMARY" \ -l "$CH24_ROOT/primary.log" \ -o "-p 55480 -c listen_addresses=127.0.0.1" starttail -n 80 "$CH24_ROOT/primary.log"
Get-Date -AsUTCpg_ctl.exe -D $env:CH24_PRIMARY stop -m immediatepg_ctl.exe -D $env:CH24_PRIMARY ` -l (Join-Path $env:CH24_ROOT "primary.log") ` -o "-p 55480 -c listen_addresses=127.0.0.1" startGet-Content (Join-Path $env:CH24_ROOT "primary.log") -Tail 80
SELECT pg_is_in_recovery();SELECT count(*) AS orders, count(*) FILTER (WHERE status NOT IN ('queued','assigned','in_progress','completed','cancelled')) AS invalid_statusFROM app.work_orders;SELECT conname,convalidatedFROM pg_constraintWHERE conrelid='app.work_orders'::regclassORDER BY conname;
Pass requires server recovery plus valid business state—not just “port 55480 accepts TCP.” Record crash-recovery log evidence and actual RTO.
3. Drill B — blocked transaction and lock queue
BEGIN;UPDATE app.work_ordersSET note = note || '-lock-drill'WHERE tenant_id = ( SELECT tenant_id FROM app.tenants WHERE tenant_code='tenant-a')AND work_order_id = ( SELECT min(work_order_id) FROM app.work_orders WHERE tenant_id = ( SELECT tenant_id FROM app.tenants WHERE tenant_code='tenant-a' ));-- Keep this transaction open until diagnosis is complete.
SET lock_timeout='3s';UPDATE app.work_ordersSET amount = amount + 0.01WHERE tenant_id = ( SELECT tenant_id FROM app.tenants WHERE tenant_code='tenant-a')AND work_order_id = ( SELECT min(work_order_id) FROM app.work_orders WHERE tenant_id = ( SELECT tenant_id FROM app.tenants WHERE tenant_code='tenant-a' ));
SELECT a.pid,a.state,a.wait_event_type,a.wait_event, now()-a.xact_start AS xact_age, pg_blocking_pids(a.pid) AS blocking_pids, a.query_idFROM pg_stat_activity AS aWHERE a.datname=current_database()ORDER BY a.xact_start NULLS LAST;
Repair by ending Session A with ROLLBACK in this
controlled lab. In production, do not terminate a blocker until
you identify ownership, transaction purpose, rollback cost, and
application retry semantics.
4. Drill C — standby replay pause, lag, and WAL retention
On the standby, an administrator can pause WAL replay. The receiver may continue receiving/flushing while replay stops; this demonstrates why “connected” does not mean “caught up.”
SELECT pg_wal_replay_pause();SELECT pg_get_wal_replay_pause_state(), pg_last_wal_receive_lsn(), pg_last_wal_replay_lsn();
INSERT INTO app.work_orders(tenant_id,customer_id,external_ref,status,scheduled_at,amount,note,priority)SELECT c.tenant_id,c.customer_id, 'LAG-' || g, 'queued', clock_timestamp() + g * INTERVAL '1 second', 25.00,'lag-drill',3FROM app.customers AS cCROSS JOIN generate_series(1,1000) AS gWHERE c.tenant_id=(SELECT tenant_id FROM app.tenants WHERE tenant_code='tenant-a') AND c.customer_id=(SELECT min(customer_id) FROM app.customers WHERE tenant_id=c.tenant_id)ON CONFLICT (tenant_id,external_ref) DO NOTHING;SELECT application_name,state, sent_lsn,write_lsn,flush_lsn,replay_lsn, pg_size_pretty(pg_wal_lsn_diff(sent_lsn,replay_lsn)) AS replay_byte_lagFROM pg_stat_replication;SELECT slot_name,active,restart_lsn,wal_status,safe_wal_sizeFROM pg_replication_slotsWHERE slot_name='ch24_standby_slot';
SELECT pg_wal_replay_resume();SELECT pg_get_wal_replay_pause_state(), pg_last_wal_receive_lsn(), pg_last_wal_replay_lsn();
The drill passes only after replay catches up and primary
retained-WAL capacity remains healthy. Never “fix” slot
retention by deleting files from pg_wal.
5. Drill D — accidental data change and PITR decision
SELECT pg_create_restore_point('ch24_drill_before_delete');SELECT count(*) AS before_rowsFROM app.work_ordersWHERE status='completed';DELETE FROM app.work_ordersWHERE status='completed' AND created_at < TIMESTAMPTZ '2026-09-01 00:00+00';SELECT count(*) AS after_rowsFROM app.work_ordersWHERE status='completed';
Do not “undo” a broad DELETE by guessing an inverse INSERT.
Choose between application-level repair and PITR based on scope,
dependencies, time, and the RPO/RTO contract. For PITR, repeat
Lesson 4's base-backup/WAL-archive restore into
CH24_RESTORE with
recovery_target_name='ch24_drill_before_delete',
verify business state, then decide cutover. The original primary
remains untouched until the recovery decision is explicit.
6. Drill E — corrupt a COPY of the backup and prove detection
Physical corruption injection belongs in a copied backup
artifact, not a running cluster. Use the backup manifest to let
pg_verifybackup detect that a file no longer
matches its recorded checksum.
CORRUPT="$CH24_ROOT/corrupt_backup"rm -rf "$CORRUPT"cp -a "$CH24_BACKUP" "$CORRUPT"REL_PATH="$(psql -h 127.0.0.1 -p 55480 -U postgres \ -d servicehub_capstone -Atc \ "SELECT pg_relation_filepath('app.work_orders');")"python3 - "$CORRUPT/$REL_PATH" <<'PY'from pathlib import Pathimport sysp=Path(sys.argv[1])with p.open("r+b") as f: f.seek(min(9000, max(0, p.stat().st_size-1))) b=f.read(1) f.seek(-1,1) f.write(bytes([(b[0] ^ 0x01) if b else 0x01]))PYpg_verifybackup "$CORRUPT" || true
$corrupt = Join-Path $env:CH24_ROOT "corrupt_backup"if (Test-Path $corrupt) { Remove-Item -Recurse -Force $corrupt }Copy-Item -Recurse $env:CH24_BACKUP $corrupt$rel = psql.exe -h 127.0.0.1 -p 55480 -U postgres ` -d servicehub_capstone -Atc ` "SELECT pg_relation_filepath('app.work_orders');"$target = Join-Path $corrupt $rel.Trim()$bytes = [IO.File]::ReadAllBytes($target)$offset = [Math]::Min(9000, $bytes.Length - 1)$bytes[$offset] = $bytes[$offset] -bxor 1[IO.File]::WriteAllBytes($target,$bytes)pg_verifybackup.exe $corrupt
Expected: verification fails and names a checksum/file mismatch. This proves the manifest detects this corruption class; it does not prove every possible storage/CPU/memory corruption path or validate the application.
7. Drill F — configuration error must fail visibly and be reversible
Use the standby or restore copy—not the primary—to inject a syntactically invalid memory setting.
# Add temporarily to the standby's postgresql.conf:shared_buffers = 'not-a-size'
pg_ctl -D "$CH24_STANDBY" stop -m fast# After adding the invalid line:pg_ctl -D "$CH24_STANDBY" \ -l "$CH24_ROOT/standby-config-error.log" \ -o "-p 55481" start || truetail -n 60 "$CH24_ROOT/standby-config-error.log"# Remove/comment the invalid line, then:pg_ctl -D "$CH24_STANDBY" \ -l "$CH24_ROOT/standby.log" \ -o "-p 55481" start
The pass condition is that monitoring notices the node is unavailable, the log identifies the setting problem, the operator can repair it without touching primary data, and replication resumes.
8. Drill G — bounded load spike and connection pressure
A load drill should declare concurrency, query, duration, cache state, JIT/parallel settings, and host resources. The fan-out commands below assume libpq can authenticate non-interactively through a protected passfile (PGPASSFILE / pgpass.conf) or another approved local secret mechanism. Do not place a password in the command line or source file. The example launches only eight clients against the disposable cluster.
for i in $(seq 1 8); do psql -h 127.0.0.1 -p 55480 -U postgres \ -d servicehub_capstone -c " SELECT tenant_id,status,count(*),sum(amount) FROM app.work_orders GROUP BY tenant_id,status ORDER BY tenant_id,status;" \ >/dev/null &donewait
$jobs = 1..8 | ForEach-Object { Start-Job -ScriptBlock { psql.exe -h 127.0.0.1 -p 55480 -U postgres ` -d servicehub_capstone -c ` "SELECT tenant_id,status,count(*),sum(amount) FROM app.work_orders GROUP BY tenant_id,status ORDER BY tenant_id,status;" | Out-Null }}$jobs | Wait-Job | Receive-Job | Out-Null$jobs | Remove-Job
SELECT state,count(*) AS sessionsFROM pg_stat_activityWHERE backend_type='client backend'GROUP BY state;SELECT wait_event_type,wait_event,count(*) AS backendsFROM pg_stat_activityWHERE state='active'GROUP BY wait_event_type,wait_eventORDER BY backends DESC;SELECT numbackends,deadlocks,temp_bytes,blk_read_time,blk_write_timeFROM pg_stat_databaseWHERE datname=current_database();
Eight clients are not a performance claim. The goal is to prove the evidence path and determine whether the system saturates CPU, I/O, memory, locks, or connection admission before changing configuration.
9. Drill H — operator failover with explicit fencing
START RTO CLOCK[ ] Stop application writes / routing to primary[ ] Fence old primary so it cannot accept writes[ ] Record standby replay position and expected RPO impact[ ] Promote standby[ ] Verify pg_is_in_recovery() = false[ ] Run one business read/write invariant[ ] Reroute clients[ ] Verify new writes only on promoted node[ ] Keep old primary isolated[ ] Record timeline / rebuild planSTOP RTO CLOCKPass:- data correctness accepted- measured RPO <= 60 s target- measured RTO <= 15 min targetIf not met:- record exact step/time that breached the SLO; do not relabel the target.
10. Defend the design with evidence and admitted gaps
| Decision | Evidence to defend it | Residual risk / next improvement |
|---|---|---|
| async physical standby | measured replay lag + failover drill stays inside 60 s RPO | single-host lab proves mechanism, not independent failure domain; production needs separate host/AZ/site decision |
| WAL archive + PITR | manifest verification + named-target restore + business invariants | lab archive is local; production requires off-host/immutable retention and access controls |
| RLS tenant isolation | cross-tenant read/write denial under authenticated role mapping | role-per-tenant lab may not scale; production identity propagation needs equivalent non-forgeable trust boundary |
| monthly event partitions | pruning evidence + retention runbook | future partition automation/failure alert required |
| targeted index/autovacuum/memory changes | before/after EXPLAIN/stats evidence | synthetic data/load is not production capacity certification |
11. Final acceptance query and design-defense rubric
SELECT current_database(),version();SELECT count(*) AS invalid_constraintsFROM pg_constraintWHERE connamespace='app'::regnamespace AND NOT convalidated;SELECT count(*) AS invalid_indexesFROM pg_indexWHERE NOT indisvalid AND indrelid IN ( SELECT oid FROM pg_class WHERE relnamespace='app'::regnamespace );SELECT relname,n_live_tup,n_dead_tup,last_autovacuum,last_autoanalyzeFROM pg_stat_user_tablesWHERE schemaname='app'ORDER BY relname;SELECT archived_count,failed_count,last_archived_time,last_failed_timeFROM pg_stat_archiver;SELECT slot_name,active,wal_status,safe_wal_sizeFROM pg_replication_slotsORDER BY slot_name;
Zero invalid constraints/indexes is necessary but not sufficient. The defense also needs SLO load-test results, restore/failover timing, RLS tests, capacity forecast versus observed growth, current release/security posture, external routing/fencing evidence, and an owner for every open risk.
12. Deliberately wrong conclusion: “all drills passed, therefore production-ready”
A single laptop lab cannot prove independent failure domains, network partitions, cloud/SAN behavior, production client-driver compatibility, certificate lifecycle, real workload distribution, operator on-call response, or business disaster-recovery governance. Passing the capstone proves mechanism understanding and a testable design—not universal production certification.
13. Cleanup only after evidence review
Keep the lab until you have saved the drill log, plans, log excerpts, backup verification output, actual RPO/RTO timings, and ADR updates. If the failover drill left the original primary fenced, do not reconnect it to the application merely to clean up a slot. Either administer it in isolation or simply remove the dedicated Chapter 24 data directory after all evidence is saved.
-- Export ops.ch24_drill_log before destruction.SELECT * FROM ops.ch24_drill_log ORDER BY drill_id;-- Run the slot cleanup only on the node that owns the slot,-- only while it is isolated from application write routing.SELECT pg_drop_replication_slot('ch24_standby_slot')WHERE EXISTS ( SELECT 1 FROM pg_replication_slots WHERE slot_name='ch24_standby_slot' AND NOT active);
pg_ctl -D "$CH24_RESTORE" stop -m fast 2>/dev/null || truepg_ctl -D "$CH24_STANDBY" stop -m fast 2>/dev/null || truepg_ctl -D "$CH24_PRIMARY" stop -m fast 2>/dev/null || truerm -rf "$CH24_ROOT"
pg_ctl.exe -D $env:CH24_RESTORE stop -m fast 2>$nullpg_ctl.exe -D $env:CH24_STANDBY stop -m fast 2>$nullpg_ctl.exe -D $env:CH24_PRIMARY stop -m fast 2>$nullRemove-Item -Recurse -Force $env:CH24_ROOT
A defensible PostgreSQL platform states what it promises, shows evidence for those promises, separates mechanisms that solve different failures, and records what remains unproven. Keep the runbooks, restore drills, upgrade checks, RLS tests, capacity forecasts, and workload baselines alive after launch; production engineering is an operating loop, not a one-time configuration event.
Check your understanding
- Why is an immediate-stop drill not a corruption-recovery test?
- What does pausing standby replay demonstrate about replication health?
- Why is corrupting a copied base backup safer and more useful than touching a live relation file?
- What is the pass condition for a failover drill besides successful promotion?
- Why does passing this single-host capstone not certify a real production deployment?
Review the answers
Crash recovery replays WAL to transaction consistency but assumes storage is readable; corruption is a different failure class. Replay pause separates receive/flush from apply and exposes lag/retention behavior. A copied backup is disposable and pg_verifybackup can compare it to the manifest without risking live data. Failover must include fencing, routing, business correctness, and measured RPO/RTO—not just promotion. A laptop lab cannot reproduce independent failure domains, real traffic, platform networking/storage, certificate/secret lifecycle, or human on-call behavior.
Authoritative references
Use current upstream PostgreSQL documentation and release/support pages as the source of truth for version-, security-, topology-, and recovery-sensitive behavior.