fix: correct a Kafka version downgrade and four live-only bugs found via full-stack verification#46
Conversation
…tibility requirements
Discovered live: a1f4421's fix for #39 switched dev-start.sh's PG_PASSWORD and $_minio_path SQL interpolation to psql's -v/:'var' substitution to avoid bare string interpolation into SQL — but tested that against a real psql session for the first time just now and it doesn't work. Confirmed directly against the running postgresql-0 pod: `psql -v pw=x -c "SELECT :'pw';"` sends the literal, unexpanded text ":'pw'" to the server, which Postgres then rejects with a syntax error. `-c` bypasses the interactive- style parser that performs variable interpolation; `-f` and stdin do not. Switched both call sites from `-c "..."` to piping the SQL through stdin (kubectl exec -i for the PG_PASSWORD sync, a plain pipe for the $_minio_path UPDATE) — verified live against postgresql-0 that the substitution now actually happens (ALTER ROLE succeeded, password containing the exact value from the k8s secret). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Ran dev-start.sh end-to-end against a real k3d cluster and exercised it under real load for the first time this session — surfaced three bugs none of the unit tests, terraform validate, or standalone smoke tests could have caught: 1. services/classifier/model.py: reverted padding="max_length" (#8's earlier "fix"). Every batch was tokenizing to the full 512 tokens regardless of actual input length — reproduced live, this OOM-killed the classifier pod (1Gi limit) twice under realistic load. The "latency doesn't depend on batch content" benefit isn't worth crashing the service; verified the revert holds memory flat (~324Mi) through a 300-trace sustained load test with zero restarts. 2. services/classifier/main.py: fixed #43's registration-skip check. download_model() always caches a MinIO-backed model under a local directory (/tmp/sentinel-model-cache/...), so _classifier.model_path is *always* an absolute local path — checking model_path.startswith("/") to detect "non-portable local fallback" therefore skipped registration for the common MinIO-backed case too. Every classification write then violated classifications_model_version_fkey, since the model_version never existed in model_registry. Now uses `model_dir is not None` (only None for a genuine local-only fallback) and registers the original MinIO path from the registry lookup, not the local cache path. 3. services/stream-processor/main.py: _pg_write only caught OperationalError (connection lost) for its reconnect logic. A ForeignKeyViolation from bug #2 above is a different failure mode — the connection stays alive but its transaction is aborted, so every subsequent command fails with InFailedSqlTransaction until rolled back. Without a rollback, the long-lived connection stayed permanently broken after the first such error — reproduced live, this stalled all PostgreSQL writes indefinitely even after bug #2 was fixed and redeployed, until this rollback was added. Verified end-to-end after all three fixes: dev-start.sh completes with zero errors, Kafka survives a real pod restart with identical topic/ consumer-group state before and after, and a fresh simulate-traces.py run shows classifications/flagged_content counts increasing correctly with zero duplicates and zero consumer lag. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…he wrong model_version namespace
Live-verified against a real cluster: model_registry holds two kinds of
rows that don't share a model_version namespace. services/classifier's
get_active_model() picks an 'active'/'staging' row to decide which MinIO
model_path to download, but the classifier then self-registers a
*separate* new 'staging' row under its own freshly-derived model_version
string (sentinel-roberta-{deployed_at}-{quant_tag}) — and that string, not
the row it downloaded from, is what actually appears in
classifications.model_version.
03d2f9b's fix mirrored get_active_model()'s "prefer status='active'"
ordering exactly, which seemed principled but was wrong in practice: the
one 'active' row in this cluster was a stale promotion under a completely
different model_version string (nothing in this project's current phase —
Airflow/Phase 7 doesn't exist yet — reliably keeps 'active' pointed at
what's actually running). The drift job resolved that version, found zero
matching classification rows, and silently computed nothing meaningful.
Dropped the active-status preference — order by created_at alone, which
is "whichever pod self-registered most recently," matching what's
actually running and writing classifications. Verified live: drift_job.py
now resolves the version with 960 real classification rows, computes
PSI=0.0018/JSD=0.0002 successfully, and writes the drift_stats row under
the correct model_version.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Free Run ID: 📒 Files selected for processing (6)
📝 WalkthroughWalkthroughThis PR contains unrelated fixes: pinning Kafka Docker images to 4.3.1 in Terraform, changing active model version selection logic in the drift DB module, fixing psql variable substitution in dev-start.sh, reworking classifier model registration to use active registry data, reverting tokenizer padding, and adding Postgres rollback handling in stream-processor. ChangesKafka Image Pinning
Model Registry Selection and Registration
dev-start.sh psql Variable Substitution Fix
Classifier Padding and Stream-Processor DB Error Handling
Estimated code review effort: 3 (Moderate) | ~25 minutes Estimated code review effort: 3 (Moderate) | ~25 minutes Note 🎁 Summarized by CodeRabbit FreeYour organization is on the Free plan. CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please upgrade your subscription to CodeRabbit Pro by visiting https://app.coderabbit.ai/login. Comment |
Summary
Follow-up to #45. After that merge, ran the full stack live against a real k3d cluster (
dev-start.shend-to-end, sustained synthetic load, real pod restarts) instead of relying on unit tests and standalone smoke tests — this surfaced five real bugs, four of them regressions from #45's own fixes. All are now fixed and independently re-verified live.1. Kafka version downgrade broke the cluster outright (
7985996)#45pinnedapache/kafka:latest→3.9.0for reproducibility. In practice:latesthad already drifted to Kafka 4.3.1, which had formatted the PVC's KRaft metadata with a feature level 3.9.0 doesn't recognize (No MetadataVersion with feature level 30) — a downgrade, not a pin, and it crash-looped every broker start. Inspected the cached image directly and re-pinned to4.3.1(the version that actually formatted the data) instead of blindly using an arbitrary "recent stable" version.2.
psql -v/:'var'substitution silently doesn't work through-c(91ed645)#45's SQL-injection fix (#39) switched topsql -v pw=... -c "... :'pw' ..."to avoid bare string interpolation. Reproduced live:-cbypasses the parser that performs variable interpolation entirely — Postgres received the literal, unexpanded text:'pw'and rejected it as a syntax error.-f/stdin do support it. Switched both call sites to pipe the SQL through stdin instead.3. Three bugs only reproducible under real load/restarts (
e861865)#8'spadding="max_length"tokenized every batch to the full 512 tokens regardless of actual input length. Reverted topadding=True; verified flat ~324Mi memory through a 300-trace burst with zero restarts.#43'smodel_path.startswith("/")check couldn't distinguish "genuine local-only fallback" from "MinIO model, locally cached" (download_model()always caches locally), so it skipped registration for the normal case too. Now keyed on whether a MinIO download actually happened.InFailedSqlTransaction—_pg_writeonly rolled back onOperationalError(connection lost), not on a SQL-level error that aborts the transaction but leaves the connection alive. Added a rollback for anypsycopg.Error.4. Drift job resolved a
model_versionwith zero matching classification rows (d248302)#33's fix mirrored the classifier's own "preferstatus='active'" registry-selection logic — reasonable in principle, wrong in practice: the classifier self-registers under its own freshly-derivedmodel_versionstring on every startup, completely separate from whatevermodel_pathan'active'row points at. The one'active'row in the live registry was a stale promotion under an unrelated version string, so drift detection silently computed nothing meaningful. Dropped the active-status preference in favor ofcreated_at DESC— "whichever pod self-registered most recently" is what's actually running.Test plan
dev-start.shruns end-to-end with zero errors (image builds → terraform apply → data-layer ready → Kafka topic → password sync → port-forwards → model bootstrap → app rollout)classifications/flagged_contentrow counts increase correctly, zero duplicates, zero consumer lag, clean logsmodel_version, computes PSI/JSD on 960 real rows, writesdrift_statscorrectlyterraform planclean (only expectedkubectl rollout restartannotation drift, not Terraform-managed)🤖 Generated with Claude Code
Summary by CodeRabbit