IBEX Harness
DocsBenchmarksBlogChangelogRoadmap
GitHub
IBEX Harness

Documentation

Architecture Decision RecordsADR-0002: Repository foundation bootstrapADR-0003: Branch protection and merge policyADR-0004: Protobuf and code generation policyADR-0005: Postgres migration strategyADR-0006: Auth protobuf contract (`ibex.auth.v1`)ADR-0007: Auth token validation implementationADR-0008: Security scanning and CI quality gatesADR-0009: Permission bitmap layoutADR-0010: Cryptography policyADR-0011: Proxy auth gRPC client and middlewareADR-0012: Proxy request normalization (OpenAI chat)ADR-0013: Proxy input validation and stable error envelopeADR-0014: Core domain migration sequencingADR-0015: Proxy rate limit skeleton (Phase 1)ADR-0016: Proxy agent identity verification (Phase 1)ADR-0017: Request ID and trace context strategy (Phase 1)ADR-0018: Graceful shutdown contract (Phase 1)ADR-0019: OpenTelemetry provider configuration (Phase 1)ADR-0020: Shared package boundaries — `packages/config` and `packages/apierror`ADR-0021: Prometheus Metric Catalog (Phase 1)ADR-0022: Health check contract (Phase 1)ADR-0023: Docs site architecture (Phase 1.5)ADR-0024: Benchmark data publishing modelADR-0025: LLM provider abstractionADR-0026: OpenAI client designADR-0027: Streaming dual-write strategyADR-0028: Auth cache designADR-0029: Token revocation propagation via Redis pub/subADR-0030: Directive versioning strategyADR-0031: System prompt injection strategyADR-0032: Session data model and retentionADR-0033: ClickHouse llm_traces schema and retentionADR-0034: Proxy overhead performance measurement methodologyADR-0035: Chat Idempotency-Key Redis dedupeADR-0038: Context assembly service design and gRPC contractADR-0039: Proxy Postgres ownership for session and directive storesADR-0040: Anthropic provider adapterADR-0041: Model capability registryADR-0042: Self-hosted OpenAI-compatible LLM adapterADR-0043: Tokenizer registry architectureADR-0044: Non-streaming response pipelineADR-0045: Streaming response transformationADR-0046: Embedder interface and profile registryADR-0047: Memory temporal validity foundationADR-0048: Memory multi-label categoriesADR-0049: Memory relationship graph readinessADR-0050: MCP server skeleton (transport, auth, audit)ADR-0051: Local LGTM observability stack (Phase 2.5 exit pull-forward)ADR-0052: Memory schema v2 expand (HNSW, quality columns)ADR-0053: Vector store abstraction and composite scoring v2ADR-0054: In-process Presidio PII detection in memoryADR-0055: Memory write-path exact and near-duplicate dedupADR-0056: Temporal-interval-aware conflict detectionADR-0057: Memory write-path persistence and orchestrationADR-0058: Semantic search FTS fallback triggerADR-0059: Hot cache read pathADR-0060: Memory service tenant isolation architectureADR-0061: Retrieval-quality benchmark methodologyADR-0062: Worker task observability and dead-letter handlingADR-0063: Extraction output schema v2ADR-0064: Extraction provider registry and batch executionADR-0065: Extraction batch idempotency and turn pointer orderingADR-0066: Extraction quality evaluation harnessADR-0067: Context capability catalog generate-and-diff for token budgetsADR-0068: Composite scoring-time relevance gateADR-0069: Context packer bounded DP knapsackADR-0070: Context formatting locked ordering and session nonceADR-0071: Context assembly gRPC degradation ladder and retrieval deadlineADR-0072: Extraction enqueue via worker HTTP (not Celery Redis wire format)ADR-0073: Organization Lifecycle and Cascading SuspensionADR-0074 Provider Credential StorageADR-0075 Per-Org Model Routing PolicyADR-0076: Circuit Breaker per ProviderADR-0077: Provider Fallback RoutingADR-0078: Operator runtime topology & environment contract (4.P.0)ADR-0079: Operator session RS256 dual-verify and TOTP step-upIBEX Console service rename and boundaryADR-0081: Fail-closed Proxy runtime controlsD2 metadata-only trace read contractD2 publication, identity, query, and lifecycle contract
ADRs›ADR-0062: Worker task observability and dead-letter handling
ADRs

ADR-0062: Worker task observability and dead-letter handling

Architecture decision record 0062 — OTel per-task spans, Postgres failed_tasks dead-letter store, Prometheus counter + alert.

ADR-0062: Worker task observability and dead-letter handling

  • Status: Accepted
  • Date: 2026-09-02
  • Authors: IBEX Harness team
  • Milestone: 3.5.A.2 Task observability and dead-letter handling

Context

Milestone 3.5.A.1 shipped the Celery worker skeleton with retry policy on IbexTask but no visibility into terminal failures. Without dead-letter handling, a systematically-failing task (misconfigured upstream, bad credentials) accumulates silently in Redis unacked state.

Requirements:

  1. Every task execution emits an OTel span compatible with Go services (ADR-0019).
  2. After retries are exhausted, persist failure context durably and increment a low-cardinality Prometheus counter.
  3. Page operators when dead-letter rate is non-zero.

Options considered

1) Dead-letter destination

  1. Redis / Celery native DLQ — requires a consumer and admin UI to be useful.
  2. Flower as source of truth — operational convenience only; not a production dependency.
  3. Postgres ibex_core.failed_tasks (chosen) — SQL-queryable immediately; survives Flower downtime; aligns with Phase 4 operator dashboard.

2) Span wrapping strategy

  1. Decorate every task function with @traced_task manually — error-prone as task count grows.
  2. Override IbexTask.__call__ to call run_task_in_span (chosen) — guarantees coverage for all tasks using the base class.

3) RLS on failed_tasks

  1. Enable RLS with org filter — blocks cross-org forensics during incidents.
  2. No RLS (chosen) — follows ibex_audit.audit_log pattern; inserts use app.is_service_account = true; reads restricted at application layer.

Decision

OpenTelemetry (first Python bootstrap)

  • Module: services/worker/app/telemetry.py
  • Tracer name: ibex-worker
  • Resource attributes mirror ADR-0019 (service.name, service.version, deployment.environment)
  • Sampling: ParentBased(TraceIDRatioBased(OTEL_SAMPLE_RATIO)), default 0.01
  • Exporter: OTLP gRPC when OTEL_EXPORTER_OTLP_ENDPOINT set; no-op provider otherwise (CI/unit tests)
  • Init: Celery worker_ready signal (once per child process)

Per-task spans (run_task_in_span):

  • Attributes: worker.task, celery.task_id, ibex.org_id / ibex.agent_id / ibex.session_id when present in kwargs
  • Never log or span memory content
  • On exception: record_exception, StatusCode.ERROR, re-raise

Dead-letter handler

Celery task_failure signal in app/observability.py:

Retry guard (critical):

Python
if request is not None and request.retries < sender.max_retries:
    return  # intermediate attempt — autoretry follows

On terminal failure:

  1. Increment ibex_worker_task_dead_letter_total{task_name} when a new failed_tasks row is inserted
  2. Increment ibex_worker_task_dead_letter_persist_failed_total{task_name} when persistence is unavailable or errors
  3. Persist via _run_on_worker_loop(insert_failed_task(...)) — uses the per-child event loop from worker_process_init (_worker_loop.run_until_complete); asyncio.run() is fallback only when no worker loop exists (tests / edge cases). SQLAlchemyError / asyncpg.PostgresError caught and logged.
  4. Does not suppress the original task exception

Tenant context: org_id is required for tenant-scoped tasks; ibex.worker.maintenance.* tasks may persist with NULL org_id (system/maintenance failures). When a non-maintenance task exhausts retries without a valid org_id, persistence is skipped (no row inserted), ibex_worker_task_dead_letter_persist_failed_total increments, and an ERROR log (dead_letter_skipped_missing_org_id) is emitted — fail-closed, not a silent drop.

Idempotency: UNIQUE(task_id) on failed_tasks; IntegrityError → no-op.

Prometheus

  • HTTP /metrics via prometheus_client.start_http_server on IBEX_WORKER_METRICS_PORT (default 8006)
  • Gauge ibex_process_up = 1 when metrics server started
  • Alert IBEXWorkerTaskDeadLettered: rate(ibex_worker_task_dead_letter_total[5m]) > 0 for 1m, severity page

Schema

Migration 000021_create_failed_tasks — see DATABASE_SCHEMA.md § FAILED TASKS.

Consequences

  • Worker integration CI requires Postgres + db-migrate (mirrors memory-integration pattern).
  • Dead-letter persistence reuses the per-child Celery event loop; asyncio.run() is fallback only — not a new loop per failure.
  • Flower remains optional for live debugging; Postgres is the source of truth.
  • Prefork Prometheus multi-process metrics (PROMETHEUS_MULTIPROC_DIR + MultiProcessCollector) are wired in code but lack a full scrape-after-fork integration test in CI — tracked in GitHub #675 (prefork Prometheus multi-process metrics validation).

References

  • ADR-0019: OpenTelemetry provider configuration
  • ADR-0021: Prometheus metric catalog
  • Migration: infra/migrations/postgres/000021_create_failed_tasks.up.sql
  • Alert rule: infra/monitoring/prometheus/rules/ibex-worker.yml

Was this page helpful?

Edit on GitHub

Last updated on

PreviousADR-0061: Retrieval-quality benchmark methodologyNextADR-0063: Extraction output schema v2

On this page

  • Context
  • Options considered
  • 1) Dead-letter destination
  • 2) Span wrapping strategy
  • 3) RLS on failed_tasks
  • Decision
  • OpenTelemetry (first Python bootstrap)
  • Dead-letter handler
  • Prometheus
  • Schema
  • Consequences
  • References
0%