Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions deploy/aks/scripts/check-chart-invariants.sh
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,9 @@ REFUSALS=(
"a database release AND the bundled Postgres (two answers to which database)|deploy/helm/values.yaml:deploy/aks/scripts/testdata/values.db-release-with-bundled-postgres.yaml|exclusive with postgres.enabled"
# Plugins#1738: an executor the portal would silently read as Job must fail the render.
"a misspelled operator executor|deploy/helm/values.yaml:deploy/aks/scripts/testdata/values.operator-executor-misspelled.yaml|must be Job or Actions"
# MeshWeaver#6052: two replicas on per-pod emptyDir /data — each pod's assembly cache is its own,
# and the shared NodeType records name bytes only the compiling pod holds.
"two replicas with a pod-local /data (the #6052 estate)|deploy/helm/values.yaml:deploy/aks/scripts/testdata/values.two-replicas-pod-local-data.yaml|needs a SHARED /data"
)
refused=0
for entry in "${REFUSALS[@]}"; do
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# FIXTURE for check-chart-invariants.sh — a REFUSAL (MeshWeaver#6052).
#
# Two plain replicas with NO persistence.data claim, so each pod's /data would be its own emptyDir.
# /data holds the NodeType assembly cache, and every compiled NodeType's shared record names a file
# in it, so a pod that did not compile a type cannot load it. The client estate in #6052 ran this
# shape: two replicas, `bake-report bytesmissing=93` on both. Everything else is the valid
# two-replica shape (values.two-replicas-no-keda.yaml), so the ONLY thing the render can refuse is
# the pod-local /data. Placeholders, no credential.
replicas:
portal: 2
keda:
enabled: false
config:
memex_portal:
Deployment__Orleans__Clustering: "AdoNet"
secrets:
memex_portal:
ConnectionStrings__memex: "Host=pg.example.test;Database=memex;Username=memex;Password=placeholder"
ConnectionStrings__orleans: "Host=pg.example.test;Database=orleans;Username=memex;Password=placeholder"
memex_migration:
ConnectionStrings__memex: "Host=pg.example.test;Database=memex;Username=memex;Password=placeholder"
ConnectionStrings__orleans: "Host=pg.example.test;Database=orleans;Username=memex;Password=placeholder"
17 changes: 17 additions & 0 deletions deploy/helm/templates/memex-portal/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -679,6 +679,23 @@ spec:
# self-host (the default), higher for a fixed-size install that wants HA without an autoscaler.
# ⚠️ Above 1 the same prerequisites apply as for KEDA — AdoNet clustering and RWX on every claim
# (values.yaml → persistence). helm-template asserts both; see .github/workflows/chart-gate.yml.
{{- /*
🚨 MORE THAN ONE PORTAL POD NEEDS A SHARED /data — refused at render time (MeshWeaver#6052).
/data holds the NodeType assembly cache (FileSystemAssemblyStore, collection "local"), and every
compiled NodeType's record — which is in the SHARED database — names a file under it. On emptyDir
each pod has its own cache, so a type compiled on pod A leaves a record naming bytes pod B never
holds; pod B's instances then render "its compiled assembly could not be loaded on this node", and
a recompile does not help, because it runs where the NodeType's own hub lives, which may be pod A
again. Measured on a client estate on 2026-09-28: two replicas, no shared /data, /health
`bake-report bytesmissing=93` on both pods. chart-gate's invariant 3 asserts RWX only over the
values files tracked in this repository; a client's values are not, so the render itself refuses.
persistDataVolume (a single node's hostPath) counts as shared: every pod lands on that one node.
*/}}
{{- $multiReplica := or (.Values.keda).enabled (gt (int ((.Values.replicas).portal | default 1)) 1) }}
{{- $sharedData := or ((.Values.persistence).data).claimName .Values.persistDataVolume }}
{{- if and $multiReplica (not $sharedData) }}
{{- fail "more than one portal replica (keda.enabled, or replicas.portal > 1) needs a SHARED /data, and persistence.data.claimName is empty, so every pod would get its own emptyDir. /data holds the NodeType assembly cache; each compiled NodeType's shared record names a file in it, and a pod that did not compile that type cannot load it (MeshWeaver#6052). Set persistence.data.claimName to a ReadWriteMany claim, or run one replica." }}
{{- end }}
{{- if not (.Values.keda).enabled }}
replicas: {{ (.Values.replicas).portal | default 1 }}
{{- end }}
Expand Down
5 changes: 4 additions & 1 deletion deploy/helm/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,10 @@ hostingOperator:
# ring, the NodeType assembly cache, the NuGet cache); a ReadWriteOnce claim either pins both
# pods to one node or leaves the second pod unschedulable.
# Both are asserted at render time by .github/workflows/chart-gate.yml, so a values file that
# raises this without them fails the build instead of the deployment.
# raises this without them fails the build instead of the deployment. A floor above one with NO
# persistence.data.claimName (per-pod emptyDir /data) is refused by the chart itself, for every
# values file including a client's untracked ones: the NodeType assembly cache lives in /data and
# the shared NodeType records name files in it (MeshWeaver#6052).
replicas:
portal: 1
# ---- Key Vault secrets, DECLARED (renders the SecretProviderClass) ---------
Expand Down