--- apiVersion: v1 kind: Namespace metadata: name: shortsmith --- apiVersion: v1 kind: PersistentVolumeClaim metadata: annotations: # Never let ArgoCD prune this PVC: it holds the job store and the rendered videos. argocd.argoproj.io/sync-options: Prune=false name: shortsmith-data namespace: shortsmith spec: accessModes: - ReadWriteOnce resources: requests: storage: 5Gi --- apiVersion: apps/v1 kind: Deployment metadata: name: shortsmith namespace: shortsmith labels: app: shortsmith spec: replicas: 1 # Single replica on a RWO local-path volume holding a SQLite job store in WAL mode: # Recreate keeps two pods from opening /data/shortsmith.db during a roll. The queue # is per-process anyway, so a second replica would not share work, only the file. strategy: type: Recreate selector: matchLabels: app: shortsmith template: metadata: labels: app: shortsmith spec: containers: - name: shortsmith image: git.chemavx.xyz/chemavx/shortsmith:f6ff2a71 imagePullPolicy: Always ports: - name: http containerPort: 8080 env: - name: SHORTSMITH_DATA_DIR value: "/data" volumeMounts: - name: data mountPath: /data # Measured IN THIS POD, sampling the cgroup's own memory.stat every 30 ms. # An earlier set of numbers taken on the dev host said 522 MB and produced a # 1Gi limit; the first real render was OOMKilled at 1483 MB. The host runs # ffmpeg 4.4.2 and the image ships 7.1.5, and 7.0 rewrote transcoding onto a # threaded scheduler whose inter-component queues hold decoded frames. So: # numbers measured outside the deployment target are a guess about it. # # Rendering is this service's normal mode, not a burst, so the request comes # from the peak and not from the resting figure. # # at rest (imports + FastAPI app) 40 MB current # frames, 3 workers, 1260 of them +145 MB anon # audio synthesis and WAV +82 MB anon # encode pass 1, video only, -threads 3 +562 MB anon <- the cost # encode pass 2, mux the audio in +15 MB anon # two renders back to back, peak 640 MB anon / 716 current # one render at the spec's 180 s ceiling 620 MB anon / 923 current # # anon is flat with duration; what grows is the page cache holding the PNG # frames (70 MB at 42 s, 287 MB at the 180 s ceiling). That is reclaimable, # so the request is sized against anon and the limit against current. # # 768Mi covers the 640 MB anon peak with ~20% headroom. The limit is 1.5Gi # and not 1Gi because of the ceiling case: at 1Gi that render peaked at # 940 MB of 1024, i.e. 84 MB of margin, surviving only because the kernel # had reclaimable cache to drop. Verified at 1.5Gi with memory.events # reading `max 0` — the limit was never reached, not merely never fatal. # # The CPU limit is what sizes the worker pool, not the request — the code # reads cpu.max, since cpu_count() reports the node's 16. At limits.cpu 3 # that is 3 workers, verified in this pod. 42 s of video renders in ~31 s, # the 180 s ceiling in ~135 s. # # Consequence, deliberate: under sustained contention the pod runs 3 workers # on the 1 CPU it is guaranteed and a render takes about 3x longer. That is # benign here — POST /render returns a job id, the queue runs at concurrency # 1 and the caller polls, so a slow render is a later `done`, never a # timeout. Do not "fix" it by sizing the pool from the request: that would # trade an occasional slowdown for a permanent 3x on every render, including # the ones on an idle node, which is the common case. resources: requests: cpu: "1" memory: "768Mi" limits: cpu: "3" memory: "1536Mi" # The render runs off the event loop (asyncio.to_thread, and the frame # workers are separate processes), so /healthz answers while a job is in # flight. A probe timing out means the process is actually wedged. readinessProbe: httpGet: path: /healthz port: http initialDelaySeconds: 5 periodSeconds: 10 livenessProbe: httpGet: path: /healthz port: http initialDelaySeconds: 15 periodSeconds: 30 failureThreshold: 3 volumes: - name: data persistentVolumeClaim: claimName: shortsmith-data imagePullSecrets: - name: gitea-registry-infisical --- apiVersion: v1 kind: Service metadata: # Must end in -svc. researchowl hit this: a Service named `searxng` collided with # the SEARXNG_* env vars k8s injects into every pod in the namespace and returned # 403s until it was renamed. name: shortsmith-svc namespace: shortsmith spec: selector: app: shortsmith ports: - name: http port: 8080 targetPort: http # No Ingress on purpose: this is an internal API with no authentication, reached at # shortsmith-svc.shortsmith.svc.cluster.local:8080 from inside the cluster. Ollama was # exposed without auth once and had to be retrofitted with Authentik forward-auth.