1
0
mirror of https://github.com/alexandrev/xslt-lab.git synced 2026-09-13 08:43:16 +00:00
Files
xslt-lab/charts/xslt-playground/values.yaml
T
claude-code 4e1f3ffdac Fix: give the backend room to run, and stop transforming half-typed expressions
The backend container runs three JVMs — Saxon 12, Saxon 9.6 and Xalan — whose
-Xmx values add up to 512 MB, inside a container limited to 256Mi. The limit was
half the heap the JVMs believed they had, so the kernel was killing the
container: OOMKilled twice a day on the 16th and 17th of August, five times a
day on the 18th and 19th, and 81 users in 24 hours got "transform service
unavailable" instead of a result. Before each kill the serial collector spends
hours in continuous full GC, which is why three of the five pods sat pinned at
exactly 0.5 CPU — their limit — around the clock, and why XSLT 2.0's p95 was
5.5s against 0.7s for the other two engines.

Those limits were never in the chart to begin with: the backend deployment has
never rendered a resources block, values.resources.backend was declared and
unused, and the live numbers came from a manual kubectl patch that an ArgoCD
sync would have silently dropped. So the chart now owns them, with room for
what actually runs in there. The CPU request moves 10m -> 250m as well: against
real usage of 500m the HPA read utilisation in the thousands of percent and the
deployment sat permanently at maxReplicas, unable to signal anything. And the
backend gets minReplicas 2, for the reason the frontend already learnt.

The other half is the mid-keystroke problem again, one level down. Gating the
automatic run on well-formedness took the error rate from 54% to 24%, but a
stylesheet whose XPath is half-typed is perfectly good XML, so it still went to
the backend and still came back as a compile error. A day of logs is mostly
that, keystroke by keystroke: 'current()/..[@N' then '[@Na' then '[@Name', and
"Required attribute 'select' is missing" 65 times on a single line. So the gate
now also declines to send an expression that is provably unfinished — a
required attribute that is absent, an empty value, a quote or bracket still
open, a trailing token that cannot end an expression. Facts about the text, not
guesses at intent: there are twice as many tests for what it must leave alone
as for what it catches, including every template the app itself ships. It is
skipped above 64KB, where a document was pasted rather than typed, and as
before it only holds back the automatic run — "Run it anyway" is always there.

Finally, the error classifier learns the rest of what was sitting in "other":
Xalan's "Syntax error in '<expr>'" is the stylesheet's XPath, not the input.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018HnUwBeXv6PCTEzRpFoMSU
2026-08-19 19:30:02 +00:00

119 lines
3.2 KiB
YAML

replicaCount:
frontend: 1
backend: 1
image:
frontend:
repository: xslt-playground-frontend
tag: "latest"
pullPolicy: IfNotPresent
backend:
repository: xslt-playground-backend
tag: "latest"
pullPolicy: IfNotPresent
service:
frontend:
type: ClusterIP
port: 80
backend:
type: ClusterIP
port: 8000
ingress:
enabled: true
className: ""
annotations: {}
hosts:
- host: xslt.local
paths:
- path: /
pathType: Prefix
tls: []
resources:
frontend: {}
# The backend container runs three JVMs side by side (Saxon 12, Saxon 9.6 and
# Xalan — see backend/start.sh), whose -Xmx values alone add up to 512 MB,
# before metaspace, thread stacks and the Go server. The limit has to leave
# room for all of it or the kernel kills the container mid-transform.
backend:
requests:
# Sized so the HPA reads something meaningful: with a 10m request the
# utilisation ratio is permanently in the thousands of percent and the
# deployment sits pinned at maxReplicas.
cpu: 250m
memory: 384Mi
limits:
cpu: "1"
memory: 1Gi
hpa:
frontend:
enabled: true
minReplicas: 1
maxReplicas: 5
targetCPUUtilizationPercentage: 80
backend:
enabled: true
# Two, so a single evicted or restarting pod never leaves the app with no
# backend at all — the frontend learnt this the hard way.
minReplicas: 2
maxReplicas: 5
targetCPUUtilizationPercentage: 80
firebase:
enabled: true
secretName: firebase-config
credentialsKey: service-account.json
configKey: firebase-config.json
credentialsMountPath: /var/secrets/firebase
storage:
enabled: true
databaseUrl: postgres://postgres:postgres@db/xslt?sslmode=disable
frontend:
# URL used by the frontend to reach the backend. Defaults to the
# internal service URL when empty.
backendUrl: ""
# Optional Google Analytics measurement ID
googleAnalyticsId: ""
# Optional Microsoft Clarity project ID
clarityId: ""
# Optional n8n webhook URL for in-app feedback form
feedbackWebhookUrl: ""
# Content for the optional ads.txt file served by the frontend
adsTxt: ""
# Prometheus metrics + Grafana dashboard
metrics:
enabled: true
# Dedicated internal port where the Go backend exposes /metrics.
# Not routed through the public ingress.
backendPort: 9100
# nginx-prometheus-exporter sidecar for the frontend.
frontend:
exporter:
image: nginx/nginx-prometheus-exporter:1.3.0
port: 9113
# Internal-only nginx stub_status endpoint scraped by the exporter.
stubStatusPort: 8081
serviceMonitor:
enabled: true
interval: 30s
scrapeTimeout: 10s
# Label the kube-prometheus-stack Prometheus selects ServiceMonitors by.
releaseLabel: kube-prom-stack
dashboard:
enabled: true
# Namespace the dashboard ConfigMap is created in. The kube-prometheus-stack
# Grafana dashboard sidecar only watches its own namespace (observability),
# so the ConfigMap must live there to be auto-loaded. Empty = release namespace.
namespace: observability
# Label the Grafana dashboard sidecar selects ConfigMaps by.
label: grafana_dashboard
labelValue: "1"