Skip to content

Phases 1-7: classifier, optimizer, infra/observability, stream processing, drift detection, and full MLOps retraining loop#47

Merged
VjayRam merged 8 commits into
masterfrom
development
Jul 4, 2026
Merged

Phases 1-7: classifier, optimizer, infra/observability, stream processing, drift detection, and full MLOps retraining loop#47
VjayRam merged 8 commits into
masterfrom
development

Conversation

@VjayRam

@VjayRam VjayRam commented Jul 4, 2026

Copy link
Copy Markdown
Owner

Summary

Merges the full development branch (formerly feat/classifier) into master — everything built since the last merge, spanning Phases 1 through 7 of the project plan:

  • Classifier service (FastAPI + ONNX Runtime): async batching via DynamicBatcher, OpenAI-compatible /v1/moderations endpoint, Prometheus metrics, model registry integration.
  • Model optimization pipeline: HuggingFace Optimum export → ONNX O2 graph optimization → INT8 dynamic quantization, with MinIO upload and model_registry registration.
  • Local infra (Terraform + k3d): PostgreSQL, MongoDB, MinIO, Kafka, Prometheus, Grafana, Jaeger, OTel Collector, spark-operator, Airflow, and MLflow — all namespace-scoped and fully declared in Terraform.
  • Trace ingestion + stream processing: OTel Collector → Kafka → stream processor (classify → PostgreSQL + MongoDB), with at-least-once delivery, manual offset commits, and idempotent writes on both stores.
  • Drift detection: PySpark job computing PSI/JSD against a reference window, now running automatically every hour via orchestration/drift_dag.py.
  • Full retraining loop: a manual-labelling UI (services/label-ui) feeds a fine-tuning pipeline (pipelines/retraining, logging to MLflow) that reuses the optimizer/evaluation pipelines unchanged; orchestration/retrain_dag.py gates promotion on the quality gate and rolls out the classifier/stream-processor on success. drift_dag.py triggers this automatically when drift is detected; a human can also trigger it manually from the labelling UI.

Every explanation.md across the repo is kept up to date with the actual code, including the live-debugged gotchas found along the way (OOM roots-causes, Kafka/KRaft compatibility, Airflow ConfigMap-mount quirks, ONNX Runtime thread-thrashing, etc.) — see each directory's explanation.md for details.

Test plan

  • terraform validate / terraform plan clean against the local k3d workspace
  • ./scripts/dev-start.sh brings up the full stack from a clean cluster (all pods ready, both retrain_dag and drift_dag unpaused)
  • Classifier: /health/ready, /classify, /v1/moderations all respond correctly
  • Stream processor: scripts/simulate-traces.py traffic shows up in classifications and flagged_content
  • Label UI (:8001): label a batch of flagged content, confirm accept/reject persists and survives simulated Kafka redelivery
  • Manually trigger retrain_dag from the label UI; confirm a run completes, logs to MLflow (:5000), and (on a passing quality gate) promotes + restarts the classifier
  • Manually trigger drift_dag; confirm it submits the Spark job, reads drift_stats, and branches correctly

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added Airflow-based orchestration for health checks, drift detection, and retraining workflows.
    • Introduced MLflow tracking and a new manual labeling UI for reviewing items and triggering retraining.
    • Added retraining support with dataset building, model fine-tuning, evaluation, and promotion steps.
  • Bug Fixes

    • Improved data handling so labeling decisions are preserved during reprocessing.
    • Updated local service startup and port-forwarding for the new UI and tracking tools.
  • Documentation

    • Expanded setup and architecture docs for local development, orchestration, retraining, drift, evaluation, and labeling.

VjayRam and others added 6 commits July 3, 2026 00:15
…nfigMap DAGs)

Deploys Airflow into sentinel-pipeline via the official Helm chart, reusing
existing infra rather than adding new stateful services: LocalExecutor (no
Celery/Redis/Flower), the existing PostgreSQL instance (new "airflow"
database), and DAG files mounted from orchestration/*.py via a ConfigMap
(same file-read-and-embed pattern main.tf already uses for Prometheus/
Postgres config) instead of git-sync or a custom image.

Added a healthcheck_dag.py placeholder to prove the deployment mechanism
works before any real pipeline logic (retrain_dag.py, Phase 7.3) depends
on it. Also pre-provisions the ServiceAccount + Role + RoleBinding for
kubectl rollout restart in sentinel-app, needed once the retrain DAG's
promotion task lands, so it doesn't need re-plumbing through the chart
values later.

Three real bugs found and fixed via live iteration against the actual
cluster (not caught by `helm template`/`terraform validate` alone):

1. wait-for-airflow-migrations init container crash-looped forever — no
   migrateDatabaseJob Job was ever created. Chart default didn't reliably
   trigger it with an external (non-subchart) postgresql; set
   migrateDatabaseJob.enabled explicitly.

2. extraVolumes/extraVolumeMounts silently did nothing when set at the
   top level of the Helm values — this chart scopes them per-component
   (scheduler/webserver/workers/triggerer), not globally. No error, just
   an empty /opt/airflow/dags on every pod. Moved under scheduler.* and
   webserver.*.

3. Even correctly mounted, Airflow's DAG-directory walker
   (find_path_from_directory) raised "Detected recursive loop" against a
   ConfigMap volume mounted as a whole directory — Kubernetes mounts
   ConfigMaps through a "..data -> ..<timestamp>" symlink indirection for
   atomic updates, which Airflow's walker doesn't handle. Switched to one
   subPath mount per DAG file (local.dag_volume_mounts, built from the
   same fileset() the ConfigMap data uses), which bypasses the symlink
   structure entirely.

Also worked around two Terraform/Helm-provider-level issues: an
interrupted apply left a Helm release stuck in "pending-install" with
healthy underlying pods (fixed by patching the release secret's status
directly and importing into Terraform state, rather than a destructive
uninstall/reinstall of already-working pods), and a "Provider produced
inconsistent final plan" error from using a "~> 1.15" version constraint
(fixed by pinning the exact resolved version, 1.15.0).

Verified live: terraform plan clean (zero drift), scheduler 2/2 Running,
webserver 1/1 Running, migration Job completed, healthcheck DAG parses
with zero import errors, and a manually triggered run completed with
state=success end to end.

CLAUDE.md's target folder structure updated: dags/ -> orchestration/,
per user preference over the originally-planned name.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Extends the standard dev-start.sh flow to cover Phase 7.1's Airflow
deployment the same way every other component is handled:

- Waits for airflow-scheduler and airflow-webserver readiness alongside
  the other data-layer/monitoring pods
- Verifies healthcheck_dag.py actually loads with zero import errors
  (airflow dags list-import-errors), not just that the pods are Running —
  a pod can be healthy while the DAG itself fails to parse, which is the
  actual signal worth checking. Polls for up to 60s rather than triggering
  a new DAG run every invocation, which would clutter run history for no
  added benefit given the DAG's trivial logic.
- Opens the airflow-webserver port-forward (localhost:8080) alongside the
  others, with the same wait_for_port confirmation
- Adds the Airflow UI to the final summary banner

Verified live: full dev-start.sh run completes with "Airflow DAGs loaded
with no import errors" and "Airflow ready on localhost:8080" both firing
correctly, ending in the normal success banner.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
http://localhost:8080 wasn't loading — reproduced live: something was
LISTENing on 0.0.0.0:8080 that lsof/fuser couldn't attribute to any local
process. Root cause: k3d-sentinel-serverlb (k3d's own load balancer
container) already publishes host port 8080 -> its internal ingress
(0.0.0.0:8080->80/tcp) by default, entirely unrelated to anything in this
repo. kubectl port-forward was silently losing that race.

Moved the local side of the port-forward to 8090 (kubectl port-forward
supports different local/remote ports — the pod/Service still listens on
8080 internally, only the host-side binding changed). Updated dev-start.sh's
port-forward, wait_for_port call, and summary banner, plus the matching
Terraform output description.

Verified live: curl http://localhost:8090/health now returns HTTP 200 with
scheduler/metadatabase status healthy.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Fixes the "Usage of a dynamic webserver secret key detected" warning
banner. Without webserverSecretKey/webserverSecretKeySecretName set, the
chart auto-generates a random Flask secret key on every deploy — every
terraform apply that touches the release invalidates all webserver
sessions, and multi-replica setups could disagree on the key (not an
issue at replicas=1 here, but still the wrong default to build on).

Added airflow_webserver_secret_key variable (sensitive, dev default,
same pattern as postgres_password/airflow_admin_password) and a
Terraform-managed Secret, wired in via webserverSecretKeySecretName.

Named airflow-webserver-secret-key-static rather than the chart's own
default name (airflow-webserver-secret-key) — that name was already
taken by the chart's auto-generated Secret from the original deploy;
Helm pruned the now-orphaned auto-generated one once the chart stopped
rendering it.

Verified live: the DASHBOARD_UIALERTS warning block is no longer present
in the rendered airflow_local_settings.py, and deleted+recreated the
webserver pod to confirm the secret key byte-for-byte matches before and
after — genuinely static now, not per-deploy.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Brings every explanation.md back in sync with the code after the Phase 5-7
work (drift detection, model optimizer registry/upload fixes, classifier
persistence/registration fixes, stream processor /v1/moderations migration,
Airflow orchestration): fixes stale details (KAFKA_CFG_* env vars, host-
machine deployment, hardcoded /classify/batch persist flag, register_model
inserting 'active'), and documents live-debugged gotchas that previously
existed only in code comments or session history (get_active_model_version's
model_version namespace mismatch, the padding="max_length" OOM revert, the
classifier model-registration bug, the /v1/moderations revert-and-catch,
Airflow's 8 deployment gotchas). Adds new files for orchestration/,
pipelines/drift/, and pipelines/evaluation/, which previously had none.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…pipeline, and automated drift-triggered retraining

Completes Phase 7 (orchestration + experiment tracking):

- infra/mlflow, infra/terraform/local/mlflow.tf: MLflow tracking server
  (custom image with psycopg2/boto3, Postgres backend store, MinIO
  artifact store).
- services/label-ui: manual labelling UI for MongoDB flagged_content —
  review model predictions, assign ground-truth labels, accept/reject for
  training, and trigger retraining directly.
- pipelines/retraining: fine-tunes the classifier on manually-labelled
  data, logs dataset/config/per-epoch+final metrics to MLflow, then hands
  off to the existing optimizer/evaluation pipelines unchanged.
- orchestration/retrain_dag.py: runs the retraining pod, gates promotion
  on the quality-gate result, and rolls out classifier/stream-processor on
  success.
- orchestration/drift_dag.py: runs hourly, submits the drift Spark job,
  and automatically triggers retrain_dag when drift_flagged is true —
  closing the loop CLAUDE.md's data flow describes ("Airflow DAG: triggers
  retrain when PSI > 0.2"). Submits/polls/cleans up the SparkApplication
  via plain Kubernetes API calls after apache-airflow-providers-cncf-
  kubernetes' SparkKubernetesOperator/Sensor turned up three separate
  undocumented incompatibilities.
- writer.py: manual_label/training_decision now use $setOnInsert so Kafka
  redelivery can never clobber a completed manual label.
- dev-start.sh: builds/imports the new images and unpauses retrain_dag +
  drift_dag automatically on a fresh cluster bring-up.
- explanation.md updates across infra/, orchestration/, pipelines/drift/,
  services/stream-processor/ documenting the new components and the
  live-debugged gotchas found along the way (MLflow host-header rejection,
  OOM from fixed-length padding, ONNX Runtime thread-thrashing under a
  tight CPU limit, subPath ConfigMap mounts not live-updating, and the
  SparkKubernetesOperator naming/labels/delete-timing issues).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@VjayRam, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 19 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Free

Run ID: 354c52c4-7138-4b64-bc1b-c36a3c00f21f

📥 Commits

Reviewing files that changed from the base of the PR and between 4b77502 and 8d14b69.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (7)
  • infra/terraform/local/airflow.tf
  • orchestration/drift_dag.py
  • orchestration/retrain_dag.py
  • pipelines/optimizer/upload.py
  • pipelines/retraining/dataset.py
  • pipelines/retraining/pipeline.py
  • pipelines/retraining/train.py
📝 Walkthrough

Walkthrough

This PR adds a Phase 7 orchestration layer: Terraform-managed Airflow, MLflow, and label-ui deployments; Airflow DAGs for healthcheck, retraining, and drift detection; a new retraining pipeline package; dev-start.sh automation; and documentation/code updates aligning classifier and stream-processor behavior with the OpenAI-compatible moderation endpoint and idempotent MongoDB writes.

Changes

Airflow Orchestration, MLflow, Label-UI, Retraining

Layer / File(s) Summary
Top-level docs and infra overview
CLAUDE.md, infra/explanation.md
Renames example DAG folder to orchestration/ and rewrites infra overview, directory structure, and connectivity docs for new components.
Terraform Airflow deployment and RBAC
infra/terraform/local/airflow.tf, infra/terraform/local/variables.tf, infra/terraform/local/outputs.tf, infra/terraform/local/explanation.md
Adds DAG ConfigMap mounts, webserver secret, ServiceAccount/RBAC for pod-launching and SparkApplication CRDs, mirrored secrets, and the Airflow Helm release with new variables/outputs.
Data-plane storage updates
infra/terraform/local/main.tf, infra/terraform/local/explanation.md
Adds airflow/mlflow Postgres databases, a Mongo compound index, an mlflow MinIO bucket, Kafka formatting cleanups, and Kafka gotcha docs.
MLflow image and deployment
infra/mlflow/Dockerfile, infra/mlflow/explanation.md, infra/terraform/local/mlflow.tf
Adds psycopg2/boto3 to the MLflow image and provisions its Kubernetes Deployment/Service against Postgres and MinIO.
Label-UI service and deployment
infra/terraform/local/label-ui.tf, services/label-ui/*
Adds Terraform deployment/service plus the FastAPI label-ui app (queue, label, stats, trigger-retrain endpoints) and static UI.
Airflow DAGs
orchestration/healthcheck_dag.py, orchestration/retrain_dag.py, orchestration/drift_dag.py, orchestration/explanation.md
Adds healthcheck, retrain (train → promote → rollout restart), and drift (Spark job → Postgres check → retrain trigger) DAGs with documentation.
Retraining pipeline package
pipelines/retraining/*, pyproject.toml
Adds dataset building, training, and pipeline orchestration reusing optimizer/evaluation stages, plus CLI entrypoint and Dockerfile.
Pipeline documentation
pipelines/drift/explanation.md, pipelines/evaluation/explanation.md, pipelines/optimizer/explanation.md
Updates drift/evaluation/optimizer docs to describe metrics, gating, and upload/registry behavior aligned with the new DAGs.
Dev-start automation
scripts/dev-start.sh
Adds image builds, readiness waits, DAG unpausing/verification, and port-forwards for Airflow/MLflow/Label-UI.

Classifier and Stream-Processor Moderation API Alignment

Layer / File(s) Summary
Classifier service documentation
services/classifier/explanation.md
Documents settings-driven configuration, /v1/moderations schema, bounded batching, graceful shutdown, and persistence-skip header.
Stream-processor upsert changes and docs
services/stream-processor/writer.py, services/stream-processor/explanation.md
Changes write_flagged_content to preserve manual_label/training_decision via $setOnInsert and updates docs for /v1/moderations chunking and idempotent writes.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant DriftDAG
  participant SparkOperator
  participant Postgres
  participant RetrainDAG
  participant Kubernetes
  DriftDAG->>SparkOperator: submit SparkApplication
  SparkOperator-->>DriftDAG: job terminal state (COMPLETED/FAILED)
  DriftDAG->>Postgres: query latest drift_stats row
  DriftDAG->>RetrainDAG: trigger_dag_run (if drift_flagged)
  RetrainDAG->>Kubernetes: run_retraining pod (KubernetesPodOperator)
  RetrainDAG->>Postgres: promote model_version (decide_promotion)
  RetrainDAG->>Kubernetes: patch rollout restart annotations
Loading
sequenceDiagram
  participant Browser
  participant LabelUI
  participant MongoDB
  participant Airflow
  Browser->>LabelUI: GET /api/queue
  LabelUI->>MongoDB: find pending flagged_content
  Browser->>LabelUI: POST /api/label/{doc_id}
  LabelUI->>MongoDB: $set manual_label, training_decision
  Browser->>LabelUI: POST /api/trigger-retrain
  LabelUI->>Airflow: POST dagRuns (retrain_dag, basic auth)
Loading

Estimated code review effort: 5 (Critical) | ~120 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 and others added 2 commits July 3, 2026 22:00
…terraform formatting

- uv.lock was stale after adding pipelines/retraining to the workspace
  members (uv lock --check was failing).
- orchestration/retrain_dag.py and infra/terraform/local/airflow.tf had
  formatting drift (ruff format, terraform fmt) from manual edits made
  outside their respective formatters.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…nd correctness

Fixes the 10 findings from the PR #47 code review, most severe first:

1. Automated retrain never checked for accuracy regression: validate()
   was called with baseline=None, so MAX_ACCURACY_DROP never ran on the
   drift-triggered path. Fixed by persisting each run's benchmark report
   to MinIO (upload_benchmark_report/download_report, new in
   pipelines/optimizer/upload.py) and looking up the currently active
   model's stored report as baseline before validating.
2. The fully-automated drift->retrain path always crashed when nobody had
   labelled anything yet (build_dataset raises with zero accepted docs,
   and retrain_dag never sets INITIAL_DATASET_PATH). Fixed by catching
   this in pipeline.py and reporting a clean gate_passed=False result,
   which decide_promotion already handles correctly.
3. decide_promotion could promote a model whose model_path is a local
   fallback path (MinIO was unreachable during that optimizer run) that
   no other pod can ever load. Now refuses to promote local-fallback paths.
4. wait_for_drift_job could crash with an uncaught ApiValueError (does not
   inherit from ApiException, confirmed live) if submit_drift_job's XCom
   was missing. Added the same None-guard cleanup_drift_job already had.
5. decide_promotion's promotion UPDATE never checked affected-row count —
   a model_version mismatch would silently retire the active model and
   promote nothing. Now checks rowcount and rolls back on a mismatch.
6. build_dataset's train/val split could produce an empty training set
   with exactly 1 accepted example. Now requires at least 2.
7. check_drift's crash-vs-benign-skip ambiguity documented as a known,
   deliberate limitation (the real fix belongs in drift_job.py's exit-code
   semantics, out of scope for a careful fix of this diff).
8. FRESHNESS_WINDOW was a wall-clock guess; check_drift now correlates
   against this run's own submission timestamp instead, with the window
   as a fallback only.
9. _TrainMetricsCallback's per-epoch train-set forward pass — the actual
   root cause of an earlier OOM — is now capped to a bounded sample
   instead of scanning the full training set every epoch.
10. Documented (not changed) why orchestration/*.py DAGs use psycopg2
    rather than psycopg v3: confirmed live that this Airflow image only
    bundles psycopg2, so matching the rest of the repo would break with
    ModuleNotFoundError.

All fixes verified: ruff/terraform format+lint clean, uv.lock unchanged,
pytest passing, both DAGs dry-run cleanly inside the live Airflow
scheduler pod, and the retraining image's modified modules import
correctly with the new logic paths (dataset size guard, bounded subset)
directly exercised.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@VjayRam
VjayRam merged commit 26a8751 into master Jul 4, 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