Skip to content

fix: correct a Kafka version downgrade and four live-only bugs found via full-stack verification#46

Merged
VjayRam merged 4 commits into
masterfrom
feat/classifier
Jul 3, 2026
Merged

fix: correct a Kafka version downgrade and four live-only bugs found via full-stack verification#46
VjayRam merged 4 commits into
masterfrom
feat/classifier

Conversation

@VjayRam

@VjayRam VjayRam commented Jul 3, 2026

Copy link
Copy Markdown
Owner

Summary

Follow-up to #45. After that merge, ran the full stack live against a real k3d cluster (dev-start.sh end-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)
#45 pinned apache/kafka:latest3.9.0 for reproducibility. In practice :latest had 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 to 4.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 to psql -v pw=... -c "... :'pw' ..." to avoid bare string interpolation. Reproduced live: -c bypasses 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)

  • Classifier OOM-killed twice (1Gi limit) under real traffic — #8's padding="max_length" tokenized every batch to the full 512 tokens regardless of actual input length. Reverted to padding=True; verified flat ~324Mi memory through a 300-trace burst with zero restarts.
  • Every classification write was silently failing a foreign-key constraint — #43's model_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.
  • Once the above hit once, the stream processor's long-lived PG connection got permanently stuck in InFailedSqlTransaction_pg_write only rolled back on OperationalError (connection lost), not on a SQL-level error that aborts the transaction but leaves the connection alive. Added a rollback for any psycopg.Error.

4. Drift job resolved a model_version with zero matching classification rows (d248302)
#33's fix mirrored the classifier's own "prefer status='active'" registry-selection logic — reasonable in principle, wrong in practice: the classifier self-registers under its own freshly-derived model_version string on every startup, completely separate from whatever model_path an '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 of created_at DESC — "whichever pod self-registered most recently" is what's actually running.

Test plan

  • dev-start.sh runs end-to-end with zero errors (image builds → terraform apply → data-layer ready → Kafka topic → password sync → port-forwards → model bootstrap → app rollout)
  • Kafka survives a real pod deletion: identical topic data and consumer-group offsets before/after
  • Classifier stable under sustained load: 300-trace burst, memory flat at ~324Mi, zero restarts (previously OOM'd twice)
  • Real trace flow end-to-end: classifications/flagged_content row counts increase correctly, zero duplicates, zero consumer lag, clean logs
  • Drift job runs against live Postgres: resolves the correct model_version, computes PSI/JSD on 960 real rows, writes drift_stats correctly
  • Evaluation pipeline benchmarks the live active model and its gate correctly fails a low-accuracy candidate
  • Prometheus scraping the classifier successfully, all 14 alert rules loaded, real metric data flowing
  • terraform plan clean (only expected kubectl rollout restart annotation drift, not Terraform-managed)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Improved classifier stability under load by reducing memory pressure during prediction.
    • Made registry and model-version handling more reliable, especially during restarts and external model downloads.
    • Strengthened PostgreSQL error handling so failed writes recover cleanly without blocking later processing.
    • Updated Kafka-related runtime components to a newer compatible version to prevent startup and metadata issues.
  • Chores
    • Refined startup and database scripts for more dependable command execution.

VjayRam and others added 4 commits July 2, 2026 22:24
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>
@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown

Review Change Stack

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Free

Run ID: 22a8268a-077c-43d3-90ba-f0c804086568

📥 Commits

Reviewing files that changed from the base of the PR and between 63753da and d248302.

📒 Files selected for processing (6)
  • infra/terraform/local/main.tf
  • pipelines/drift/db.py
  • scripts/dev-start.sh
  • services/classifier/main.py
  • services/classifier/model.py
  • services/stream-processor/main.py

📝 Walkthrough

Walkthrough

This 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.

Changes

Kafka Image Pinning

Layer / File(s) Summary
Kafka image version pin
infra/terraform/local/main.tf
Broker StatefulSet and kafka-topic-init Job images are pinned from apache/kafka:3.9.0 to 4.3.1, with comments on KRaft PVC compatibility constraints.

Model Registry Selection and Registration

Layer / File(s) Summary
Active model version query
pipelines/drift/db.py
Selection logic changed to ORDER BY created_at DESC instead of status-prioritized ordering, with docstring rationale update.
Classifier registration logic
services/classifier/main.py
Introduces an active registry variable and changes registration condition/path source to use model_dir/active["model_path"] instead of _classifier.model_path.

dev-start.sh psql Variable Substitution Fix

Layer / File(s) Summary
psql stdin piping
scripts/dev-start.sh
Password sync and model path UPDATE statements are piped via stdin into psql instead of using -c, enabling variable substitution.

Classifier Padding and Stream-Processor DB Error Handling

Layer / File(s) Summary
Tokenizer padding revert
services/classifier/model.py
Tokenizer padding changed from "max_length" to True, with comment on OOM cause.
Postgres write rollback handling
services/stream-processor/main.py
Adds a psycopg.Error handler that logs, rolls back, and re-raises, plus docstring updates for _pg_write.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Estimated code review effort: 3 (Moderate) | ~25 minutes


Note

🎁 Summarized by CodeRabbit Free

Your 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 @coderabbitai help to get the list of available commands.

@VjayRam
VjayRam merged commit cd11ae7 into master Jul 3, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant