Skip to content

Commit 7fa9584

Browse files
authored
feat: compute shared_buffers and mount /dev/shm for restore database (#11)
1 parent 087af4c commit 7fa9584

3 files changed

Lines changed: 404 additions & 2 deletions

File tree

src/controllers/restore/builders.rs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ use super::restore_owner_reference;
1818
use crate::{
1919
controllers::{env_from_secret, env_from_secret_optional, kopia_writable_env, overlay},
2020
error::{Error, Result},
21+
quantity::compute_shm_and_shared_buffers,
2122
types::*,
2223
};
2324

@@ -411,6 +412,7 @@ pub fn build_deployment(
411412
replica: &PostgresPhysicalReplica,
412413
) -> Result<Deployment> {
413414
let pvc_name = format!("{name}-data");
415+
let (shm_size, shared_buffers_mb) = compute_shm_and_shared_buffers(&replica.spec.resources);
414416
let creds_secret = SecretReference {
415417
name: Some(format!("{}-creds", restore.spec.replica.name)),
416418
namespace: Some(namespace.to_string()),
@@ -506,7 +508,7 @@ listen_addresses = '*'
506508
port = 5432
507509
max_connections = 100
508510
max_prepared_transactions = 16
509-
shared_buffers = 128MB
511+
shared_buffers = {shared_buffers_mb}MB
510512
dynamic_shared_memory_type = posix
511513
log_timezone = 'UTC'
512514
datestyle = 'iso, mdy'
@@ -526,6 +528,7 @@ sed -i \
526528
-e '/^[[:space:]]*ident_file[[:space:]]*=/d' \
527529
-e '/^[[:space:]]*data_directory[[:space:]]*=/d' \
528530
-e '/^[[:space:]]*dynamic_shared_memory_type[[:space:]]*=/d' \
531+
-e '/^[[:space:]]*shared_buffers[[:space:]]*=/d' \
529532
-e '/^[[:space:]]*log_destination[[:space:]]*=/d' \
530533
-e '/^[[:space:]]*logging_collector[[:space:]]*=/d' \
531534
-e '/^[[:space:]]*log_directory[[:space:]]*=/d' \
@@ -546,6 +549,7 @@ echo "Configuring stderr logging..."
546549
echo "log_destination = 'stderr'" >> "$PGDATA/postgresql.conf"
547550
echo "password_encryption = 'scram-sha-256'" >> "$PGDATA/postgresql.conf"
548551
echo "logging_collector = off" >> "$PGDATA/postgresql.conf"
552+
echo "shared_buffers = {shared_buffers_mb}MB" >> "$PGDATA/postgresql.conf"
549553
550554
PG_MAJOR=$(cat "$PGDATA/PG_VERSION")
551555
@@ -908,6 +912,11 @@ exec postgres -D /pgdata/pgdata ${PGRO_LOG_LEVEL:+-c log_min_messages=$PGRO_LOG_
908912
mount_path: "/usr/lib/locale".to_string(),
909913
..Default::default()
910914
},
915+
VolumeMount {
916+
name: "dshm".to_string(),
917+
mount_path: "/dev/shm".to_string(),
918+
..Default::default()
919+
},
911920
]),
912921
readiness_probe: Some(Probe {
913922
exec: Some(ExecAction {
@@ -960,6 +969,16 @@ exec postgres -D /pgdata/pgdata ${PGRO_LOG_LEVEL:+-c log_min_messages=$PGRO_LOG_
960969
),
961970
..Default::default()
962971
},
972+
Volume {
973+
name: "dshm".to_string(),
974+
empty_dir: Some(
975+
k8s_openapi::api::core::v1::EmptyDirVolumeSource {
976+
medium: Some("Memory".to_string()),
977+
size_limit: Some(shm_size),
978+
},
979+
),
980+
..Default::default()
981+
},
963982
]),
964983
affinity: replica.spec.affinity.clone(),
965984
tolerations: Some(replica.spec.tolerations.clone()),

src/controllers/restore/tests.rs

Lines changed: 169 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
use std::collections::BTreeMap;
2+
13
use jiff::Span;
24
use k8s_openapi::api::core::v1::{
35
Affinity, LocalObjectReference, NodeAffinity, NodeSelector, NodeSelectorRequirement,
4-
NodeSelectorTerm, SecretReference,
6+
NodeSelectorTerm, ResourceRequirements, SecretReference,
57
};
68
use k8s_openapi::apimachinery::pkg::api::resource::Quantity;
79

@@ -172,3 +174,169 @@ fn version_detect_job_has_ttl_seconds_after_finished() {
172174
.expect("version-detect job must set ttlSecondsAfterFinished");
173175
assert!(ttl > 0, "ttlSecondsAfterFinished must be positive");
174176
}
177+
178+
#[test]
179+
fn deployment_has_dshm_volume_with_default_resources() {
180+
let (mut restore, replica) = test_restore_and_replica();
181+
restore.status = Some(PostgresPhysicalRestoreStatus {
182+
postgres_version: Some("16".to_string()),
183+
..Default::default()
184+
});
185+
186+
let deploy = build_deployment(&restore, "test-restore", "default", &replica).unwrap();
187+
let pod_spec = deploy.spec.unwrap().template.spec.unwrap();
188+
189+
let dshm_vol = pod_spec
190+
.volumes
191+
.as_ref()
192+
.unwrap()
193+
.iter()
194+
.find(|v| v.name == "dshm")
195+
.expect("dshm volume must exist");
196+
197+
let empty_dir = dshm_vol.empty_dir.as_ref().expect("dshm must be emptyDir");
198+
assert_eq!(empty_dir.medium.as_deref(), Some("Memory"));
199+
// Default 1Gi request: min(512Mi, ceil(36% of 1Gi)) = min(512Mi, 369Mi) = 369Mi
200+
assert_eq!(empty_dir.size_limit.as_ref().unwrap().0, "369Mi");
201+
}
202+
203+
#[test]
204+
fn deployment_has_dshm_volume_with_custom_resources() {
205+
let (mut restore, mut replica) = test_restore_and_replica();
206+
restore.status = Some(PostgresPhysicalRestoreStatus {
207+
postgres_version: Some("16".to_string()),
208+
..Default::default()
209+
});
210+
replica.spec.resources = Some(ResourceRequirements {
211+
requests: Some(BTreeMap::from([(
212+
"memory".to_string(),
213+
Quantity("4Gi".to_string()),
214+
)])),
215+
limits: Some(BTreeMap::from([(
216+
"memory".to_string(),
217+
Quantity("8Gi".to_string()),
218+
)])),
219+
..Default::default()
220+
});
221+
222+
let deploy = build_deployment(&restore, "test-restore", "default", &replica).unwrap();
223+
let pod_spec = deploy.spec.unwrap().template.spec.unwrap();
224+
225+
let dshm_vol = pod_spec
226+
.volumes
227+
.as_ref()
228+
.unwrap()
229+
.iter()
230+
.find(|v| v.name == "dshm")
231+
.expect("dshm volume must exist");
232+
233+
let empty_dir = dshm_vol.empty_dir.as_ref().expect("dshm must be emptyDir");
234+
assert_eq!(empty_dir.medium.as_deref(), Some("Memory"));
235+
// 4Gi request, 8Gi limit: min(4Gi/2, ceil(36% of 8Gi)) = min(2048Mi, 2950Mi) = 2048Mi
236+
assert_eq!(empty_dir.size_limit.as_ref().unwrap().0, "2048Mi");
237+
}
238+
239+
#[test]
240+
fn deployment_mounts_dshm_on_postgres_and_setup_auth() {
241+
let (mut restore, replica) = test_restore_and_replica();
242+
restore.status = Some(PostgresPhysicalRestoreStatus {
243+
postgres_version: Some("16".to_string()),
244+
..Default::default()
245+
});
246+
247+
let deploy = build_deployment(&restore, "test-restore", "default", &replica).unwrap();
248+
let pod_spec = deploy.spec.unwrap().template.spec.unwrap();
249+
250+
let postgres = &pod_spec.containers[0];
251+
assert_eq!(postgres.name, "postgres");
252+
let pg_mounts = postgres.volume_mounts.as_ref().unwrap();
253+
let pg_shm = pg_mounts
254+
.iter()
255+
.find(|m| m.mount_path == "/dev/shm")
256+
.expect("postgres container must mount /dev/shm");
257+
assert_eq!(pg_shm.name, "dshm");
258+
259+
let setup_auth = pod_spec
260+
.init_containers
261+
.as_ref()
262+
.unwrap()
263+
.iter()
264+
.find(|c| c.name == "setup-auth")
265+
.expect("setup-auth init container must exist");
266+
let sa_mounts = setup_auth.volume_mounts.as_ref().unwrap();
267+
assert!(
268+
!sa_mounts.iter().any(|m| m.mount_path == "/dev/shm"),
269+
"setup-auth must NOT mount /dev/shm (it never uses shared memory)"
270+
);
271+
}
272+
273+
#[test]
274+
fn deployment_init_script_sets_shared_buffers() {
275+
let (mut restore, replica) = test_restore_and_replica();
276+
restore.status = Some(PostgresPhysicalRestoreStatus {
277+
postgres_version: Some("16".to_string()),
278+
..Default::default()
279+
});
280+
281+
let deploy = build_deployment(&restore, "test-restore", "default", &replica).unwrap();
282+
let pod_spec = deploy.spec.unwrap().template.spec.unwrap();
283+
284+
let setup_auth = pod_spec
285+
.init_containers
286+
.as_ref()
287+
.unwrap()
288+
.iter()
289+
.find(|c| c.name == "setup-auth")
290+
.expect("setup-auth init container must exist");
291+
let script = &setup_auth.args.as_ref().unwrap()[0];
292+
293+
// Default 1Gi: SHM=369Mi, shared_buffers = floor(70% of 369) = 258MB
294+
assert!(
295+
script.contains("shared_buffers = 258MB"),
296+
"init script must set shared_buffers to computed value, got script containing: {}",
297+
script
298+
.lines()
299+
.find(|l| l.contains("shared_buffers"))
300+
.unwrap_or("<not found>")
301+
);
302+
303+
// The sed block must strip shared_buffers from source config
304+
assert!(
305+
script.contains("shared_buffers[[:space:]]*="),
306+
"sed must strip shared_buffers from source config"
307+
);
308+
}
309+
310+
#[test]
311+
fn deployment_shared_buffers_with_custom_resources() {
312+
let (mut restore, mut replica) = test_restore_and_replica();
313+
restore.status = Some(PostgresPhysicalRestoreStatus {
314+
postgres_version: Some("16".to_string()),
315+
..Default::default()
316+
});
317+
replica.spec.resources = Some(ResourceRequirements {
318+
requests: Some(BTreeMap::from([(
319+
"memory".to_string(),
320+
Quantity("2Gi".to_string()),
321+
)])),
322+
..Default::default()
323+
});
324+
325+
let deploy = build_deployment(&restore, "test-restore", "default", &replica).unwrap();
326+
let pod_spec = deploy.spec.unwrap().template.spec.unwrap();
327+
328+
let setup_auth = pod_spec
329+
.init_containers
330+
.as_ref()
331+
.unwrap()
332+
.iter()
333+
.find(|c| c.name == "setup-auth")
334+
.expect("setup-auth init container must exist");
335+
let script = &setup_auth.args.as_ref().unwrap()[0];
336+
337+
// 2Gi request: SHM=738Mi, shared_buffers = floor(70% of 738) = 516MB
338+
assert!(
339+
script.contains("shared_buffers = 516MB"),
340+
"init script must set shared_buffers for 2Gi request"
341+
);
342+
}

0 commit comments

Comments
 (0)