Skip to content

Fix/detector readiness - #12

Merged
inesaranab merged 5 commits into
mainfrom
fix/detector-readiness
Aug 11, 2026
Merged

Fix/detector readiness#12
inesaranab merged 5 commits into
mainfrom
fix/detector-readiness

Conversation

@inesaranab

@inesaranab inesaranab commented Aug 11, 2026

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

  • New Features
    • Added detector availability checks before screening begins, with polling and configurable time limits.
    • Jobs are now marked unavailable when the detector cannot be reached, instead of being processed prematurely.
  • Bug Fixes
    • Prevented completed jobs from being overwritten during abandonment.
    • Improved request-size validation to account for JSON escaping accurately.
    • Reduced the default guardrail timeout to stay within platform ingress limits.
  • Documentation
    • Added guidance on detector startup behavior, ingress timeouts, and GPU cost considerations.

The first end-to-end run in production failed after exactly 240 seconds:

  13:32:50  guardrail begins
  13:36:50  API call failed on attempt 1: stream timeout

That is the platform's ingress limit, and the finding is that it applies to
internal ingress too. Moving the caller off the HTTP path was not sufficient:
the worker still reaches the detector over HTTP, so somebody was still holding a
connection open across a 13-minute cold start. llm_guardrail_timeout_s of 900
never applied, because the proxy severs the connection first -- the same way
requestTimeout never applied on the public side.

The worker now establishes readiness by repeating a short request until the
detector answers, then screens against a warm server, where the call returns in
seconds. No single request spans the cold start. The probe doubles as the
activation, since a request to an app at zero replicas is what starts it.

A job taken while the detector stays unreachable is recorded as failed rather
than attempted, because attempting it would spend the whole request budget on a
call that cannot succeed and end in the same failure.

Records in infra/gemma/README.md that the 240s limit is not specific to public
ingress, and that cost scales with wake-ups rather than screenings: the same
fifty screenings cost about EUR 2 batched and about EUR 50 scattered.
…blished

Two defects found reviewing the async split.

abandon() overwrote any job, including one already carrying a result. A worker
that records an outcome and then dies before deleting its message leaves that
message to be redelivered; on the delivery that exceeds the limit, drain()
abandons the job and rewrites the row, so a caller that had already read a result
would later be told the screening failed. Only a job still PENDING is settled now.

The queue-size validator counted raw UTF-8 bytes, but the fields are published
inside a JSON document, where a control character occupies six bytes rather than
one. A transcript of 24,000 such characters passed every cap and produced a
144,107-byte message against a 64 KiB ceiling. The rejection then arrived from
the transport after the job row existed, so the caller was told to retry a body
that can never be published. The validator now measures the escaped size, which
is the size that actually travels.
llm_guardrail_timeout_s was 900 seconds, which could never fire: ingress severs
any single request at 240 seconds, internal routes included. The call ended as a
transport error from the proxy rather than a timeout naming the endpoint, which
is what made the first production failure read as an unexplained stream timeout.

It no longer has to cover the cold start either. The worker establishes that the
detector is serving by repeating a short probe before screening anything, so this
bounds a call to an endpoint already known to be up.

That also closes the budget flagged in review. Worst case per job was 900 + 900 +
60 = 1860 seconds, above both the queue's visibility timeout and replicaTimeout,
so a message could reappear while the execution holding it still ran. It is now
900 + 200 + 60 = 1160.
.codex/ was swept in by a broad add. It is local tooling config, not part of the
detector readiness fix, and belongs in its own change if it is to be tracked.
@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

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

Next review available in: 43 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 539da68e-4c68-406c-869d-89c98a70561c

📥 Commits

Reviewing files that changed from the base of the PR and between 27956d4 and d216ed0.

📒 Files selected for processing (14)
  • .codex/config.toml
  • .codex/hooks.json
  • .codex/hooks/conventions_reminder.py
  • .codex/hooks/response_style.py
  • app/adapters/detector_readiness.py
  • app/adapters/job_store_memory.py
  • app/adapters/job_store_table.py
  • app/config.py
  • app/domain/service.py
  • app/ports/job_store.py
  • infra/gemma/README.md
  • tests/unit/test_detector_readiness.py
  • tests/unit/test_job_store_memory.py
  • tests/unit/test_job_store_table.py
📝 Walkthrough

Walkthrough

Changes

Detector readiness and job handling

Layer / File(s) Summary
Readiness probe and polling
app/adapters/detector_readiness.py, tests/unit/test_detector_readiness.py
Adds an HTTP /models probe that accepts only status 200. Polling retries until readiness or a deadline. Probe failures count as not-ready.
Worker readiness integration
app/worker.py, app/config.py, tests/unit/test_worker.py, tests/unit/test_config.py, infra/gemma/README.md
The worker checks detector readiness before screening. Unavailable jobs are marked DetectorUnavailable and are not screened. Timing defaults and deployment guidance are updated.
Pending-job abandonment
app/domain/service.py, tests/unit/test_service_async.py
abandon changes only pending jobs and preserves completed results and status.

JSON-aware request sizing

Layer / File(s) Summary
Escaped request-size validation
app/domain/models.py, tests/unit/test_models.py
Request validation now counts JSON quotes and escaped UTF-8 bytes. Tests cover exact-limit requests and control-character expansion.

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

Sequence Diagram(s)

sequenceDiagram
  participant Worker
  participant DetectorClient
  participant Detector
  participant ScreenService
  Worker->>DetectorClient: Probe /models
  DetectorClient->>Detector: GET /models
  Detector-->>DetectorClient: HTTP status
  DetectorClient-->>Worker: Readiness result
  alt Detector ready
    Worker->>ScreenService: Screen queued job
  else Detector unavailable
    Worker->>ScreenService: Abandon job with DetectorUnavailable
  end
Loading

Possibly related PRs

  • inesaranab/screening#11: This PR extends the same worker, configuration, model, service, and test areas with readiness polling, timeout changes, JSON-size validation, and safe abandonment.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies detector readiness, which is the main focus of the changes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/detector-readiness

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (2)
app/worker.py (1)

13-17: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Declare httpx as a direct dependency.

app/worker.py and multiple tests import httpx. Add httpx to project.dependencies in pyproject.toml and regenerate uv.lock.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/worker.py` around lines 13 - 17, Declare httpx as a direct project
dependency in pyproject.toml under project.dependencies, then regenerate uv.lock
so the lockfile reflects the updated dependency metadata. Preserve the existing
imports and dependency versions unless resolution requires changes.
tests/unit/test_models.py (1)

81-102: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Assert the full queue payload stays within 64 KiB.

Use encode_message and UTF-8 byte length in test_a_request_at_the_byte_limit_is_accepted. This covers the job identifier and JSON envelope that _json_string_bytes does not measure. The queue client sends raw text by default.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/test_models.py` around lines 81 - 102, Update
test_a_request_at_the_byte_limit_is_accepted to measure the complete queue
payload using encode_message, then assert its UTF-8 byte length equals
MAX_REQUEST_BYTES. Replace the _json_string_bytes-only calculation so the job
identifier and JSON envelope are included, matching the queue client’s raw-text
payload behavior.

Source: MCP tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/adapters/detector_readiness.py`:
- Around line 55-70: Update the readiness polling function containing the
waited/interval_s/deadline_s loop to use a monotonic clock and elapsed time
rather than incrementing waited only after sleep. Recalculate remaining time
before each probe and sleep, stop immediately once the deadline has elapsed, and
preserve returning True when probe succeeds. Add a test that advances or
consumes monotonic time during probe execution and verifies polling stops at the
configured deadline.

In `@app/config.py`:
- Around line 42-51: Update the Settings documentation for
llm_guardrail_timeout_s to remove the stale claim that the timeout covers GPU
cold starts, keeping the explanation that readiness polling handles startup
before screening and that the value remains below ingress limits.

In `@app/domain/service.py`:
- Around line 118-123: Make pending-only abandonment atomic by replacing the
separate get/status check and fail sequence in the abandonment method with a
JobStore contract operation such as fail_if_pending(job_id, reason). Implement
this operation atomically in InMemoryJobStore._settle() under its lock and with
a conditional ETag update in Azure Table Storage, preserving the skip behavior
for already-settled jobs. Add an interleaving test that completes the job
between abandonment’s attempted read and write and verifies the completed result
is not overwritten.

In `@infra/gemma/README.md`:
- Around line 375-379: Update the fenced log block containing
“worker_job_started” and “guardrail begins” to specify the text language on its
opening fence, preserving the log contents unchanged.

---

Nitpick comments:
In `@app/worker.py`:
- Around line 13-17: Declare httpx as a direct project dependency in
pyproject.toml under project.dependencies, then regenerate uv.lock so the
lockfile reflects the updated dependency metadata. Preserve the existing imports
and dependency versions unless resolution requires changes.

In `@tests/unit/test_models.py`:
- Around line 81-102: Update test_a_request_at_the_byte_limit_is_accepted to
measure the complete queue payload using encode_message, then assert its UTF-8
byte length equals MAX_REQUEST_BYTES. Replace the _json_string_bytes-only
calculation so the job identifier and JSON envelope are included, matching the
queue client’s raw-text payload behavior.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 7f63e596-fea5-40df-be8e-46e7ebf4e5b0

📥 Commits

Reviewing files that changed from the base of the PR and between a2a66e8 and 27956d4.

📒 Files selected for processing (11)
  • app/adapters/detector_readiness.py
  • app/config.py
  • app/domain/models.py
  • app/domain/service.py
  • app/worker.py
  • infra/gemma/README.md
  • tests/unit/test_config.py
  • tests/unit/test_detector_readiness.py
  • tests/unit/test_models.py
  • tests/unit/test_service_async.py
  • tests/unit/test_worker.py

Comment thread app/adapters/detector_readiness.py Outdated
Comment thread app/config.py
Comment thread app/domain/service.py Outdated
Comment thread infra/gemma/README.md Outdated
…cally

Four findings from review of the readiness change.

The readiness deadline counted only the intervals between probes, not the time
the probes themselves took. A probe against an endpoint that is not listening
consumes its full timeout before failing, so at the worker's values the wait
could reach 2,730 seconds against a configured 900 -- past the limits the
deadline exists to stay inside. It now measures elapsed time on a monotonic
clock.

abandon() still read the job and wrote the failure as two operations, so a
completion landing between them was overwritten. The decision moves into the
store as fail_if_pending: one operation, guarded by a lock in memory and by a
conditional write on the row's etag in Table Storage. The table's test double
now models etags, since one that accepted any write would hide exactly the
failure being guarded against.

Also corrects the Settings documentation, which still described the guardrail
timeout as covering a cold start, and labels a fenced block in the runbook.
@inesaranab
inesaranab merged commit 735d35c into main Aug 11, 2026
2 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