Skip to content

Commit 86ba399

Browse files
authored
fix(migration): TCP keepalives + bigger memory limit (#51)
2 parents 284a134 + 0909710 commit 86ba399

1 file changed

Lines changed: 101 additions & 5 deletions

File tree

src/controllers/replica/schema_migration.rs

Lines changed: 101 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,18 @@ done
6565
# Capture psql's stderr for visibility on partial failures.
6666
PSQL_STDERR=$(mktemp)
6767
68+
# TCP keepalive options on the connection URI. Without keepalives a
69+
# silently-dropped pod-to-pod TCP connection (network policy change,
70+
# node disruption, etc.) leaves both ends waiting indefinitely — pg_dump
71+
# blocked on read, postgres marked "idle in transaction" with
72+
# wait_event=ClientRead. Observed in production: a migration sat
73+
# stuck for 39 hours after the connection died, with no detection.
74+
# 60s idle + 10s × 3 probes = dead connection killed within ~90s.
75+
KEEPALIVES="keepalives=1&keepalives_idle=60&keepalives_interval=10&keepalives_count=3"
76+
APPNAME="application_name=pgro-schema-migration"
77+
SOURCE_URI="postgresql://${SOURCE_USER}@${SOURCE_HOST}:5432/${SOURCE_DB}?${KEEPALIVES}&${APPNAME}"
78+
TARGET_URI="postgresql://${TARGET_USER}@${TARGET_HOST}:5432/${TARGET_DB}?${KEEPALIVES}&${APPNAME}"
79+
6880
# ON_ERROR_STOP is deliberately NOT set: persistent_schemas like dbt
6981
# contain views derived from upstream tables, and across upstream schema
7082
# changes (renamed columns, dropped tables) some view DDL in the old
@@ -74,13 +86,13 @@ PSQL_STDERR=$(mktemp)
7486
# for replica availability — clients can regenerate the broken views
7587
# afterward, but the replica must be reachable.
7688
PGPASSWORD="$SOURCE_PASSWORD" pg_dump \
77-
-h "$SOURCE_HOST" -p 5432 -U "$SOURCE_USER" -d "$SOURCE_DB" \
89+
-d "$SOURCE_URI" \
7890
"${SCHEMA_ARGS[@]}" \
7991
--no-owner --no-privileges \
8092
--no-publications --no-subscriptions \
8193
--verbose \
8294
| PGPASSWORD="$TARGET_PASSWORD" psql \
83-
-h "$TARGET_HOST" -p 5432 -U "$TARGET_USER" -d "$TARGET_DB" \
95+
-d "$TARGET_URI" \
8496
--quiet 2> >(tee "$PSQL_STDERR" >&2)
8597
8698
PSQL_EXIT=$?
@@ -208,6 +220,16 @@ pub fn build_schema_migration_job(
208220
env_literal("MIGRATION_CALLBACK_URL", callback_url),
209221
]),
210222
resources: Some(ResourceRequirements {
223+
// pg_dump and psql each buffer some state per
224+
// large object / row, and for non-trivial
225+
// persistent schemas (dbt with many tables and
226+
// indexes) the streaming pipe peaks well past
227+
// the original 512Mi limit. Observed in
228+
// production: the migration container was being
229+
// OOMKilled dozens of times in succession.
230+
// Bump to a generous limit so the migration
231+
// completes; the Job is short-lived and only
232+
// runs during switchover.
211233
requests: Some(BTreeMap::from([
212234
(
213235
"cpu".to_string(),
@@ -218,21 +240,21 @@ pub fn build_schema_migration_job(
218240
(
219241
"memory".to_string(),
220242
k8s_openapi::apimachinery::pkg::api::resource::Quantity(
221-
"128Mi".to_string(),
243+
"256Mi".to_string(),
222244
),
223245
),
224246
])),
225247
limits: Some(BTreeMap::from([
226248
(
227249
"cpu".to_string(),
228250
k8s_openapi::apimachinery::pkg::api::resource::Quantity(
229-
"1".to_string(),
251+
"2".to_string(),
230252
),
231253
),
232254
(
233255
"memory".to_string(),
234256
k8s_openapi::apimachinery::pkg::api::resource::Quantity(
235-
"512Mi".to_string(),
257+
"4Gi".to_string(),
236258
),
237259
),
238260
])),
@@ -313,6 +335,80 @@ mod tests {
313335
);
314336
}
315337

338+
#[test]
339+
fn migration_script_uses_tcp_keepalives() {
340+
// Without TCP keepalives a silently-dropped pod-to-pod connection
341+
// leaves pg_dump and psql blocked indefinitely on a dead socket,
342+
// while postgres marks the session "idle in transaction" with
343+
// wait_event=ClientRead. Observed in production: a migration sat
344+
// stuck for 39 hours.
345+
assert!(
346+
MIGRATION_SCRIPT.contains("keepalives=1"),
347+
"migration script must enable libpq TCP keepalives"
348+
);
349+
assert!(
350+
MIGRATION_SCRIPT.contains("keepalives_idle="),
351+
"migration script must set keepalives_idle"
352+
);
353+
assert!(
354+
MIGRATION_SCRIPT.contains("keepalives_interval="),
355+
"migration script must set keepalives_interval"
356+
);
357+
assert!(
358+
MIGRATION_SCRIPT.contains("keepalives_count="),
359+
"migration script must set keepalives_count"
360+
);
361+
// application_name shows up in pg_stat_activity, making the
362+
// migration session identifiable for diagnosis.
363+
assert!(
364+
MIGRATION_SCRIPT.contains("application_name=pgro-schema-migration"),
365+
"migration script must set application_name for visibility"
366+
);
367+
}
368+
369+
#[test]
370+
fn migration_job_has_enough_memory_for_dbt_scale_schemas() {
371+
// Default limits must be high enough for realistic
372+
// persistent_schemas like dbt with many tables and indexes.
373+
// pg_dump + psql peak well past the historic 512Mi default and
374+
// OOMKill in production. Memory limit must be at least 2Gi.
375+
let replica = make_replica(vec!["dbt"]);
376+
let job = build_schema_migration_job(
377+
&replica,
378+
"test-ns",
379+
"old",
380+
"new",
381+
"db",
382+
"db",
383+
&["dbt".to_string()],
384+
"reader",
385+
"super",
386+
"http://op",
387+
18,
388+
);
389+
let resources = job.spec.unwrap().template.spec.unwrap().containers[0]
390+
.resources
391+
.clone()
392+
.expect("migration container must declare resources");
393+
let limits = resources
394+
.limits
395+
.expect("migration container must declare limits");
396+
let mem_limit = &limits.get("memory").expect("memory limit set").0;
397+
// Accept anything ending with Gi where N >= 2, or Mi where N >= 2048.
398+
let mem_ok = mem_limit
399+
.strip_suffix("Gi")
400+
.and_then(|n| n.parse::<u64>().ok())
401+
.is_some_and(|n| n >= 2)
402+
|| mem_limit
403+
.strip_suffix("Mi")
404+
.and_then(|n| n.parse::<u64>().ok())
405+
.is_some_and(|n| n >= 2048);
406+
assert!(
407+
mem_ok,
408+
"migration memory limit must be at least 2Gi (got {mem_limit})"
409+
);
410+
}
411+
316412
#[test]
317413
fn migration_job_name_format() {
318414
assert_eq!(

0 commit comments

Comments
 (0)