Phases 1-7: classifier, optimizer, infra/observability, stream processing, drift detection, and full MLOps retraining loop#47
Conversation
…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>
|
Warning Review limit reached
Next review available in: 19 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the 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 configurationConfiguration used: defaults Review profile: CHILL Plan: Free Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (7)
📝 WalkthroughWalkthroughThis 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. ChangesAirflow Orchestration, MLflow, Label-UI, Retraining
Classifier and Stream-Processor Moderation API Alignment
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
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)
Estimated code review effort: 5 (Critical) | ~120 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 |
…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>
Summary
Merges the full
developmentbranch (formerlyfeat/classifier) intomaster— everything built since the last merge, spanning Phases 1 through 7 of the project plan:DynamicBatcher, OpenAI-compatible/v1/moderationsendpoint, Prometheus metrics, model registry integration.model_registryregistration.orchestration/drift_dag.py.services/label-ui) feeds a fine-tuning pipeline (pipelines/retraining, logging to MLflow) that reuses the optimizer/evaluation pipelines unchanged;orchestration/retrain_dag.pygates promotion on the quality gate and rolls out the classifier/stream-processor on success.drift_dag.pytriggers this automatically when drift is detected; a human can also trigger it manually from the labelling UI.Every
explanation.mdacross 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'sexplanation.mdfor details.Test plan
terraform validate/terraform planclean against the local k3d workspace./scripts/dev-start.shbrings up the full stack from a clean cluster (all pods ready, bothretrain_daganddrift_dagunpaused)/health/ready,/classify,/v1/moderationsall respond correctlyscripts/simulate-traces.pytraffic shows up inclassificationsandflagged_content:8001): label a batch of flagged content, confirm accept/reject persists and survives simulated Kafka redeliveryretrain_dagfrom the label UI; confirm a run completes, logs to MLflow (:5000), and (on a passing quality gate) promotes + restarts the classifierdrift_dag; confirm it submits the Spark job, readsdrift_stats, and branches correctly🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Bug Fixes
Documentation