Files
MichilisandClaude Opus 5 37370dd079
CI / types, lint and rules coverage (push) Canceled after 0s
CI / tests (postgres) (push) Canceled after 0s
CI / tests (sqlite) (push) Canceled after 0s
CI / playwright (push) Canceled after 0s
phase-8: run it on Postgres, and find out what that was hiding
Six phases claimed the product runs on SQLite and on Postgres. Nothing
had ever run it on Postgres. TEST_DATABASE_URL now points the whole
suite at a real server and CI runs both arms.

The first run found a bug that would have shipped. better-auth's banned
flag is integer 0/1 on SQLite and a real boolean on Postgres, and the
code read it as `banned === 1`, so on Postgres an account someone asked
us to freeze went on receiving email. Both of those flags are now typed
for either dialect and read through isFlagSet.

It also found the migration advisory lock being taken on a pool. An
advisory lock belongs to the session that took it, so a lock on one
pooled connection and an unlock on another leaves it held. Two replicas
migrating at once is the ordinary case in k8s and is precisely what it
was there to protect.

New for the scaled mode: k8s manifests with migrations as an
initContainer, one poller in its own worker Deployment rather than one
per API replica, and an Ingress that exposes the web app only. The
storage drivers finally have tests, S3 included, since scaled mode
requires it and it had never been exercised.

Two acceptance tests, both checked against a deliberately broken build
first: two workers claiming a hundred jobs report 188 claims with SKIP
LOCKED removed, and the in-flight request is cut off with the drain wait
removed.

340 tests on SQLite, 341 on Postgres, 70 Playwright, rules coverage 100%.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 22:03:13 +00:00

85 lines
2.9 KiB
YAML

apiVersion: apps/v1
kind: Deployment
metadata:
name: impuestos-api
namespace: impuestos
labels: { app: impuestos, component: api }
spec:
replicas: 2
selector:
matchLabels: { app: impuestos, component: api }
template:
metadata:
labels: { app: impuestos, component: api }
spec:
# 30s: the process drains in-flight requests for up to 25s before it exits.
terminationGracePeriodSeconds: 30
initContainers:
# Every replica runs this. Concurrent runs are safe: the migration holds a Postgres
# advisory lock on one pinned connection, so the second pod waits for the first.
- name: migrate
image: ghcr.io/example/impuestos-api:latest
command: ['node', 'dist/db/migrate.cli.js']
envFrom:
- configMapRef: { name: impuestos-config }
- secretRef: { name: impuestos-secrets }
resources:
requests: { cpu: 50m, memory: 128Mi }
limits: { memory: 256Mi }
containers:
- name: api
image: ghcr.io/example/impuestos-api:latest
ports:
- name: http
containerPort: 4000
env:
- name: ROLE
value: 'server'
# The dedicated worker Deployment does the jobs. An API replica that also
# polled would multiply the pollers by the replica count.
- name: JOBS_INLINE
value: 'false'
envFrom:
- configMapRef: { name: impuestos-config }
- secretRef: { name: impuestos-secrets }
# Liveness answers as long as the process is alive; readiness also checks the
# database and the storage driver, and goes false the moment a drain begins.
livenessProbe:
httpGet: { path: /healthz, port: http }
initialDelaySeconds: 5
periodSeconds: 10
readinessProbe:
httpGet: { path: /readyz, port: http }
initialDelaySeconds: 2
periodSeconds: 5
failureThreshold: 2
resources:
requests: { cpu: 100m, memory: 256Mi }
limits: { memory: 512Mi }
volumeMounts:
- name: tmp
mountPath: /tmp
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities: { drop: ['ALL'] }
volumes:
# The root filesystem is read only, so the one place anything may be written is an
# empty directory that dies with the pod. Nothing durable belongs here: uploads go
# through the storage driver to S3.
- name: tmp
emptyDir: {}
---
apiVersion: v1
kind: Service
metadata:
name: impuestos-api
namespace: impuestos
spec:
type: ClusterIP
selector: { app: impuestos, component: api }
ports:
- name: http
port: 4000
targetPort: http