Skip to content

Commit 0d1015e

Browse files
authored
feat(canopy): shm_size_floor spec + intent-driven floors (#83)
2 parents 4e62b89 + 9fea1e9 commit 0d1015e

10 files changed

Lines changed: 76 additions & 1 deletion

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ Defines a continuously-refreshed replica of a PostgreSQL database restored from
9595
| `storageSizeOverride` | `Quantity` | No | — | Override dynamic sizing with a fixed PVC size. When absent, PVC size is calculated from snapshot size. |
9696
| `storageSizeMaximum` | `Quantity` | No | `2Ti` | Maximum allowed PVC size. The restore will fail if the computed size exceeds this limit. |
9797
| `resources` | `ResourceRequirements` | No | — | CPU/memory resource requirements for the PostgreSQL pods. |
98+
| `shmSizeFloor` | `Quantity` | No | — | Floor on the postgres pod's `/dev/shm` sizing. When set, the Deployment uses `max(computed, shmSizeFloor)` — the computed value is derived from `resources` by [`compute_shm_and_shared_buffers`]. Useful when a workload's `shared_buffers` needs more shm than the resource-derived value provides, without wanting to bump the container's memory request. |
9899
| `serviceAnnotations` | `map[string]string` | No | — | Annotations applied to the Service. |
99100
| `podAnnotations` | `map[string]string` | No | — | Annotations applied to the PostgreSQL pods. |
100101
| `affinity` | `Affinity` | No | — | Pod scheduling affinity rules. |

crds.yaml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -704,6 +704,17 @@ spec:
704704
type: string
705705
nullable: true
706706
type: object
707+
shmSizeFloor:
708+
description: |-
709+
Floor on the postgres pod's `/dev/shm` sizing. When set, the
710+
Deployment builder uses `max(computed, shmSizeFloor)` — computed
711+
comes from [`compute_shm_and_shared_buffers`] driven by
712+
[`resources`]. Useful when the resource-derived value would be
713+
smaller than what a workload's `shared_buffers` needs (analytics
714+
/ dbt) without wanting to bump the container's memory request
715+
upward just to raise shm.
716+
nullable: true
717+
x-kubernetes-int-or-string: true
707718
snapshotFilter:
708719
nullable: true
709720
properties:

src/controllers/canopy/intent.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,13 @@ pub struct IntentConfig {
4141
pub service_annotations: Option<BTreeMap<String, String>>,
4242
pub switchover_grace_period: TimeSpan,
4343
pub storage_size_override: Quantity,
44+
/// Floor on the postgres pod's `/dev/shm` sizing. Materialised into
45+
/// `PostgresPhysicalReplicaSpec.shm_size_floor` so the shared
46+
/// Deployment builder picks `max(computed_from_resources, floor)`.
47+
/// Analytics workloads (`analytics-dev` / `analytics-dbt`) want a
48+
/// higher shm than what a 2 GiB memory request would derive, without
49+
/// paying the k8s scheduling cost of bumping the request.
50+
pub shm_size_floor: Quantity,
4451
}
4552

4653
fn resources(cpu_req: &str, mem_req: &str, cpu_lim: &str, mem_lim: &str) -> ResourceRequirements {
@@ -72,6 +79,7 @@ pub fn config_for(intent: &str) -> Option<IntentConfig> {
7279
service_annotations: None,
7380
switchover_grace_period: TimeSpan(Span::new().minutes(5)),
7481
storage_size_override: Quantity("20Gi".to_string()),
82+
shm_size_floor: Quantity("512Mi".to_string()),
7583
}),
7684
"analytics-dev" => Some(IntentConfig {
7785
resources: Some(resources("500m", "2Gi", "4", "8Gi")),
@@ -81,6 +89,7 @@ pub fn config_for(intent: &str) -> Option<IntentConfig> {
8189
service_annotations: None,
8290
switchover_grace_period: TimeSpan(Span::new().minutes(5)),
8391
storage_size_override: Quantity("50Gi".to_string()),
92+
shm_size_floor: Quantity("2Gi".to_string()),
8493
}),
8594
"analytics-dbt" => Some(IntentConfig {
8695
resources: Some(resources("500m", "2Gi", "4", "8Gi")),
@@ -96,6 +105,7 @@ pub fn config_for(intent: &str) -> Option<IntentConfig> {
96105
])),
97106
switchover_grace_period: TimeSpan(Span::new().minutes(2)),
98107
storage_size_override: Quantity("50Gi".to_string()),
108+
shm_size_floor: Quantity("2Gi".to_string()),
99109
}),
100110
_ => None,
101111
}
@@ -141,6 +151,7 @@ impl IntentConfig {
141151
storage_class: None,
142152
storage_size_override: Some(self.storage_size_override.clone()),
143153
resources: self.resources.clone(),
154+
shm_size_floor: Some(self.shm_size_floor.clone()),
144155
service_annotations: self
145156
.service_annotations
146157
.clone()

src/controllers/replica/scheduling.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,7 @@ mod tests {
267267
storage_class: None,
268268
storage_size_override: None,
269269
resources: None,
270+
shm_size_floor: None,
270271
service_annotations: None,
271272
pod_annotations: None,
272273
affinity: None,

src/controllers/replica/schema_migration.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,7 @@ mod tests {
296296
storage_class: None,
297297
storage_size_override: None,
298298
resources: None,
299+
shm_size_floor: None,
299300
service_annotations: None,
300301
pod_annotations: None,
301302
affinity: None,

src/controllers/replica/tests.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ fn make_replica(
3232
storage_class: None,
3333
storage_size_override: None,
3434
resources: None,
35+
shm_size_floor: None,
3536
service_annotations: None,
3637
pod_annotations: None,
3738
affinity: None,
@@ -191,6 +192,7 @@ fn snapshot_list_job_rotates_kopia_logs() {
191192
storage_class: None,
192193
storage_size_override: None,
193194
resources: None,
195+
shm_size_floor: None,
194196
service_annotations: None,
195197
pod_annotations: None,
196198
affinity: None,

src/controllers/restore/builders.rs

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -952,7 +952,13 @@ pub fn build_deployment(
952952
replica: &PostgresPhysicalReplica,
953953
) -> Result<Deployment> {
954954
let pvc_name = format!("{name}-data");
955-
let (shm_size, shared_buffers_mb) = compute_shm_and_shared_buffers(&replica.spec.resources);
955+
let (computed_shm, computed_shared_buffers_mb) =
956+
compute_shm_and_shared_buffers(&replica.spec.resources);
957+
let (shm_size, shared_buffers_mb) = apply_shm_floor(
958+
&computed_shm,
959+
computed_shared_buffers_mb,
960+
replica.spec.shm_size_floor.as_ref(),
961+
);
956962
let creds_secret = SecretReference {
957963
name: Some(format!("{}-creds", restore.spec.replica.name)),
958964
namespace: Some(namespace.to_string()),
@@ -1001,6 +1007,35 @@ pub fn build_deployment(
10011007
})
10021008
}
10031009

1010+
/// Apply the caller's `shm_size_floor` (if any) to the resource-derived
1011+
/// shm + shared_buffers pair. `shared_buffers` scales linearly with shm
1012+
/// (postgres wants ~70 % of shm), so bumping shm bumps
1013+
/// `shared_buffers_mb` proportionally.
1014+
fn apply_shm_floor(
1015+
computed_shm: &Quantity,
1016+
computed_shared_buffers_mb: u64,
1017+
floor: Option<&Quantity>,
1018+
) -> (Quantity, u64) {
1019+
let Some(floor) = floor else {
1020+
return (computed_shm.clone(), computed_shared_buffers_mb);
1021+
};
1022+
let computed_bytes = ParsedQuantity::try_from(computed_shm.clone())
1023+
.ok()
1024+
.and_then(|q| q.to_bytes_f64());
1025+
let floor_bytes = ParsedQuantity::try_from(floor.clone())
1026+
.ok()
1027+
.and_then(|q| q.to_bytes_f64());
1028+
match (computed_bytes, floor_bytes) {
1029+
(Some(c), Some(f)) if f > c => {
1030+
let ratio = f / c;
1031+
let scaled_shared_buffers =
1032+
((computed_shared_buffers_mb as f64) * ratio).floor() as u64;
1033+
(floor.clone(), scaled_shared_buffers.max(16))
1034+
}
1035+
_ => (computed_shm.clone(), computed_shared_buffers_mb),
1036+
}
1037+
}
1038+
10041039
/// Shared postgres Deployment builder — no CR dependencies. Both the
10051040
/// CRD path (`build_deployment`) and the canopy path
10061041
/// (`controllers::canopy::builders::build_canopy_postgres_deployment`) go

src/controllers/restore/tests.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ fn deployment_uses_affinity_not_node_selector() {
3131
storage_class: None,
3232
storage_size_override: None,
3333
resources: None,
34+
shm_size_floor: None,
3435
service_annotations: None,
3536
pod_annotations: None,
3637
affinity: None,
@@ -116,6 +117,7 @@ fn test_restore_and_replica() -> (PostgresPhysicalRestore, PostgresPhysicalRepli
116117
storage_class: None,
117118
storage_size_override: None,
118119
resources: None,
120+
shm_size_floor: None,
119121
service_annotations: None,
120122
pod_annotations: None,
121123
affinity: None,

src/types/replica.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,16 @@ pub struct PostgresPhysicalReplicaSpec {
9292
#[serde(default, skip_serializing_if = "Option::is_none")]
9393
pub resources: Option<ResourceRequirements>,
9494

95+
/// Floor on the postgres pod's `/dev/shm` sizing. When set, the
96+
/// Deployment builder uses `max(computed, shmSizeFloor)` — computed
97+
/// comes from [`compute_shm_and_shared_buffers`] driven by
98+
/// [`resources`]. Useful when the resource-derived value would be
99+
/// smaller than what a workload's `shared_buffers` needs (analytics
100+
/// / dbt) without wanting to bump the container's memory request
101+
/// upward just to raise shm.
102+
#[serde(default, skip_serializing_if = "Option::is_none")]
103+
pub shm_size_floor: Option<Quantity>,
104+
95105
#[serde(default, skip_serializing_if = "Option::is_none")]
96106
pub service_annotations: Option<BTreeMap<String, String>>,
97107

tests/helpers.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ pub fn build_replica(name: &str, secret_ref: &str, opts: ReplicaOpts) -> Postgre
152152
storage_class: None,
153153
storage_size_override: None,
154154
resources: None,
155+
shm_size_floor: None,
155156
service_annotations: None,
156157
pod_annotations: None,
157158
affinity: None,

0 commit comments

Comments
 (0)