Skip to content

perf(cloud-ai): run AWS Rekognition boto3 calls off the event loop - #1205

Merged
groupthinking merged 5 commits into
mainfrom
perf/rekognition-offloop
Aug 1, 2026
Merged

perf(cloud-ai): run AWS Rekognition boto3 calls off the event loop#1205
groupthinking merged 5 commits into
mainfrom
perf/rekognition-offloop

Conversation

@groupthinking

@groupthinking groupthinking commented Aug 1, 2026

Copy link
Copy Markdown
Owner

Head: b5721eb4f

Canonical issue

Closes #1204

Outcome

boto3 is a synchronous SDK. Every Rekognition call sat directly inside an async def, so each blocked the event loop for a whole network round-trip. All 14 now dispatch through await asyncio.to_thread(...).

This PR does This PR does not
Stop the event loop stalling during every Rekognition call Make Rekognition calls themselves any faster
Keep the loop responsive across _wait_for_job_completion's poll loop (up to 120 blocking calls per analysis) Change polling cadence, timeouts or retry semantics
Move the local-image read in _prepare_image_input off the loop Change the http(s) branch (already httpx.AsyncClient)
Preserve every response shape and error path byte-for-byte Add concurrency between the four detect_* calls

The win is loop availability, not latency. While one request waits on Rekognition, other requests, WebSocket frames and health checks now continue to be served.

Scope

src/youtube_extension/integrations/cloud_ai/providers/aws_rekognition.py only.

  • 14 call sites wrapped in await asyncio.to_thread(...) (asyncio.to_thread forwards **kwargs, so keyword arguments are unchanged).
  • New module-level _read_file_bytes(path) -> bytes helper; _prepare_image_input now does await asyncio.to_thread(_read_file_bytes, image_url).
  • Plus 6 new tests. No other module touched.

Design notes

Why to_thread and not gather. The four detect_* calls in analyze_image could run concurrently, but that changes AWS request-rate behaviour and per-call error attribution. This PR deliberately keeps them sequential and fixes only the loop-blocking defect. Parallelising them is a separate, arguable change.

Cancellation / use-after-close. await asyncio.to_thread(...) is not cancellation-safe when a caller tears the client down in a finally — that was a real regression in #1190. Here AWSRekognition.cleanup() only sets self._rekognition_client = None (its own comment: "boto3 clients don't require explicit cleanup"); it closes no transport. A thread already holds the bound method, so an in-flight call completes normally. No shield is warranted — adding one would be unfalsifiable ceremony.

Serialisation. These calls share no local mutable resource, so moving them off-loop removes no implicit ordering guarantee (contrast #1194, which needed a lock).

Risk

Low. Behaviour-preserving; all 89 pre-existing tests pass unmodified. The residual risk is thread-pool saturation under very high concurrency — bounded by the default executor, same as the already-merged #1190/#1196.

Verification

89 pre-existing tests ....................... pass, zero edits
95 total (89 + 6 new) ....................... pass
ruff .......................................  8 findings — exact parity with origin/main (0 added)

Non-vacuity (both dimensions reverted simultaneously): boto3 calls put back on the loop and _read_file_bytes called directly →

5 failed, 90 passed

Exactly the 5 heartbeat tests failed. The 6th new test is a guard (test_local_image_bytes_are_read_correctly) and correctly still passed, as did all 89 pre-existing tests — confirming the mutation was behavioural, not structural.

Each new test runs a heartbeat task alongside the work; blocking I/O on the loop yields 0 ticks.

Review round 2 — replaced a load-sensitive assertion

CI surfaced a single failure at 792604bd5:
TestRekognitionDoesNotBlockEventLoop::test_local_image_read_does_not_stall_the_event_loop.

This was the exact risk I flagged as challenge 4 in the review request. Unlike the four
boto3 tests — which drive a controllable 0.12s mock and passed on CI — the local file read
is a few microseconds of real work, so "did the event loop tick while it ran" measures
scheduler behaviour under load rather than the property I care about.

Replaced it with a direct assertion of the property: record threading.get_ident() inside
_read_file_bytes and require it to differ from the thread running the event loop. That is
precisely what "dispatched off the loop" means, requires no sleeps, and cannot flake under
runner contention.

before after
mechanism elapsed heartbeat ticks > 0 worker thread id != loop thread id
sleeps 0.12s injected none
flakes under load yes (observed on CI) no
discriminates the bug yes yes — 1 targeted failure / 94 passed

Non-vacuity re-proven: reverting line 402 to call _read_file_bytes(image_url) directly
instead of via asyncio.to_thread produces exactly one targeted failure. The other five
tests in the class are unchanged and still pass. Suite: 95 passed.

Review round 3 — the deterministic test was patching the wrong namespace

Round 2's thread-identity test passed locally (including a full 7,731-test
single-process tests/unit run) but failed on CI with
AssertionError: _read_file_bytes was never called — while the result
assertion immediately above it passed. That combination is diagnostic: the
real helper ran and returned the right bytes, but the recording wrapper was
never invoked. The module object returned by a fresh import inside the test
was not the namespace _prepare_image_input resolves names from.

The wider unit suite makes that reachable. tests/unit/test_core_mcp_registry.py
re-registers canonical dotted names (sys.modules[canonical] = mod) and
tests/unit/test_v1_router_extended.py installs MagicMock() under
youtube_extension.* names. Either leaves a class imported earlier holding
__globals__ pointing at a different dict than a later import returns, so
patch.object(<freshly imported module>, ...) silently patches nothing.

Fix: patch type(provider)._prepare_image_input.__globals__ via patch.dict.
That is by definition the namespace the call site looks the name up in, so it
cannot drift no matter what sibling modules do to sys.modules, and it needs
no import at all.

The same round also parametrized the off-loop coverage so that every
converted SDK operation is asserted individually rather than in batches.

Non-vacuity (re-proved for the expanded suite)

Reverting all 15 await asyncio.to_thread(...) sites to direct calls
produces exactly 15 targeted failures, 90 passed — a 1:1 mapping of
converted call site to failing assertion. Restoring the source returns the
file to 105 passed.

Run Result
test_aws_rekognition_provider.py 105 passed (89 pre-existing untouched + 16)
full tests/unit, one process 7,731 passed, 0 failed
all 15 conversions reverted 15 failed / 90 passed
ruff parity with origin/main

Attribution

Commits 2c0fec7f7 and af04aedb5 were pushed to this branch by another
agent while I was preparing an equivalent fix. Its diagnosis matched mine, and
its patch.dict(method_globals, ...) form is strictly better than the
hand-rolled try/finally I had written — it restores on exception and is
idiomatic. I adopted its commits rather than force-pushing over them, then
independently re-ran and re-proved the suite above.

Production evidence

...cloud_ai.providers.aws_rekognition is in the transitive import closure of the primary production entrypoint youtube_extension.main:app (root Dockerfile:93), via main.py:171backend/cloud_ai_routes.py:89,141integrations/cloud_ai/integrator.py:297-298.

Agent handoff

Review focus: the cancellation argument above, and whether keeping the four detect_* calls sequential is the right call.

Review round 4 — bounding the work I moved into the shared thread pool

CodeRabbit raised two Major findings at af04aedb5. They were judged separately.

Finding 2 — no botocore timeouts (aws_rekognition.py:115) — ACCEPTED, fixed in b5721eb4f

This one is a direct consequence of this PR and was fixed here.

initialize() built both clients with session.client('rekognition') / session.client('s3')
and no botocore.config.Config. On its own that is merely untidy. Combined with this PR it
is a real hazard: the 14 SDK calls now run via asyncio.to_thread, i.e. on the process-wide
default executor. botocore's defaults leave a request effectively unbounded, so a stalled AWS
call no longer blocks the event loop (the old, obvious failure) — it silently pins one of that
pool's limited worker threads forever
. That pool is shared with the metrics persistence
(#1194), sqlite access (#1196) and result writes (#1203) already merged onto it, and
_wait_for_job_completion can issue up to 120 such calls per job. This is the same
pool-exhaustion class of regression caught on #1152.

Fix: both clients are constructed with an explicit Config(connect_timeout, read_timeout, retries={'max_attempts', 'mode': 'standard'}). Values are overridable via
AWS_REKOGNITION_CONNECT_TIMEOUT / _READ_TIMEOUT / _MAX_ATTEMPTS, validated with
math.isfinite and raising rather than clamping, so inf, nan, 0 and negatives are
rejected outright.

Parsing is hoisted above initialize()'s try, because that method ends in a catch-all
except Exception -> CloudAIError which would otherwise bury a precise ConfigurationError
behind a generic init failure.

Finding 1 — local path traversal (aws_rekognition.py:37) — REJECTED as out of scope, tracked in #1209

The finding is correct on the merits, but it is pre-existing and unchanged by this PR.
origin/main already read the value verbatim at line 373:

with open(image_url, 'rb') as image_file:

This PR moved that read onto a worker thread. The set of readable paths is byte-for-byte
identical before and after — no widening. Fixing it properly means choosing and enforcing a
media-root policy, handling symlink escape as well as lexical ../, and deciding whether the
local branch should exist in production at all. That is a behavioural security change that
deserves its own PR and its own tests, not a rider on a performance PR.

Filed as #1209 with full acceptance criteria. This mirrors the scope objection CodeRabbit
itself raised on #1152, where an out-of-scope guard was reverted and tracked as #1162.

Verification of round 4

Dimension mutated Expected failures Observed
env helpers ignore the environment and config= dropped from both clients 18 18 failed / 109 passed

Breakdown of the 18: 1 client-config assertion, 1 env-override assertion, 12 timeout-rejection
cases (2 vars x 6 bad values), 4 max-attempts rejection cases. The three "blank value falls back
to default" cases and the "defaults are finite" case correctly still pass — they are guards that
survive this mutation by design.

Suite: 127 passed (105 pre-existing, unmodified except for adding a symmetric
"botocore.config" entry to the three existing patch.dict("sys.modules", ...) stubs, which
already stubbed "botocore.exceptions"; plus 22 new). ruff parity with origin/main unchanged
(8 = 8).

boto3 is a synchronous SDK. Every Rekognition call in AWSRekognition was
issued directly inside an `async def`, so each one blocked the event loop
for a full network round-trip. `_wait_for_job_completion` is the worst
case: it polls every 5s for up to 600s, so a single video analysis could
stall the loop up to 120 times.

All 14 boto3 calls now dispatch via `await asyncio.to_thread(...)`, and
the local-image read in `_prepare_image_input` goes through a new
module-level `_read_file_bytes` helper on the same path.

- 89 pre-existing tests pass with zero edits
- 6 new heartbeat tests (`TestRekognitionDoesNotBlockEventLoop`); 5 of the
  6 discriminate, proven by reverting both dimensions simultaneously
  (5 targeted failures / 90 passed)
- ruff: exact parity with origin/main (8 pre-existing findings, 0 added)

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 1, 2026 22:54
@vercel

vercel Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
v0-uvai Canceled Canceled Aug 1, 2026 11:36pm

@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

Auto reviews are limited based on label configuration.

🏷️ Required labels (at least one) (1)
  • [‘architecture-gap’, ‘bug’, ‘ci-cd’, ‘ci/cd’, ‘copilot-rabbit’, ‘documentation’, ‘duplicate’, ‘enhancement’, ‘frontend’, ‘github_actions’, ‘good first issue’, ‘help wanted’, ‘high-priority’, ‘invalid’, ‘javascript’, ‘ml-model’, ‘needs-triage’, ‘pipeline-critical’, ‘placeholder-code’, ‘priority:high’, ‘python’, ‘python:uv’, ‘question’, ‘styling’, ‘tests’, ‘v0’]

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository YAML (base), Repository UI (inherited), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 26b76c23-383b-4c7b-8efa-8b727e8252c4

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Summary by CodeRabbit

  • Performance

    • Improved responsiveness by running AWS Rekognition operations without blocking other application tasks.
    • Local image processing now runs asynchronously for smoother operation.
  • Bug Fixes

    • Reduced delays and potential stalls during image analysis, video processing, connection checks, and service-status checks.

Walkthrough

AWS Rekognition’s blocking boto3 calls now run through asyncio.to_thread. Local image reads also use a worker thread. Analysis selection, result processing, polling, retries, timeouts, and error handling remain unchanged.

Changes

AWS Rekognition asynchronous I/O

Layer / File(s) Summary
Input and health-check offloading
src/youtube_extension/integrations/cloud_ai/providers/aws_rekognition.py
Local image reads and collection health checks now run in worker threads.
Image analysis offloading
src/youtube_extension/integrations/cloud_ai/providers/aws_rekognition.py
Image label, face, text, and moderation calls now use asyncio.to_thread without changing request parameters or result mapping.
Video submission and polling offloading
src/youtube_extension/integrations/cloud_ai/providers/aws_rekognition.py
Video job submission and polling calls now use worker threads. Existing status handling, retries, timeouts, and error wrapping remain unchanged.

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

Possibly related PRs

Suggested labels: copilot-rabbit

Poem

Boto calls step off the loop,
Threads carry each blocking troop.
Images load and videos flow,
Polling waits without a stall below.
The async path keeps time in tune.

🚥 Pre-merge checks | ✅ 5 | ❌ 2

❌ Failed checks (2 inconclusive)

Check name Status Explanation Resolution
Linked Issues check ❓ Inconclusive [#1204] The source change matches the 14-call and local-read requirements, but test criteria cannot be verified because the test file is excluded by !tests/**. Review tests/unit/test_aws_rekognition_provider.py or provide unfiltered evidence for the six new tests, 89 unchanged tests, and ruff parity.
Enforce Copilot Verification ❓ Inconclusive Pending verification of an explicit GitHub Copilot approval on this pull request. Inspect the pull request review records and confirm a GitHub Copilot review with an approved state.
✅ Passed checks (5 passed)
Check name Status Explanation
Out of Scope Changes check ✅ Passed [#1204] The reviewed changes are limited to AWS Rekognition off-loop dispatch and local-file reads, which match the linked issue.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Require Ai Unit Tests ✅ Passed PR #1205 has the copilot-rabbit label, and commits 792604b/9b3f5e4 add AI-attributed unit tests in tests/unit/test_aws_rekognition_provider.py alongside the provider changes.
Title check ✅ Passed The title clearly and concisely describes moving synchronous AWS Rekognition boto3 calls off the event loop.
Description check ✅ Passed The description covers the issue, outcome, scope, risks, verification, production evidence, and handoff details.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch perf/rekognition-offloop
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch perf/rekognition-offloop

Warning

Review ran into problems

🔥 Problems

These MCP integrations need to be re-authenticated in the Integrations settings: Sentry


Linked repositories: Public OSS repositories can only analyze public repositories installed in this organization. No linked repositories were analyzed; skipped groupthinking/uvai-skills.


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.

@groupthinking

Copy link
Copy Markdown
Owner Author

@coderabbitai review

Adversarial challenges — please try to break these specifically:

  1. Cancellation. I argue no asyncio.shield is needed because AWSRekognition.cleanup() only nulls references and closes no transport (unlike the gRPC channel in perf(cloud-tasks): move blocking gRPC calls off the event loop #1190). Is there any path where a cancelled to_thread call leaves boto3 in a bad state, or where integrator.py:66's await provider.cleanup() runs in a finally around an in-flight call?

  2. Thread-pool saturation. _wait_for_job_completion polls up to 120 times per analysis. With N concurrent video analyses, is the default ThreadPoolExecutor a new bottleneck — and is that worse than the status quo of blocking the loop outright?

  3. Sequential detect_*. I deliberately did NOT parallelise the four calls in analyze_image. Is that the wrong call given they are independent?

  4. Test honesty. 5 of my 6 new tests are heartbeat-based. Are they timing-flaky on a loaded CI runner? They assert ticks > 0 against a 0.12s blocking stub with a 0.005s heartbeat — I chose a floor of 0.005s rather than a count to avoid flakiness, but tell me if that is still too tight.

  5. Coverage gap I will name myself. get_service_status and _test_connection are converted but have no new heartbeat test — they are covered only by the pre-existing tests, which do not assert loop responsiveness. Is that an acceptable gap or should I add them?

Context: reviewers have caught genuine bugs in my previous PRs here (#1186, #1188, #1190, #1194) — including two regressions I introduced myself. Please be equally sceptical.

@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

provider='src/youtube_extension/integrations/cloud_ai/providers/aws_rekognition.py'

echo '=== Changed files ==='
git diff --name-only origin/main...HEAD

echo '=== Provider outline ==='
ast-grep outline "$provider" --items all --type function,class --view expanded

echo '=== Integrator lifecycle references ==='
rg -n -C 8 --type py 'await\s+provider\.cleanup\s*\(|async\s+def\s+.*cleanup|provider\.cleanup' src

echo '=== Provider call sites and cleanup ==='
rg -n -C 5 --type py \
  'asyncio\.to_thread|_rekognition_client|def\s+_read_file_bytes|async\s+def\s+_prepare_image_input|async\s+def\s+_wait_for_job_completion|async\s+def\s+analyze_image|async\s+def\s+get_service_status|async\s+def\s+_test_connection|async\s+def\s+cleanup' \
  "$provider"

echo '=== Tests mentioning provider or heartbeat ==='
fd -e py . | xargs rg -n -l -i 'AWSRekognition|heartbeat|_wait_for_job_completion|analyze_image|get_service_status|_test_connection' || true

echo '=== PR test diff ==='
git diff --unified=80 origin/main...HEAD -- \
  "$provider" \
  '*aws_rekognition*' \
  '*rekognition*'

Length of output: 85988


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '=== CloudAIIntegrator public analysis and cleanup methods ==='
sed -n '35,180p' src/youtube_extension/integrations/cloud_ai/integrator.py

echo '=== Integrator construction, cleanup, and analysis call sites ==='
rg -n -C 8 --type py \
  '(CloudAIIntegrator\s*\(|cloud_ai_integrator|\.analyze_image\s*\(|\.analyze_video\s*\(|\.cleanup\s*\()' \
  src/youtube_extension \
  -g '!src/youtube_extension/integrations/cloud_ai/integrator.py'

echo '=== Relevant provider methods with complete control flow ==='
sed -n '60,250p' src/youtube_extension/integrations/cloud_ai/providers/aws_rekognition.py
sed -n '260,410p' src/youtube_extension/integrations/cloud_ai/providers/aws_rekognition.py

echo '=== Test module setup, fixtures, and async configuration ==='
sed -n '1,180p' tests/unit/test_aws_rekognition_provider.py
fd -HI 'pyproject.toml' 'pytest.ini' 'tox.ini' 'setup.cfg' . -d 3 -x sh -c \
  'echo "=== $1 ==="; rg -n -C 4 "asyncio|pytest" "$1" || true' sh {}

echo '=== All changes to the target test module, with concise context ==='
git diff --unified=25 origin/main...HEAD -- tests/unit/test_aws_rekognition_provider.py

Length of output: 42949


🏁 Script executed:

#!/bin/bash
set -u

echo '=== Locate Rekognition test files ==='
fd -HI -t f '.*aws.*rekognition.*\.py$' . || true

echo '=== Test module locations in the PR diff ==='
git diff --name-only origin/main...HEAD | nl -ba

echo '=== Pytest and asyncio configuration ==='
for file in $(fd -HI -t f 'pyproject.toml|pytest.ini|tox.ini|setup.cfg' . -d 4); do
  echo "=== $file ==="
  rg -n -C 5 'pytest|asyncio|testpaths|python_files' "$file" || true
done

echo '=== Cloud AI router: request and batch lifecycle ==='
sed -n '200,420p' src/youtube_extension/backend/cloud_ai_routes.py

echo '=== Cloud AI integrator construction and all provider map mutations ==='
rg -n -C 6 --type py \
  '(self\.providers|CloudAIIntegrator\s*\(|get_cloud_ai_config\s*\()' \
  src/youtube_extension/integrations/cloud_ai/integrator.py \
  src/youtube_extension/backend/cloud_ai_routes.py

echo '=== Precise await-to-thread call count in the changed provider ==='
ast-grep run --pattern 'await asyncio.to_thread($$$ARGS)' \
  --lang python \
  src/youtube_extension/integrations/cloud_ai/providers/aws_rekognition.py

Length of output: 42681


@groupthinking I found no production-blocking defect in the cancellation or sequencing design.

  1. Cancellation: No asyncio.shield() is needed for this provider.

    AWSRekognition.cleanup() only clears self._rekognition_client and self._s3_client. It does not close a boto3 transport. Each asyncio.to_thread() call receives a bound boto3 method, which retains the client reference after cleanup clears the provider attribute.

    A cancelled task can stop awaiting the worker result. The synchronous boto3 call can still finish in the executor. This is the normal to_thread() cancellation behavior. shield() would not cancel the request or make it safer. It would only delay cancellation propagation.

    The inspected production routes create an integrator in async with CloudAIIntegrator(...). On request cancellation, context exit calls cleanup after the provider coroutine unwinds. I found no path where integrator.py cleanup closes a client during a still-awaiting Rekognition call.

  2. Thread-pool saturation: This is a real capacity limit, but it is not worse than blocking the event loop.

    Each video analysis has at most one Rekognition worker call in flight. The four analysis types and each poll sequence remain sequential. With N concurrent analyses, the process can queue roughly N Rekognition calls in the shared default executor, plus unrelated to_thread() work.

    Queueing can delay polls beyond five seconds. It can also increase cross-request AWS request concurrency compared with the previous event-loop-blocking implementation. These are expected consequences of restoring concurrency. The old behavior prevented unrelated requests, health checks, and WebSocket work from running at all. This PR is a clear improvement.

    Do not add a private executor or semaphore in this PR unless there is an established service-wide concurrency policy. That would change capacity and request-rate behavior beyond the stated scope.

  3. Sequential detect_*: Keeping these calls sequential is correct.

    Parallelizing them would increase per-image AWS request concurrency, alter failure timing, and change which error becomes visible first. It would also make throttling behavior less predictable. This PR should only move synchronous work off the event loop.

  4. Heartbeat timing: The 0.12s blocking stub with a 0.005s heartbeat is acceptable for this purpose.

    The assertion uses ticks > 0, not a precise tick count. This reduces scheduler sensitivity. A severely overloaded runner can still cause a false failure if it cannot schedule the event loop for the full 120 ms interval. That risk is low. I would not increase the delay because it makes the suite slower.

  5. Test coverage: I request changes here.

    test_every_detection_type_runs_off_the_loop does not prove that every detect_* call is off-loop. If detect_faces, detect_text, or detect_moderation_labels regresses to a direct synchronous call, the other offloaded calls can still produce ticks > 0. The test passes.

    The suite also has no responsiveness coverage for _test_connection() or get_service_status(). Both call describe_collection, but they are separate public or lifecycle paths that this PR changes.

    Please parameterize or split the heartbeat tests so each changed boto3 method has an isolated slow stub and an independent ticks > 0 assertion. Add isolated coverage for:

    • _test_connection()
    • get_service_status()
    • detect_labels
    • detect_faces
    • detect_text
    • detect_moderation_labels
    • each start_*_detection method
    • each get_*_detection poll method

    The local-file heartbeat test is good. The non-blocking byte-content guard is also good.

    This test gap does not invalidate the implementation. It does invalidate the claim that the new tests verify all 14 boto3 call sites.

You are interacting with an AI system.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Offloads synchronous AWS Rekognition operations from the event loop.

Changes:

  • Dispatches 14 boto3 calls through asyncio.to_thread.
  • Offloads local image reads.
  • Adds responsiveness and behavior tests.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
aws_rekognition.py Moves blocking AWS and file operations to worker threads.
test_aws_rekognition_provider.py Adds event-loop responsiveness tests.

Comment thread tests/unit/test_aws_rekognition_provider.py Outdated
…elapsed ticks

The heartbeat form of this one test failed on CI. Unlike the four boto3 tests,
which drive a controllable 0.12s mock, the local file read is a few
microseconds of real work, so "did the loop tick while it ran" is a
load-sensitive proxy rather than a property.

Assert the property directly instead: record `threading.get_ident()` inside
`_read_file_bytes` and require it to differ from the thread running the event
loop. That is exactly what "dispatched off the loop" means, needs no sleeps,
and cannot flake under runner contention.

- 95 tests pass (89 pre-existing, unmodified, + 6 new)
- Non-vacuity: calling `_read_file_bytes` directly instead of via
  `asyncio.to_thread` yields exactly 1 targeted failure / 94 passed
- Suite runtime for the file drops to 0.85s (the 0.12s sleep is gone)

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@github-actions

github-actions Bot commented Aug 1, 2026

Copy link
Copy Markdown

Agent Completion Truth Gate: NOT_APPLICABLE

Evidence agrees.

Machine-readable verdict
{
  "details": {},
  "reasons": [],
  "verdict": "not_applicable"
}

Workflow evidence

@groupthinking

Copy link
Copy Markdown
Owner Author

@coderabbitai review

New head 9b3f5e484. Round 1 shipped 14 boto3 calls onto asyncio.to_thread; this round
replaces one test whose assertion was load-sensitive and failed on CI.

Specific challenges for this round:

  1. Is the thread-identity assertion actually equivalent to the property? I assert
    threading.get_ident() inside _read_file_bytes differs from the id captured on the
    loop thread. Is there any way that passes while the read still blocks the loop — e.g.
    could asyncio.to_thread ever run the callable inline on the loop thread?

  2. Did I weaken coverage? The old test also implicitly proved the loop stayed
    responsive. The new one proves only which thread ran. Given the read is
    microseconds, is responsiveness still worth asserting, or is thread identity strictly
    the better signal here?

  3. The other four heartbeat tests still use wall-clock (tick=0.005, delay=0.12 on a
    mocked boto3 call). They passed on CI. Should I convert those to thread-identity too for
    consistency, or does the injected 0.12s make them genuinely safe?

  4. patch.object(_rek_mod, '_read_file_bytes', ...) — line 402 resolves the name as a
    module global at call time, so the patch takes effect. Confirm that's right and that I
    haven't created a test that would silently stop exercising the code path if someone
    refactored the call site to a local import.

  5. Cancellation. Per PR perf(cloud-tasks): move blocking gRPC calls off the event loop #1190 I checked cleanup() (L96-99): it only sets client refs
    to None and closes no transport, so an in-flight to_thread cannot suffer
    use-after-close and I deliberately added no shield. Is that reasoning sound for boto3
    clients specifically?

@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor
🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

provider='src/youtube_extension/integrations/cloud_ai/providers/aws_rekognition.py'

echo '=== Changed files ==='
git diff --name-only origin/main...HEAD

echo '=== Provider outline ==='
ast-grep outline "$provider" --items all --type function,class --view expanded

echo '=== Provider: imports, cleanup, local-image preparation, and thread dispatch ==='
rg -n -C 6 --type py \
  'import asyncio|import threading|def _read_file_bytes|async def cleanup|async def _prepare_image_input|asyncio\.to_thread|_rekognition_client|_s3_client' \
  "$provider"

echo '=== Rekognition tests: outline and revised-test contexts ==='
fd -HI -t f '.*aws.*rekognition.*\.py$' . -x sh -c '
  echo "=== $1 ==="
  ast-grep outline "$1" --items all --type function,class --view expanded
  rg -n -C 12 --type py \
    "_read_file_bytes|threading\.get_ident|patch\.object|heartbeat|tick|sleep|to_thread|local_image" \
    "$1"
' sh {}

echo '=== Exact PR diff for provider and relevant tests ==='
git diff --unified=50 origin/main...HEAD -- \
  "$provider" \
  'tests/unit/test_aws_rekognition_provider.py'

echo '=== Integrator cleanup lifecycle ==='
sed -n '45,110p' src/youtube_extension/integrations/cloud_ai/integrator.py

Length of output: 54768


🤖 web_search executed:

`Official Python asyncio.to_thread documentation: does asyncio.to_thread run the callable in a separate thread, and can it run inline on the event-loop thread?

Official boto3 documentation: are low-level boto3 clients thread-safe, and what does client close() do for in-flight requests or client cleanup?`

Length of output: 12216


@groupthinking I found no blocking defect in this revision.

  1. Thread identity is equivalent to the required property.
    asyncio.to_thread() runs its callable in a separate thread. It delegates to loop.run_in_executor(None, ...), which uses the loop’s default ThreadPoolExecutor. It does not run the callable inline on the event-loop thread. Python documentation and the CPython implementation confirm this behavior.

    The test also calls the real _read_file_bytes. It therefore proves that the actual file read runs on a worker thread.

  2. The revised test does not weaken meaningful coverage.
    It removes an unreliable proxy for this small operation. It directly tests the required invariant: the local file read does not execute on the event-loop thread.

    A heartbeat test cannot reliably measure a read that takes microseconds. A runner can complete the read before the heartbeat task gets scheduled. The thread-identity assertion is the better test for this code path.

  3. Keep the four heartbeat tests. Do not replace them with thread-identity tests.
    The injected 0.12s synchronous delay makes ticks > 0 a strong and useful behavior test. If the synchronous mock runs on the event-loop thread, _count_heartbeats() cancels the heartbeat before it can increment. If the call runs in a worker thread, the event loop has many scheduling opportunities.

    This is still a wall-clock test. An exceptionally overloaded runner could fail to schedule the heartbeat for 120 ms. That is possible, but it is a very low risk. Thread identity would test dispatch implementation. The heartbeat tests test the user-visible requirement: the event loop remains available while boto3 blocks. Retain both test styles for their separate purposes.

  4. The patch targets the active lookup correctly.
    Line 402 resolves _read_file_bytes from aws_rekognition module globals when _prepare_image_input() executes. Therefore, patch.object(_rek_mod, '_read_file_bytes', _recording_read) intercepts this call.

    A refactor to a local import or an inline read would not silently bypass this test. observed['thread_id'] would remain absent and the explicit assertion would fail. A refactor that preserves the module-global lookup continues to receive the intended coverage.

  5. The cancellation reasoning is sound for this provider.
    The bound boto3 method is obtained before the task awaits the executor result. It retains its client reference even if AWSRekognition.cleanup() later clears self._rekognition_client.

    A cancellation can stop the coroutine from awaiting the worker result. It cannot reliably stop a worker that has already started the synchronous boto3 call. However, this provider cleanup does not close the client or its transport. There is no provider-level use-after-close path.

    Boto3 low-level clients are generally thread-safe, subject to the documented restrictions on multiprocess sharing, metadata mutation, and custom botocore events. This provider uses one client in worker threads and does not perform those unsafe operations. Boto3 client documentation

    asyncio.shield() would not cancel an already-running boto3 request. It would only change cancellation propagation to the awaiting task. Do not add it here.

The revised local-image test is a correct replacement for the flaky heartbeat test. No changes requested. 🐇

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@groupthinking
groupthinking marked this pull request as draft August 1, 2026 23:15
@github-actions

github-actions Bot commented Aug 1, 2026

Copy link
Copy Markdown

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Snapshot Warnings

⚠️: No snapshots were found for the head SHA b5721eb.
Ensure that dependencies are being submitted on PR branches and consider enabling retry-on-snapshot-warnings. See the documentation for more information and troubleshooting advice.

Scanned Files

None

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/youtube_extension/integrations/cloud_ai/providers/aws_rekognition.py (1)

260-278: 🎯 Functional Correctness | 🔴 Critical | 🏗️ Heavy lift

Remove the production stub in _ensure_video_in_s3.

Lines 260-278 return a guessed S3 key for every non-s3:// input. The code only logs that an upload would occur. It does not upload the video. Lines 294-320 then submit that nonexistent object to Rekognition, so HTTP and local video inputs fail in production.

Reject non-S3 inputs until upload support exists, or implement the upload with verified bucket and key handling before starting the Rekognition jobs. Remove the demo fallback.

As per path instructions, flag placeholder or stub implementations as blocking issues.

Also applies to: 294-320

🤖 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 `@src/youtube_extension/integrations/cloud_ai/providers/aws_rekognition.py`
around lines 260 - 278, Remove the non-S3 demo fallback from
_ensure_video_in_s3: reject inputs that do not start with s3:// instead of
constructing a guessed bucket/key and logging a placeholder upload. Preserve
valid S3 URL parsing, and ensure the Rekognition job flow only proceeds with
verified existing S3 objects until real upload support is implemented.

Source: Path instructions

🤖 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 `@src/youtube_extension/integrations/cloud_ai/providers/aws_rekognition.py`:
- Around line 34-37: Update analyze_image() and _read_file_bytes() to constrain
local image_url paths to the configured media root: resolve the candidate path,
reject absolute paths and traversal that resolve outside the root, and only then
pass the validated path to asyncio.to_thread. Preserve existing handling for S3
and HTTP URLs.
- Around line 113-115: Update initialize() to create the Rekognition and S3
clients with botocore.config.Config specifying explicit connect_timeout and
read_timeout values, ensuring requests invoked through asyncio.to_thread are
bounded without changing their existing behavior.

---

Outside diff comments:
In `@src/youtube_extension/integrations/cloud_ai/providers/aws_rekognition.py`:
- Around line 260-278: Remove the non-S3 demo fallback from _ensure_video_in_s3:
reject inputs that do not start with s3:// instead of constructing a guessed
bucket/key and logging a placeholder upload. Preserve valid S3 URL parsing, and
ensure the Rekognition job flow only proceeds with verified existing S3 objects
until real upload support is implemented.
🪄 Autofix (Beta)

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: Repository YAML (base), Repository UI (inherited), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 3ec183c6-3bc7-4682-8de3-f46b3c05ce14

📥 Commits

Reviewing files that changed from the base of the PR and between 24dcc30 and 9b3f5e4.

⛔ Files ignored due to path filters (1)
  • tests/unit/test_aws_rekognition_provider.py is excluded by !tests/**
📒 Files selected for processing (1)
  • src/youtube_extension/integrations/cloud_ai/providers/aws_rekognition.py
📜 Review details
⚠️ CI failures not shown inline (16)

GitHub Actions: Agent completion enforcement / 0_Agent completion enforcement.txt: perf(cloud-ai): run AWS Rekognition boto3 calls off the event loop

Conclusion: failure

View job details

##[group]Run actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3
 with:
   script: const fs = require('fs');
const pull = await github.rest.pulls.get({
  owner: context.repo.owner,
  repo: context.repo.repo,
  pull_number: Number(process.env.PR)
});
let verdict = {
  conclusion: 'failure',
  reason: 'verifier_did_not_publish',
  details: {}
};
try {
  verdict = JSON.parse(fs.readFileSync(
    'enforcement-verdict.json', 'utf8'
  ));
} catch (error) {
  core.warning(error.message);
}
const conclusion = verdict.conclusion === 'success'
  ? 'success'
  : 'failure';
const summary = JSON.stringify(verdict);
await github.rest.checks.create({
  owner: context.repo.owner,
  repo: context.repo.repo,
  name: 'Agent completion enforcement',
  head_sha: pull.data.head.sha,
  status: 'completed',
  conclusion,
  output: {
    title: conclusion === 'success'
      ? 'Trusted evidence verified'
      : 'Trusted evidence blocked',
    summary: summary.slice(0, 60000)
  }
});
if (conclusion !== 'success') {
  core.setFailed(verdict.reason || 'trusted evidence blocked');
}
   github-***REDACTED_SECRET_ASSIGNMENT***
   debug: false
   user-agent: actions/github-script
   result-encoding: json
   retries: 0
   retry-exempt-status-codes: 400,401,403,404,422
 env:
   PR: 1205
 ##[endgroup]
 ##[error]missing_trusted_publication

GitHub Actions: Agent completion enforcement / Agent completion enforcement: perf(cloud-ai): run AWS Rekognition boto3 calls off the event loop

Conclusion: failure

View job details

##[group]Run actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3
 with:
   script: const fs = require('fs');
const pull = await github.rest.pulls.get({
  owner: context.repo.owner,
  repo: context.repo.repo,
  pull_number: Number(process.env.PR)
});
let verdict = {
  conclusion: 'failure',
  reason: 'verifier_did_not_publish',
  details: {}
};
try {
  verdict = JSON.parse(fs.readFileSync(
    'enforcement-verdict.json', 'utf8'
  ));
} catch (error) {
  core.warning(error.message);
}
const conclusion = verdict.conclusion === 'success'
  ? 'success'
  : 'failure';
const summary = JSON.stringify(verdict);
await github.rest.checks.create({
  owner: context.repo.owner,
  repo: context.repo.repo,
  name: 'Agent completion enforcement',
  head_sha: pull.data.head.sha,
  status: 'completed',
  conclusion,
  output: {
    title: conclusion === 'success'
      ? 'Trusted evidence verified'
      : 'Trusted evidence blocked',
    summary: summary.slice(0, 60000)
  }
});
if (conclusion !== 'success') {
  core.setFailed(verdict.reason || 'trusted evidence blocked');
}
   github-***REDACTED_SECRET_ASSIGNMENT***
   debug: false
   user-agent: actions/github-script
   result-encoding: json
   retries: 0
   retry-exempt-status-codes: 400,401,403,404,422
 env:
   PR: 1205
 ##[endgroup]
 ##[error]missing_trusted_publication

GitHub Actions: 🔍 Dependency Review / dependency-review: perf(cloud-ai): run AWS Rekognition boto3 calls off the event loop

Conclusion: failure

View job details

##[group]Run actions/dependency-review-action@v5
 with:
   fail-on-severity: moderate
   allow-ghsas: GHSA-w5hq-g745-h8pq
   allow-licenses: MIT, Apache-2.0, BSD-2-Clause, BSD-3-Clause, ISC, Python-2.0, BlueOak-1.0.0, MPL-2.0, CC0-1.0, 0BSD
   allow-dependencies-licenses: pkg:npm/caniuse-lite, pkg:npm/@sentry/nextjs, pkg:npm/@sentry/cli, pkg:npm/@sentry/cli-darwin, pkg:npm/@sentry/cli-linux-arm, pkg:npm/@sentry/cli-linux-arm64, pkg:npm/@sentry/cli-linux-i686, pkg:npm/@sentry/cli-linux-x64, pkg:npm/@sentry/cli-win32-arm64, pkg:npm/@sentry/cli-win32-i686, pkg:npm/@sentry/cli-win32-x64, pkg:npm/@sentry/bundler-plugin-core, pkg:npm/@sentry/babel-plugin-component-annotate
   comment-summary-in-pr: always
   repo-***REDACTED_SECRET_ASSIGNMENT***
 ##[endgroup]
 ##[error]Dependency review is not supported on this repository. Please ensure that Dependency graph is enabled, see https://github.com/groupthinking/EventRelay/settings/security_analysis

GitHub Actions: 🔍 Dependency Review / 0_dependency-review.txt: perf(cloud-ai): run AWS Rekognition boto3 calls off the event loop

Conclusion: failure

View job details

##[group]Run actions/dependency-review-action@v5
 with:
   fail-on-severity: moderate
   allow-ghsas: GHSA-w5hq-g745-h8pq
   allow-licenses: MIT, Apache-2.0, BSD-2-Clause, BSD-3-Clause, ISC, Python-2.0, BlueOak-1.0.0, MPL-2.0, CC0-1.0, 0BSD
   allow-dependencies-licenses: pkg:npm/caniuse-lite, pkg:npm/@sentry/nextjs, pkg:npm/@sentry/cli, pkg:npm/@sentry/cli-darwin, pkg:npm/@sentry/cli-linux-arm, pkg:npm/@sentry/cli-linux-arm64, pkg:npm/@sentry/cli-linux-i686, pkg:npm/@sentry/cli-linux-x64, pkg:npm/@sentry/cli-win32-arm64, pkg:npm/@sentry/cli-win32-i686, pkg:npm/@sentry/cli-win32-x64, pkg:npm/@sentry/bundler-plugin-core, pkg:npm/@sentry/babel-plugin-component-annotate
   comment-summary-in-pr: always
   repo-***REDACTED_SECRET_ASSIGNMENT***
 ##[endgroup]
 ##[error]Dependency review is not supported on this repository. Please ensure that Dependency graph is enabled, see https://github.com/groupthinking/EventRelay/settings/security_analysis

GitHub Actions: Secret Scan / 0_gitleaks (working tree).txt: perf(cloud-ai): run AWS Rekognition boto3 calls off the event loop

Conclusion: failure

View job details

##[group]Run gitleaks detect --no-git --config .gitleaks.toml --redact --verbose --exit-code 1
 �[36;1mgitleaks detect --no-git --config .gitleaks.toml --redact --verbose --exit-code 1�[0m
 shell: /usr/bin/bash -e {0}
 ##[endgroup]
     ○
     │╲
     │ ○
     ○ ░
     ░    gitleaks
 Finding:     ...gz", hash = "sha256:�[1;3;mREDACTED�[0m, size = 401824, upl...
 ***REDACTED_SECRET_ASSIGNMENT***
 RuleID:      square-access-token
 Entropy:     3.884400
 File:        uv.lock
 Line:        5129
 Fingerprint: uv.lock:square-access-***REDACTED_SECRET_ASSIGNMENT***
 �[90m11:06PM�[0m �[32mINF�[0m scan completed in 5.87s
 �[90m11:06PM�[0m �[31mWRN�[0m leaks found: 1
 ##[error]Process completed with exit code 1.

GitHub Actions: Secret Scan / gitleaks (working tree): perf(cloud-ai): run AWS Rekognition boto3 calls off the event loop

Conclusion: failure

View job details

##[group]Run gitleaks detect --no-git --config .gitleaks.toml --redact --verbose --exit-code 1
 �[36;1mgitleaks detect --no-git --config .gitleaks.toml --redact --verbose --exit-code 1�[0m
 shell: /usr/bin/bash -e {0}
 ##[endgroup]
     ○
     │╲
     │ ○
     ○ ░
     ░    gitleaks
 Finding:     ...gz", hash = "sha256:�[1;3;mREDACTED�[0m, size = 401824, upl...
 ***REDACTED_SECRET_ASSIGNMENT***
 RuleID:      square-access-token
 Entropy:     3.884400
 File:        uv.lock
 Line:        5129
 Fingerprint: uv.lock:square-access-***REDACTED_SECRET_ASSIGNMENT***
 �[90m11:06PM�[0m �[32mINF�[0m scan completed in 5.87s
 �[90m11:06PM�[0m �[31mWRN�[0m leaks found: 1
 ##[error]Process completed with exit code 1.

GitHub Actions: Coverage / Generate and Upload Coverage: perf(cloud-ai): run AWS Rekognition boto3 calls off the event loop

Conclusion: failure

View job details

ion PASSED [ 96%]
 tests/unit/test_video_utils.py::TestParseDurationToSeconds::test_minutes_and_seconds PASSED [ 96%]
 tests/unit/test_video_utils.py::TestParseDurationToSeconds::test_seconds_only PASSED [ 96%]
 tests/unit/test_video_utils.py::TestParseDurationToSeconds::test_hours_and_minutes PASSED [ 96%]
 tests/unit/test_video_utils.py::TestParseDurationToSeconds::test_hours_only PASSED [ 96%]
 tests/unit/test_video_utils.py::TestParseDurationToSeconds::test_minutes_only PASSED [ 96%]
 tests/unit/test_video_utils.py::TestParseDurationToSeconds::test_invalid_returns_zero PASSED [ 96%]
 tests/unit/test_video_utils.py::TestParseDurationToSeconds::test_zero_duration PASSED [ 96%]
 tests/unit/test_video_utils.py::TestParseDurationToSeconds::test_large_hours PASSED [ 96%]
 tests/unit/test_video_utils.py::TestFormatDuration::test_full_duration PASSED [ 96%]
 tests/unit/test_video_utils.py::TestFormatDuration::test_minutes_and_seconds PASSED [ 96%]
 tests/unit/test_video_utils.py::TestFormatDuration::test_seconds_only PASSED [ 96%]
 tests/unit/test_video_utils.py::TestFormatDuration::test_hours_and_minutes PASSED [ 96%]
 tests/unit/test_video_utils.py::TestFormatDuration::test_hours_only PASSED [ 96%]
 tests/unit/test_video_utils.py::TestFormatDuration::test_minutes_only PASSED [ 96%]
 tests/unit/test_video_utils.py::TestFormatDuration::test_zero_seconds PASSED [ 96%]
 tests/unit/test_video_utils.py::TestFormatDuration::test_invalid_returns_original PASSED [ 96%]
 tests/unit/test_videopack.py::TestVPVersion::test_v0_value PASSED        [ 96%]
 tests/unit/test_videopack.py::TestTranscriptSegment::test_basic_creation PASSED [ 96%]
 tests/unit/test_videopack.py::TestTranscriptSegment::test_idx_stored PASSED [ 96%]
 tests/unit/test_videopack.py::TestTranscript::test_full_text_stored PASSED [ 97%]
 tests/unit/test_videopack.py::TestTranscript::test_segments_default_empty PASSED [ 97%]
 tests/unit/test_videopack.py::TestTranscript::test_language_optional PASSED [ 97%]
 test...

GitHub Actions: Coverage / 0_Generate and Upload Coverage.txt: perf(cloud-ai): run AWS Rekognition boto3 calls off the event loop

Conclusion: failure

View job details

ion PASSED [ 96%]
 tests/unit/test_video_utils.py::TestParseDurationToSeconds::test_minutes_and_seconds PASSED [ 96%]
 tests/unit/test_video_utils.py::TestParseDurationToSeconds::test_seconds_only PASSED [ 96%]
 tests/unit/test_video_utils.py::TestParseDurationToSeconds::test_hours_and_minutes PASSED [ 96%]
 tests/unit/test_video_utils.py::TestParseDurationToSeconds::test_hours_only PASSED [ 96%]
 tests/unit/test_video_utils.py::TestParseDurationToSeconds::test_minutes_only PASSED [ 96%]
 tests/unit/test_video_utils.py::TestParseDurationToSeconds::test_invalid_returns_zero PASSED [ 96%]
 tests/unit/test_video_utils.py::TestParseDurationToSeconds::test_zero_duration PASSED [ 96%]
 tests/unit/test_video_utils.py::TestParseDurationToSeconds::test_large_hours PASSED [ 96%]
 tests/unit/test_video_utils.py::TestFormatDuration::test_full_duration PASSED [ 96%]
 tests/unit/test_video_utils.py::TestFormatDuration::test_minutes_and_seconds PASSED [ 96%]
 tests/unit/test_video_utils.py::TestFormatDuration::test_seconds_only PASSED [ 96%]
 tests/unit/test_video_utils.py::TestFormatDuration::test_hours_and_minutes PASSED [ 96%]
 tests/unit/test_video_utils.py::TestFormatDuration::test_hours_only PASSED [ 96%]
 tests/unit/test_video_utils.py::TestFormatDuration::test_minutes_only PASSED [ 96%]
 tests/unit/test_video_utils.py::TestFormatDuration::test_zero_seconds PASSED [ 96%]
 tests/unit/test_video_utils.py::TestFormatDuration::test_invalid_returns_original PASSED [ 96%]
 tests/unit/test_videopack.py::TestVPVersion::test_v0_value PASSED        [ 96%]
 tests/unit/test_videopack.py::TestTranscriptSegment::test_basic_creation PASSED [ 96%]
 tests/unit/test_videopack.py::TestTranscriptSegment::test_idx_stored PASSED [ 96%]
 tests/unit/test_videopack.py::TestTranscript::test_full_text_stored PASSED [ 97%]
 tests/unit/test_videopack.py::TestTranscript::test_segments_default_empty PASSED [ 97%]
 tests/unit/test_videopack.py::TestTranscript::test_language_optional PASSED [ 97%]
 test...

GitHub Actions: CI / guards: perf(cloud-ai): run AWS Rekognition boto3 calls off the event loop

Conclusion: failure

View job details

##[group]Run # Opening/closing conflict sentinels always carry a label after the
 �[36;1m# Opening/closing conflict sentinels always carry a label after the�[0m
 �[36;1m# space, so this never matches decorative "=======" underlines.�[0m
 �[36;1mif git grep -nE '^(<<<<<<<|>>>>>>>) ' -- . ':(exclude).github/workflows/ci.yml'; then�[0m
 �[36;1m  echo "::error::Committed merge-conflict markers found (see matches above)."�[0m

GitHub Actions: CI / guards: perf(cloud-ai): run AWS Rekognition boto3 calls off the event loop

Conclusion: failure

View job details

##[group]Run # VS Code forks (Antigravity, Cursor, Windsurf) write their own
 �[36;1m# VS Code forks (Antigravity, Cursor, Windsurf) write their own�[0m
 �[36;1m# extension IDs into workspace settings; those IDs resolve to�[0m
 �[36;1m# nothing in stock VS Code and fail silently. Mirrors the�[0m
 �[36;1m# vscode-ide-self-reference pre-commit hook, which not every�[0m
 �[36;1m# committer has installed.�[0m
 �[36;1mif git grep -nE 'google\.antigravity|anysphere\.|codeium\.windsurf' -- .vscode/; then�[0m
 �[36;1m  echo "::error::IDE self-identifier found in shared .vscode/ config (see matches above)."�[0m

GitHub Actions: CI / 3_lint-python.txt: perf(cloud-ai): run AWS Rekognition boto3 calls off the event loop

Conclusion: failure

View job details

##[group]Run ruff check src/youtube_extension/backend/ src/youtube_extension/main.py --ignore E402,F811,F401,F821,B904,B020,E701,E722
 �[36;1mruff check src/youtube_extension/backend/ src/youtube_extension/main.py --ignore E402,F811,F401,F821,B904,B020,E701,E722�[0m
 shell: /usr/bin/bash -e {0}
 env:
   pythonLocation: /opt/hostedtoolcache/Python/3.12.13/x64
   PKG_CONFIG_PATH: /opt/hostedtoolcache/Python/3.12.13/x64/lib/pkgconfig
   Python_ROOT_DIR: /opt/hostedtoolcache/Python/3.12.13/x64
   Python2_ROOT_DIR: /opt/hostedtoolcache/Python/3.12.13/x64
   Python3_ROOT_DIR: /opt/hostedtoolcache/Python/3.12.13/x64
   LD_LIBRARY_PATH: /opt/hostedtoolcache/Python/3.12.13/x64/lib
 ##[endgroup]
 UP035 `typing.Dict` is deprecated, use `dict` instead
  --> src/youtube_extension/backend/deploy/__init__.py:3:1
   |
 1 | import importlib
 2 | from collections.abc import Awaitable
 3 | from typing import Any, Callable, Dict
   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 4 |
 5 | AdapterFunc = Callable[[str, dict[str, Any], dict[str, Any]], Awaitable[dict[str, Any]]]
   |
 B025 try-except block with duplicate exception `Exception`
    --> src/youtube_extension/backend/services/data_service.py:340:16
     |
 338 |             return None
 339 |
 340 |         except Exception as e:
     |                ^^^^^^^^^
 341 |             logger.error(f"Error reading video detail: {e}")
 342 |             return None
     |
 Found 2 errors.
 ##[error]Process completed with exit code 1.

GitHub Actions: CI / 2_guards.txt: perf(cloud-ai): run AWS Rekognition boto3 calls off the event loop

Conclusion: failure

View job details

##[group]Run # Opening/closing conflict sentinels always carry a label after the
 �[36;1m# Opening/closing conflict sentinels always carry a label after the�[0m
 �[36;1m# space, so this never matches decorative "=======" underlines.�[0m
 �[36;1mif git grep -nE '^(<<<<<<<|>>>>>>>) ' -- . ':(exclude).github/workflows/ci.yml'; then�[0m
 �[36;1m  echo "::error::Committed merge-conflict markers found (see matches above)."�[0m

GitHub Actions: CI / lint-python: perf(cloud-ai): run AWS Rekognition boto3 calls off the event loop

Conclusion: failure

View job details

##[group]Run ruff check src/youtube_extension/backend/ src/youtube_extension/main.py --ignore E402,F811,F401,F821,B904,B020,E701,E722
 �[36;1mruff check src/youtube_extension/backend/ src/youtube_extension/main.py --ignore E402,F811,F401,F821,B904,B020,E701,E722�[0m
 shell: /usr/bin/bash -e {0}
 env:
   pythonLocation: /opt/hostedtoolcache/Python/3.12.13/x64
   PKG_CONFIG_PATH: /opt/hostedtoolcache/Python/3.12.13/x64/lib/pkgconfig
   Python_ROOT_DIR: /opt/hostedtoolcache/Python/3.12.13/x64
   Python2_ROOT_DIR: /opt/hostedtoolcache/Python/3.12.13/x64
   Python3_ROOT_DIR: /opt/hostedtoolcache/Python/3.12.13/x64
   LD_LIBRARY_PATH: /opt/hostedtoolcache/Python/3.12.13/x64/lib
 ##[endgroup]
 UP035 `typing.Dict` is deprecated, use `dict` instead
  --> src/youtube_extension/backend/deploy/__init__.py:3:1
   |
 1 | import importlib
 2 | from collections.abc import Awaitable
 3 | from typing import Any, Callable, Dict
   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 4 |
 5 | AdapterFunc = Callable[[str, dict[str, Any], dict[str, Any]], Awaitable[dict[str, Any]]]
   |
 B025 try-except block with duplicate exception `Exception`
    --> src/youtube_extension/backend/services/data_service.py:340:16
     |
 338 |             return None
 339 |
 340 |         except Exception as e:
     |                ^^^^^^^^^
 341 |             logger.error(f"Error reading video detail: {e}")
 342 |             return None
     |
 Found 2 errors.
 ##[error]Process completed with exit code 1.

GitHub Actions: CI / test: perf(cloud-ai): run AWS Rekognition boto3 calls off the event loop

Conclusion: failure

View job details

s_not_propagate
 -------------------------------- live log call ---------------------------------
  [ WARNING] youtube_extension.backend.services.video_processing_service: Processor cleanup warning: close error
 PASSED                                                                   [ 95%]
 tests/unit/test_video_processor_factory.py::TestAutoMode::test_auto_defaults_to_enhanced_when_no_env
 -------------------------------- live log call ---------------------------------
  [    INFO] youtube_extension.backend.video_processor_factory: Creating video processor: enhanced
  [    INFO] youtube_extension.backend.video_processor_factory: ✅ Using EnhancedVideoProcessor (Gemini + YouTube API)
 PASSED                                                                   [ 95%]
 tests/unit/test_video_processor_factory.py::TestAutoMode::test_auto_env_var_enhanced
 -------------------------------- live log call ---------------------------------
  [    INFO] youtube_extension.backend.video_processor_factory: Creating video processor: enhanced
  [    INFO] youtube_extension.backend.video_processor_factory: ✅ Using EnhancedVideoProcessor (Gemini + YouTube API)
 PASSED                                                                   [ 95%]
 tests/unit/test_video_processor_factory.py::TestAutoMode::test_auto_env_var_real
 -------------------------------- live log call ---------------------------------
  [    INFO] youtube_extension.backend.video_processor_factory: Creating video processor: real
  [    INFO] youtube_extension.backend.video_processor_factory: ✅ Using RealVideoProcessor (MCP ecosystem)
 PASSED                                                                   [ 95%]
 tests/unit/test_video_processor_factory.py::TestEnhancedProcessorType::test_returns_enhanced_processor
 -------------------------------- live log call ---------------------------------
  [    INFO] youtube_extension.backend.video_processor_factory: Creating video processor: enhanced
  [    INFO] youtube_exte...

GitHub Actions: CI / 1_test.txt: perf(cloud-ai): run AWS Rekognition boto3 calls off the event loop

Conclusion: failure

View job details

s_not_propagate
 -------------------------------- live log call ---------------------------------
  [ WARNING] youtube_extension.backend.services.video_processing_service: Processor cleanup warning: close error
 PASSED                                                                   [ 95%]
 tests/unit/test_video_processor_factory.py::TestAutoMode::test_auto_defaults_to_enhanced_when_no_env
 -------------------------------- live log call ---------------------------------
  [    INFO] youtube_extension.backend.video_processor_factory: Creating video processor: enhanced
  [    INFO] youtube_extension.backend.video_processor_factory: ✅ Using EnhancedVideoProcessor (Gemini + YouTube API)
 PASSED                                                                   [ 95%]
 tests/unit/test_video_processor_factory.py::TestAutoMode::test_auto_env_var_enhanced
 -------------------------------- live log call ---------------------------------
  [    INFO] youtube_extension.backend.video_processor_factory: Creating video processor: enhanced
  [    INFO] youtube_extension.backend.video_processor_factory: ✅ Using EnhancedVideoProcessor (Gemini + YouTube API)
 PASSED                                                                   [ 95%]
 tests/unit/test_video_processor_factory.py::TestAutoMode::test_auto_env_var_real
 -------------------------------- live log call ---------------------------------
  [    INFO] youtube_extension.backend.video_processor_factory: Creating video processor: real
  [    INFO] youtube_extension.backend.video_processor_factory: ✅ Using RealVideoProcessor (MCP ecosystem)
 PASSED                                                                   [ 95%]
 tests/unit/test_video_processor_factory.py::TestEnhancedProcessorType::test_returns_enhanced_processor
 -------------------------------- live log call ---------------------------------
  [    INFO] youtube_extension.backend.video_processor_factory: Creating video processor: enhanced
  [    INFO] youtube_exte...

Commit Status: Vercel: Vercel

Conclusion: failure

Canceled from the Vercel Dashboard
🧰 Additional context used
📓 Path-based instructions (10)
**/*.{py,js,jsx,ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{py,js,jsx,ts,tsx}: Use Python 3.9+ and Node 18+ for development
Never hardcode API keys, database URLs, or secrets in code
Make minimal, surgical changes and avoid deleting working code unless fixing security issues

Files:

  • src/youtube_extension/integrations/cloud_ai/providers/aws_rekognition.py
**/*.py

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.py: Always use type hints for Python functions
Use Black formatter with 88 character line length for Python code
Follow PEP 8 conventions for Python code
Use descriptive variable names and add docstrings to all public functions in Python
Group imports in Python: standard library, third-party, local
Use SQLAlchemy ORM and never write raw SQL queries
Use environment variables via os.getenv() or pydantic-settings to access configuration
Wrap database operations in try-except blocks and use context managers or FastAPI dependencies for connection cleanup
Implement comprehensive error handling with proper logging in all functions
Version APIs using /api/v1/ prefix for stability
Use JSON-RPC 2.0 protocol for all MCP communication
Follow the single-flow workflow: YouTube link → context extraction → agent dispatch → outputs
Use context managers for resource management in Python code
Implement comprehensive input validation and sanitize outputs for security
Use parameterized queries and SQLAlchemy ORM to prevent SQL injection
Define API request/response models using Pydantic for FastAPI endpoints
Store the single unified workflow as the only workflow; never introduce alternate flows or manual triggers
Use SQLAlchemy with connection pooling for database connections and manage sessions with context managers
Provide sensible defaults for non-sensitive configuration in Python settings
Maintain backward compatibility and do not break existing API endpoints
Include comprehensive logging for debugging in MCP implementations

Files:

  • src/youtube_extension/integrations/cloud_ai/providers/aws_rekognition.py

⚙️ CodeRabbit configuration file

Python backend code. Check for type hints, proper exception handling, async context manager usage, and potential blocking calls in async functions. Flag any bare except clauses or missing timeout parameters on network calls. CRITICAL: Flag any file that contains placeholder/stub implementations — especially in unified_ai_sdk. Any class or function that says "TODO: Replace with production implementation" or returns mock/fake data must be flagged as a blocking issue. Flag any code generation output that reaches users without AST validation or syntax checking.

Files:

  • src/youtube_extension/integrations/cloud_ai/providers/aws_rekognition.py
**/*.{py,js,ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Maintain >80% code coverage for new features

Files:

  • src/youtube_extension/integrations/cloud_ai/providers/aws_rekognition.py
**/*.{py,ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{py,ts,tsx}: Keep frontend and backend data models synchronized using matching Pydantic (backend) and TypeScript (frontend) interfaces
Use type-safe interfaces for backend-frontend data exchange

**/*.{py,ts,tsx}: Production code must use real behavior only: no mock delays, fake data, or simulated responses.
Maintain strict type safety: mypy strict mode for Python and TypeScript strict mode for the frontend.
Name events using the <domain>.<entity>.<action> format.

Files:

  • src/youtube_extension/integrations/cloud_ai/providers/aws_rekognition.py
**/*

📄 CodeRabbit inference engine (Custom checks)

**/*: Strictly verify that GitHub Copilot has explicitly reviewed and approved the pull request; human approvals alone must not satisfy this check.
Before allowing a merge, require the copilot-rabbit label and AI-generated unit tests committed alongside the code changes; fail the check if either is missing.

For Vercel-specific work, include https://vercel.com/docs/llms-full.txt in the AI assistant context set.

Files:

  • src/youtube_extension/integrations/cloud_ai/providers/aws_rekognition.py
**/*.{py,pyw}

📄 CodeRabbit inference engine (AGENTS.md)

Write Python code to remain compatible with Linux and Windows where possible, including correct handling of asyncio event loops.

Files:

  • src/youtube_extension/integrations/cloud_ai/providers/aws_rekognition.py
src/**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

src/**/*.py: Format Python code with Black using an 88-character line length.
Sort Python imports with isort using the Black profile.
Run Ruff with E, W, F, I, B, C4, and UP rules; E501 is ignored.
Use mypy strict mode; untyped function definitions are disallowed.
Target Python 3.9 or newer.
Validate backend inputs with Pydantic and sanitize subprocess arguments.
Use Anthropic SDK features thinking={"type": "adaptive"} and output_config={"effort": "..."} with anthropic>=0.105.0; do not add TypeError fallbacks for these parameters.

Use the service container dependency-injection pattern in backend/containers/.

Files:

  • src/youtube_extension/integrations/cloud_ai/providers/aws_rekognition.py
**/*.{py,ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Do not commit secrets; store keys and credentials in gitignored .env files.

Files:

  • src/youtube_extension/integrations/cloud_ai/providers/aws_rekognition.py
**/*.{py,pyi}

📄 CodeRabbit inference engine (GEMINI.md)

**/*.{py,pyi}: Use Black formatting with an 88-character line length for Python code.
Use Ruff with rules E, W, F, I, B, C4, and UP for Python linting.
Use strict mypy type checking; do not define untyped functions.
Target Python 3.9 or newer.
Validate Python inputs with Pydantic.
Sanitize subprocess arguments before execution.
Do not add mock delays or fake data to production Python code; production runs in REAL_MODE_ONLY.
Keep secrets out of Python source code; load keys and credentials from environment variables instead.
Use absolute imports that resolve with PYTHONPATH=src in the Python backend.

Files:

  • src/youtube_extension/integrations/cloud_ai/providers/aws_rekognition.py
**/*.{py,pyi,ts,tsx}

📄 CodeRabbit inference engine (GEMINI.md)

**/*.{py,pyi,ts,tsx}: Preserve the single workflow: YouTube link → transcript → events → agents → outputs; do not introduce alternative flows or manual triggers that bypass it.
Use event names following <domain>.<entity>.<action>, such as youtube.video.captured.
Make surgical, precise changes and do not delete working code without justification.

Files:

  • src/youtube_extension/integrations/cloud_ai/providers/aws_rekognition.py
🪛 ast-grep (0.45.0)
src/youtube_extension/integrations/cloud_ai/providers/aws_rekognition.py

[warning] 35-35: File path is request-/variable-derived; validate and normalize to prevent path traversal.
Context: open(path, 'rb')
Note: [CWE-22] Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').

(open-filename-from-request)

🔍 Remote MCP GitHub Copilot

Relevant review context

  • Issue #1204 enumerates 14 blocking Rekognition calls plus a synchronous local-file read. The PR wraps all listed calls with asyncio.to_thread and changes only the provider and its unit test.
  • AWSRekognition.cleanup() only sets client references to None; the integrator awaits provider cleanup. This is relevant when assessing cancellation of in-flight worker calls.
  • The new responsiveness tests cover label detection, one video-start path, one polling path, and local reads. The aggregate image test does not independently prove that the other three detection calls are off-loop; an unresolved review comment requests isolated coverage for all 14 call sites.
  • The PR’s current checks include failures for test, coverage generation, gitleaks, dependency review, and agent-completion enforcement; Python lint, CodeQL, Python security scanning, build, and guards are reported successful. The available check metadata does not identify failure causes.
  • Merged PR #1190 used a cancellation-aware wrapper because its shared client could be closed during cancellation. That differs from this provider’s cleanup implementation, which does not close a transport.
🔇 Additional comments (1)
src/youtube_extension/integrations/cloud_ai/providers/aws_rekognition.py (1)

185-210: 🎯 Functional Correctness

Verify isolated responsiveness coverage for every offloaded call branch.

The implementation has 14 independent boto3 call sites. The supplied test evidence covers an aggregate image path, one video-submission path, and one polling path. A different branch could regress to direct boto3 execution while those tests still pass. Add parameterized tests for each client method, or assert the worker thread identity for every stubbed branch.

As per coding guidelines, maintain more than 80% code coverage for new features. As per PR objectives, Issue #1204 requires responsiveness tests that fail against the original implementation.

Also applies to: 294-320, 346-364

Source: Coding guidelines

Copy link
Copy Markdown
Owner Author

@coderabbitai review
@copilot review

Please re-review exact head af04aedb59d5b46871c46ab945536faa58c97d91. This head adds per-operation off-loop coverage for all 14 Rekognition SDK sites and transfers the verified module-eviction-safe local-read test fix from closed duplicate #1207. PR remains draft; the three newly reported provider findings remain unresolved and out of this focused test repair.

@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor
🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

target='af04aedb59d5b46871c46ab945536faa58c97d91'
provider='src/youtube_extension/integrations/cloud_ai/providers/aws_rekognition.py'
test_file='tests/unit/test_aws_rekognition_provider.py'

echo '=== Exact commit verification ==='
git rev-parse HEAD
git cat-file -t "$target"
git show -s --format='commit=%H%nparents=%P%nsubject=%s' "$target"
git merge-base --is-ancestor "$target" HEAD && echo 'target_is_ancestor_of_HEAD=true' || echo 'target_is_ancestor_of_HEAD=false'

parent="$(git show -s --format=%P "$target" | awk '{print $1}')"
echo "=== Diff: ${parent}..${target} ==="
git diff --stat "$parent" "$target"
git diff --unified=45 "$parent" "$target" -- "$provider" "$test_file"

echo '=== Provider structure ==='
ast-grep outline "$provider" --items all --type function,class --view expanded

echo '=== All async thread dispatches and Rekognition call contexts ==='
rg -n -C 6 --type py \
  'await\s+asyncio\.to_thread|_rekognition_client\.(describe_collection|detect_labels|detect_faces|detect_text|detect_moderation_labels|start_[a-z_]+_detection|get_[a-z_]+_detection)|def\s+_read_file_bytes|async\s+def\s+_prepare_image_input' \
  "$provider"

echo '=== Test structure and all off-loop/local-read coverage contexts ==='
ast-grep outline "$test_file" --items all --type function,class --view expanded
rg -n -C 12 --type py \
  'to_thread|threading\.get_ident|_read_file_bytes|sys\.modules|importlib|monkeypatch|patch\.object|heartbeat|ticks|describe_collection|detect_labels|detect_faces|detect_text|detect_moderation_labels|start_[a-z_]+_detection|get_[a-z_]+_detection' \
  "$test_file"

echo '=== Count actual await asyncio.to_thread call sites ==='
ast-grep run --pattern 'await asyncio.to_thread($$$ARGS)' --lang python "$provider"

echo '=== Test file path discovery if expected path changed ==='
fd -HI -t f '.*aws.*rekognition.*\.py$' . | sort

Length of output: 15553


🏁 Script executed:

#!/bin/bash
set -euo pipefail

target='af04aedb59d5b46871c46ab945536faa58c97d91'
base='origin/main'
test_file='tests/unit/test_aws_rekognition_provider.py'
provider='src/youtube_extension/integrations/cloud_ai/providers/aws_rekognition.py'

echo '=== Target tree entries for Rekognition tests ==='
git ls-tree -r --name-only "$target" | rg -i '(^|/).*aws.*rekognition.*\.py$|rekognition.*test.*\.py$' || true

echo '=== Confirm target objects ==='
git cat-file -e "${target}:${test_file}"
git cat-file -e "${target}:${provider}"
echo 'target_contains_expected_test_and_provider=true'

echo '=== PR-range changed test methods and helpers ==='
git diff --unified=30 "${base}...${target}" -- "$test_file"

echo '=== Target test method names ==='
git show "${target}:${test_file}" | rg -n \
  '^\s*(async\s+def|def)\s+test_|^\s*(async\s+def|def)\s+_count_heartbeats|^\s*(async\s+def|def)\s+_slow|parametrize'

echo '=== Target test contexts: coverage helpers and isolated SDK operations ==='
git show "${target}:${test_file}" | rg -n -C 14 \
  '_count_heartbeats|def _slow|test_each_|test_every_|test_local_image|describe_collection|detect_labels|detect_faces|detect_text|detect_moderation_labels|start_label_detection|start_face_detection|start_text_detection|start_content_moderation|get_label_detection|get_face_detection|get_text_detection|get_content_moderation|method_globals|patch\.dict|__globals__'

echo '=== Exact local-read test from target ==='
git show "${target}:${test_file}" | sed -n '/async def test_local_image_read_runs_off_the_event_loop_thread/,/async def test_local_image_bytes_are_read_correctly/p'

echo '=== Exact provider dispatches from target ==='
git show "${target}:${provider}" | rg -n -C 3 \
  'await asyncio\.to_thread|_read_file_bytes|describe_collection|detect_(labels|faces|text|moderation_labels)|start_(label|face|text|content_moderation)_detection|get_(label|face|text|content_moderation)(_detection)?'

Length of output: 54701


@groupthinking I reviewed exact head af04aedb59d5b46871c46ab945536faa58c97d91.

I found no new blocking defect in this test repair.

The off-loop coverage now isolates all 14 Rekognition SDK paths:

  • 4 image detection calls.
  • 4 video-start calls.
  • 4 video-poll calls.
  • 2 describe_collection paths.

Each case uses one slow mocked operation. A direct synchronous regression in any one path makes that case report zero heartbeats.

The local-read test now patches _prepare_image_input.__globals__. This is the active namespace for _read_file_bytes resolution. It does not depend on a new module import. Therefore, it remains valid if another test evicts the provider module from sys.modules. patch.dict also restores the original global binding after the test.

The three unresolved provider findings remain unresolved. This commit does not change those production paths. No additional finding applies to this focused test repair.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@groupthinking

Copy link
Copy Markdown
Owner Author

@coderabbitai review

Head is now af04aedb5. Round 2's deterministic test passed locally (including a
full 7,731-test single-process tests/unit run) yet failed on CI with
_read_file_bytes was never called. Root cause: it patched a freshly imported
module object, which is not necessarily the namespace the running coroutine
resolves from, because sibling test modules rebind youtube_extension.* entries
in sys.modules. It now patches type(provider)._prepare_image_input.__globals__
via patch.dict.

Please challenge specifically:

  1. Is patching a function's __globals__ sound here, or does it have a failure
    mode I have not considered — e.g. if _prepare_image_input were ever moved to
    a mixin/base class in a different module, would type(provider)._prepare_image_input
    still resolve to the function whose globals contain _read_file_bytes?
  2. patch.dict(method_globals, {...}) restores the dict on exit. Does it restore
    correctly if the key did not previously exist (it does here, but is the test
    relying on an accident)?
  3. The 15-site revert produces exactly 15 targeted failures. Is a 1:1
    site-to-failure mapping actually sufficient evidence of non-vacuity, or could a
    single shared helper make several of those failures redundant?
  4. _wait_for_job_completion polls every 5s up to 600s. With the four
    get_*_detection calls now off-loop, is there any remaining path in that loop
    that still performs blocking work on the event loop thread?
  5. cleanup() only sets client references to None and closes no transport, so I
    deliberately did not add cancellation shielding here (unlike perf(cloud-tasks): move blocking gRPC calls off the event loop #1190, where
    close() tore down a live gRPC transport). Do you agree that distinction is
    correct, or is there a use-after-cleanup hazard I am missing?

@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor
🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

target='af04aedb5'
provider='src/youtube_extension/integrations/cloud_ai/providers/aws_rekognition.py'
test_file='tests/unit/test_aws_rekognition_provider.py'
integrator='src/youtube_extension/integrations/cloud_ai/integrator.py'

echo '=== Exact commit ==='
git rev-parse "$target"
git show -s --format='commit=%H%nparents=%P%nsubject=%s' "$target"
git merge-base --is-ancestor "$target" HEAD && echo 'target_is_ancestor_of_HEAD=true' || echo 'target_is_ancestor_of_HEAD=false'

echo '=== Changed paths at target parent ==='
parent="$(git show -s --format=%P "$target" | awk '{print $1}')"
git diff --name-status "$parent" "$target"

echo '=== Provider structure ==='
ast-grep outline "$provider" --items all --type class,function --view expanded

echo '=== Provider: class, cleanup, file-read, and polling paths ==='
rg -n -C 14 --type py \
  'class\s+AWSRekognition|def\s+_read_file_bytes|async\s+def\s+_prepare_image_input|async\s+def\s+_wait_for_job_completion|async\s+def\s+cleanup|await\s+asyncio\.to_thread|await\s+asyncio\.sleep|_rekognition_client\.' \
  "$provider"

echo '=== Focused test methods and namespace patch ==='
ast-grep outline "$test_file" --items all --type function,class --view expanded
rg -n -C 16 --type py \
  'method_globals|__globals__|patch\.dict|_read_file_bytes|type\(provider\)\._prepare_image_input|test_.*off.*loop|heartbeat|ticks|revert|parametrize' \
  "$test_file"

echo '=== Current exact target versions of critical regions ==='
git show "${target}:${provider}" | sed -n '1,430p'
git show "${target}:${test_file}" | sed -n '340,520p'

echo '=== Integrator cleanup lifecycle ==='
sed -n '35,125p' "$integrator"

Length of output: 15919


🏁 Script executed:

#!/bin/bash
set -euo pipefail

target='af04aedb59d5b46871c46ab945536faa58c97d91'
provider='src/youtube_extension/integrations/cloud_ai/providers/aws_rekognition.py'

echo '=== Rekognition-related Python files in exact target tree ==='
git ls-tree -r --name-only "$target" | rg -i '(^|/).*rekognition.*\.py$|(^|/).*aws.*\.py$' || true

test_file="$(
  git ls-tree -r --name-only "$target" |
  rg -i '(^|/).*rekognition.*test.*\.py$|(^|/).*test.*rekognition.*\.py$' |
  head -n 1
)"
if [ -z "$test_file" ]; then
  echo 'ERROR: no Rekognition test file found in target tree' >&2
  exit 1
fi
echo "test_file=$test_file"

echo '=== Test structure in exact target ==='
git show "${target}:${test_file}" > /tmp/rekognition-test.py
ast-grep outline /tmp/rekognition-test.py --items all --type function,class --view expanded
rg -n -C 16 --type py \
  'method_globals|__globals__|patch\.dict|_read_file_bytes|type\(provider\)\._prepare_image_input|heartbeat|ticks|parametrize|slow|describe_collection|detect_|start_|get_' \
  /tmp/rekognition-test.py

echo '=== Exact local-read and per-operation tests ==='
sed -n '330,560p' /tmp/rekognition-test.py

echo '=== Full polling method in exact target ==='
git show "${target}:${provider}" | sed -n '325,385p'

echo '=== Integrator cleanup context ==='
integrator_path="$(
  git ls-tree -r --name-only "$target" |
  rg '(^|/)integrator\.py$' |
  rg 'cloud_ai' |
  head -n 1
)"
echo "integrator_path=$integrator_path"
git show "${target}:${integrator_path}" | sed -n '35,130p'

Length of output: 61990


@groupthinking I reviewed exact head af04aedb59d5b46871c46ab945536faa58c97d91. I found no blocking defect in this test repair.

  1. __globals__ patching is sound for the current implementation.
    Line 402 resolves _read_file_bytes from the global dictionary of the function object that implements _prepare_image_input().

    If _prepare_image_input() moves to a normal base class or mixin, type(provider)._prepare_image_input follows the MRO and returns that inherited function. Its __globals__ then identifies the defining module. The patch remains correct.

    The test fails clearly if a refactor changes this shape. For example, an instance-level override, a descriptor wrapper, or a callable object without __globals__ causes an attribute error or leaves observed["thread_id"] unset. It does not silently pass. The test is correctly coupled to the required implementation property.

  2. patch.dict() restores both existing and absent keys.
    patch.dict(method_globals, {"_read_file_bytes": _recording_read}) restores the prior value when the key exists. It removes the inserted key when the key did not exist before the context.

    This test does not rely on accidental absent-key behavior. The direct access at Line 1180 deliberately requires _read_file_bytes to exist in the active function globals. That requirement is valid for the current module-global lookup.

  3. The 15 targeted failures are sufficient evidence that the tests are non-vacuous.
    Each boto3 test selects one provider route, installs one slow side effect on its expected SDK method, and requires a heartbeat. Reverting one asyncio.to_thread() site makes its matching case fail.

    A future shared dispatch helper could cause several tests to fail from one shared defect. That is expected. It does not make the individual tests redundant. The parameterized cases still verify the routing from each provider operation to its specific SDK method. The local-read case independently verifies the fifteenth off-loop site.

  4. No blocking I/O remains in _wait_for_job_completion().
    Each Rekognition poll operation runs through await asyncio.to_thread(...). The wait uses await asyncio.sleep(5). The remaining loop work is local branching, dictionary access, response inspection, and exception construction. None performs blocking I/O.

  5. The cancellation distinction from #1190 is correct.
    A cancelled await does not stop a worker thread that already started a boto3 request. However, this provider does not close the boto3 client or its transport during cleanup().

    Each asyncio.to_thread() call receives a bound client method. That bound method retains the client even after cleanup() sets self._rekognition_client = None. I found no use-after-cleanup path in this provider.

    asyncio.shield() would delay cancellation propagation. It would not stop or make an already-running boto3 request safer. Do not add shielding for the current cleanup behavior.

No changes requested.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

target='b5721eb4f'
provider='src/youtube_extension/integrations/cloud_ai/providers/aws_rekognition.py'
test_file='tests/unit/test_aws_rekognition_provider.py'

echo '=== Exact target ==='
git rev-parse "$target"
git show -s --format='commit=%H%nparents=%P%nsubject=%s' "$target"
git merge-base --is-ancestor "$target" HEAD && echo 'target_is_ancestor_of_HEAD=true' || echo 'target_is_ancestor_of_HEAD=false'

parent="$(git show -s --format=%P "$target" | awk '{print $1}')"
echo '=== Commit diff ==='
git diff --stat "$parent" "$target"
git diff --unified=50 "$parent" "$target" -- "$provider" "$test_file"

echo '=== Provider outline ==='
ast-grep outline "$provider" --items all --type class,function --view expanded

echo '=== Timeout, retry, initialization, polling, and cleanup contexts ==='
rg -n -C 14 --type py \
  'Config\(|connect_timeout|read_timeout|max_attempts|retry|os\.getenv|math\.isfinite|ConfigurationError|async\s+def\s+initialize|except\s+Exception|async\s+def\s+_wait_for_job_completion|asyncio\.sleep|asyncio\.to_thread|async\s+def\s+cleanup' \
  "$provider"

echo '=== Related tests and mutation coverage ==='
rg -n -C 12 --type py \
  'timeout|retry|ConfigurationError|connect_timeout|read_timeout|max_attempts|finite|nan|inf|mutation|off_the_loop|heartbeat' \
  "$test_file"

echo '=== Configuration error definitions and provider initialization callers ==='
rg -n -C 6 --type py \
  'class\s+ConfigurationError|ConfigurationError|await\s+.*\.initialize\s*\(|\.initialize\s*\(' \
  src

echo '=== GitHub issue `#1209` ==='
gh issue view 1209 --repo groupthinking/EventRelay --json number,title,state,body,url

Length of output: 45065


🏁 Script executed:

#!/bin/bash
set -euo pipefail

target='b5721eb4fc1327611a17664659051cab1fb7dd09'
provider='src/youtube_extension/integrations/cloud_ai/providers/aws_rekognition.py'

echo '=== Locate Rekognition tests in target tree ==='
git ls-tree -r --name-only "$target" | rg -i '(^|/).*rekognition.*\.py$|(^|/).*aws.*rekognition.*\.py$' || true

test_file="$(
  git ls-tree -r --name-only "$target" |
  rg -i '(^|/)(test_)?aws_rekognition(_provider)?\.py$|(^|/).*test.*rekognition.*\.py$' |
  head -n 1
)"
test -n "$test_file"
echo "test_file=$test_file"

echo '=== Timeout tests in exact target ==='
git show "${target}:${test_file}" | rg -n -C 14 \
  'TestClientTimeoutConfiguration|_initialize|connect_timeout|read_timeout|max_attempts|mode|ConfigurationError|math\.inf|finite|blank|environment|mutation'

echo '=== Exception definitions and initialization call contexts ==='
rg -n -C 8 --type py \
  'class\s+ConfigurationError|class\s+CloudAIError|await\s+.*\.initialize\s*\(|\.initialize\s*\(' \
  src

echo '=== Relevant exact target provider lines ==='
git show "${target}:${provider}" | sed -n '36,205p'

echo '=== GitHub issue `#1209` ==='
gh issue view 1209 --repo groupthinking/EventRelay --json number,title,state,body,url

Length of output: 69728


🤖 web_search executed:

Official botocore Config retries standard mode max_attempts semantics: does max_attempts include the initial request, and how do connect_timeout and read_timeout apply?

Length of output: 16925


🏁 Script executed:

#!/bin/bash
set -euo pipefail

target='b5721eb4fc1327611a17664659051cab1fb7dd09'
test_file='tests/unit/test_aws_rekognition_provider.py'

echo '=== Exact timeout tests ==='
git show "${target}:${test_file}" | sed -n '/class TestClientTimeoutConfiguration:/,$p'

echo '=== All PR changes to timeout tests ==='
base="$(git merge-base origin/main "$target")"
git diff --unified=40 "${base}...${target}" -- "$test_file"

echo '=== Boto dependency declarations ==='
fd -HI -t f 'pyproject.toml|requirements*.txt|Pipfile|poetry.lock|uv.lock|setup.py|setup.cfg' . -d 4 \
  -x sh -c 'echo "=== $1 ==="; rg -n -i -C 3 "boto3|botocore" "$1" || true' sh {}

echo '=== Existing environment configuration conventions ==='
rg -n -C 3 --type py \
  'total_max_attempts|max_attempts|_TIMEOUT|CONNECT_TIMEOUT|READ_TIMEOUT|math\.isfinite' \
  src tests 2>/dev/null || true

Length of output: 62213


@groupthinking I reviewed b5721eb4fc1327611a17664659051cab1fb7dd09.

The timeout finding is addressed. Both clients now receive a bounded botocore.config.Config. This prevents an SDK request from holding a shared executor worker indefinitely.

  1. Parsing before try is correct.
    This changes the exception type for invalid new timeout environment variables from generic CloudAIError to specific ConfigurationError. That is the correct fail-fast behavior. ConfigurationError is also a CloudAIError subclass, so callers that catch the base type continue to work.

    One edge case changes error priority. If boto3 is missing and a timeout variable is invalid, initialization now reports the invalid configuration first. That is also correct. The deployment is invalid before it can use boto3.

  2. standard retry mode is appropriate.
    It retries transient AWS failures inside one SDK operation. _wait_for_job_completion() only performs another poll after it receives a non-terminal response. It does not restart polling after a retry-exhausted SDK error. Therefore, the two retry mechanisms do not recursively multiply on a single failed poll.

    There is one semantic detail to document or adjust. In a Config object, max_attempts=3 means one initial request plus up to three retries, or four total requests. If your intended limit is three total requests, use total_max_attempts=3 instead. AWS recommends total_max_attempts because its meaning is unambiguous. The current behavior remains bounded, so I do not treat this as blocking.
    References: Boto3 retry configuration, botocore Config reference.

  3. read_timeout=60 is reasonable.
    It is a conventional bounded default for AWS control-plane calls. A normal get_*_detection response should complete far sooner. Operators can lower it when their availability objective requires faster worker release.

    The practical upper bound for one retryable read timeout is about four 60-second attempts plus retry backoff under the current max_attempts setting. The provider’s max_wait_time counter advances only after successful non-terminal responses and sleeps. It is not a strict wall-clock deadline. This behavior predates the timeout change. The new timeout prevents an infinite worker hold.

  4. The 18-failure mutation result proves the intended wiring is non-vacuous.
    It does not prove every configuration property. The current tests would not detect:

    • a changed but still finite default timeout;
    • removal or change of mode: "standard";
    • the max_attempts versus total_max_attempts semantic difference;
    • a lost provider value or precise message on ConfigurationError;
    • environment leakage into the default test, because patch.dict(os.environ, ...) does not clear unrelated pre-existing variables.

    These are test-strengthening opportunities. They do not invalidate the timeout fix. I recommend asserting the exact default values and retry dictionary, including mode, if you want the tests to define the operational policy.

  5. I accept the #1209 scope split.
    The local-path traversal is a real security issue. This PR does not change the authorization boundary or expand the path set that _prepare_image_input() can read. It moves the same open() operation to a worker thread. That can increase concurrent read throughput, but it does not create a new path-access capability.

    Issue #1209 has the required policy decisions and test cases. Keep it prioritized. It should remain separate from this focused executor-safety change.

No additional blocking defect found. I accept the timeout fix and the tracked scope split.

[resolve]


✏️ Learnings added
Learnt from: groupthinking
URL: https://github.com/groupthinking/EventRelay/pull/1205

Timestamp: 2026-08-01T23:38:38.695Z
Learning: In `src/youtube_extension/integrations/cloud_ai/providers/aws_rekognition.py`, unrestricted local `image_url` file reads predate PR `#1205`. PR `#1205` only moves the existing read to `asyncio.to_thread` and does not expand the set of readable paths. GitHub issue `#1209` tracks the required media-root, `Path.resolve()`, symlink-escape, typed-error, and local-path-policy work.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

You are interacting with an AI system.

@groupthinking

Copy link
Copy Markdown
Owner Author

@coderabbitai Both inline comments posted at b5721eb4f re-anchor round-3 findings rather than
reflecting the code at that commit. Evidence below.

aws_rekognition.py:200 — "Add botocore timeouts to the AWS clients"

The comment states "initialize() creates the Rekognition and S3 clients without
botocore.config.Config"
. That is not true at the commit being reviewed. Line 200 is inside
_test_connection, not initialize(). The actual client construction at b5721eb4f:

144:        timeout_kwargs = _timeout_config_kwargs()
147:            from botocore.config import Config as BotoConfig
159:            client_config = BotoConfig(**timeout_kwargs)
162:            self._rekognition_client = session.client(
163:                'rekognition', config=client_config
165:            self._s3_client = session.client('s3', config=client_config)

Reproduce with:

git show b5721eb4f:src/youtube_extension/integrations/cloud_ai/providers/aws_rekognition.py \
  | grep -n "session.client\|client_config\|BotoConfig"

This is exactly the fix you asked for, shipped in the commit under review. It carries
connect_timeout, read_timeout and a bounded standard-mode retry policy, env-overridable and
validated with math.isfinite so inf/nan/0/negatives raise rather than clamp. 22 tests
cover it; mutation testing (helpers ignoring the environment + config= removed from both
clients) yields exactly 18 targeted failures against a predicted 18.

aws_rekognition.py:112 — "Restrict local image_url values before reading"

Line 112 is _read_file_bytes. This is the same finding raised at af04aedb5:37, already
adjudicated: it is pre-existing on origin/main, which read the same value verbatim at line
373 —

git show origin/main:src/youtube_extension/integrations/cloud_ai/providers/aws_rekognition.py \
  | sed -n '373p'
# with open(image_url, 'rb') as image_file:

This PR moved that read onto a worker thread. The set of readable paths is byte-for-byte
identical before and after. Tracked with acceptance criteria as #1209, consistent with the
scope split you yourself required on #1152#1162.

Request

The blocking CHANGES_REQUESTED on this PR is at 9b3f5e484, two heads stale. Its substance
was addressed in af04aedb5 (test namespace fix) and b5721eb4f (timeouts). All hard gates are
green at the current head: guards, build, test = success, plus Canonical issue and evidence = success.

Please re-review b5721eb4f as it actually stands, or @coderabbitai resolve if you agree the
two comments above are stale re-anchors. If you still believe a timeout gap exists at
b5721eb4f, quote the specific line of client construction you think is unguarded and I will fix
it immediately.

@groupthinking
groupthinking marked this pull request as ready for review August 1, 2026 23:44
@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

repo='groupthinking/EventRelay'
commit='b5721eb4f'
provider='src/youtube_extension/integrations/cloud_ai/providers/aws_rekognition.py'

echo '=== Exact commit ==='
git rev-parse "$commit"
git show -s --format='commit=%H%nsubject=%s' "$commit"

echo '=== Client construction and timeout configuration at exact commit ==='
git show "${commit}:${provider}" | nl -ba | sed -n '100,220p'
git show "${commit}:${provider}" | rg -n -C 4 \
  'def _timeout_config_kwargs|connect_timeout|read_timeout|max_attempts|mode|BotoConfig|session\.client|config=client_config'

echo '=== Local read at exact commit ==='
git show "${commit}:${provider}" | rg -n -C 5 \
  'def _read_file_bytes|open\(image_url|_prepare_image_input'

echo '=== Equivalent local read on origin/main ==='
git show "origin/main:${provider}" | rg -n -C 5 \
  'open\(image_url|_prepare_image_input' || true

echo '=== Follow-up issue ==='
gh issue view 1209 --repo "$repo" --json number,state,title,url,body

echo '=== PR review threads and bot inline comments ==='
gh api "repos/${repo}/pulls/1205/comments?per_page=100" \
  --jq '.[] | select(.user.login | test("coderabbit"; "i")) | {id, path, line, original_line, commit_id, body, html_url}' || true

echo '=== PR review states ==='
gh pr view 1205 --repo "$repo" --json headRefOid,reviews \
  --jq '{headRefOid, reviews: [.reviews[] | {author: .author.login, state, commit: .commit.oid, body}]}'

Length of output: 2039


🏁 Script executed:

#!/bin/bash
set -u

repo='groupthinking/EventRelay'
commit='b5721eb4f'
provider='src/youtube_extension/integrations/cloud_ai/providers/aws_rekognition.py'

echo '=== Exact commit ==='
git rev-parse "$commit"
git show -s --format='commit=%H%nsubject=%s' "$commit"

echo '=== Client construction and timeout configuration at exact commit ==='
git show "${commit}:${provider}" | sed -n '100,220p'
git show "${commit}:${provider}" | rg -n -C 4 \
  'def _timeout_config_kwargs|connect_timeout|read_timeout|max_attempts|mode|BotoConfig|session\.client|config=client_config' || true

echo '=== Local read at exact commit ==='
git show "${commit}:${provider}" | rg -n -C 5 \
  'def _read_file_bytes|open\(image_url|_prepare_image_input' || true

echo '=== Equivalent local read on origin/main ==='
git show "origin/main:${provider}" | rg -n -C 5 \
  'open\(image_url|_prepare_image_input' || true

echo '=== Follow-up issue ==='
gh issue view 1209 --repo "$repo" --json number,state,title,url,body || true

echo '=== CodeRabbit inline review comments ==='
gh api "repos/${repo}/pulls/1205/comments?per_page=100" \
  --jq '.[] | select(.user.login | test("coderabbit"; "i")) | {id, path, line, original_line, commit_id, body, html_url}' || true

echo '=== PR head and review states ==='
gh pr view 1205 --repo "$repo" --json headRefOid,reviews \
  --jq '{headRefOid, reviews: [.reviews[] | {author: .author.login, state, commit: .commit.oid, body}]}' || true

Length of output: 64054


🏁 Script executed:

#!/bin/bash
set -euo pipefail

repo_owner='groupthinking'
repo_name='EventRelay'
pr_number='1205'
target_ids='3696898103 3696898106'

threads_json="$(
  gh api graphql \
    -F owner="$repo_owner" \
    -F name="$repo_name" \
    -F number="$pr_number" \
    -f query='
query($owner: String!, $name: String!, $number: Int!) {
  repository(owner: $owner, name: $name) {
    pullRequest(number: $number) {
      reviewThreads(first: 100) {
        nodes {
          id
          isResolved
          comments(first: 20) {
            nodes {
              databaseId
              author { login }
              path
              line
              body
            }
          }
        }
      }
    }
  }
}' 
)"

for comment_id in $target_ids; do
  thread_id="$(
    printf '%s' "$threads_json" |
      jq -r --argjson comment_id "$comment_id" '
        .data.repository.pullRequest.reviewThreads.nodes[]
        | select(any(.comments.nodes[]; .databaseId == $comment_id))
        | .id
      '
  )"

  if [ -z "$thread_id" ] || [ "$thread_id" = "null" ]; then
    echo "ERROR: no review thread found for comment ${comment_id}" >&2
    exit 1
  fi

  is_resolved="$(
    printf '%s' "$threads_json" |
      jq -r --arg thread_id "$thread_id" '
        .data.repository.pullRequest.reviewThreads.nodes[]
        | select(.id == $thread_id)
        | .isResolved
      '
  )"

  if [ "$is_resolved" = "true" ]; then
    echo "already_resolved comment=${comment_id} thread=${thread_id}"
    continue
  fi

  gh api graphql \
    -F threadId="$thread_id" \
    -f query='
mutation($threadId: ID!) {
  resolveReviewThread(input: {threadId: $threadId}) {
    thread { id isResolved }
  }
}' \
    --jq '.data.resolveReviewThread.thread | "resolved thread=\(.id) isResolved=\(.isResolved)"'
done

Length of output: 2038


@groupthinking Confirmed and resolved both stale inline threads.

  • The timeout comment was invalid at b5721eb4f. Both boto3 clients receive BotoConfig(**timeout_kwargs) through config=client_config.
  • The local-path traversal behavior predates this PR. Issue #1209 tracks the required security remediation.

I resolved only those two threads. I did not resolve the separate outside-diff finding.

🐇

You are interacting with an AI system.

Copy link
Copy Markdown
Owner Author

Remediation runbook — publish gate reached (awaiting human merge approval)

Ran the per-PR loop after the ready_for_review event. This PR is merge-ready on the merits; the review loop has fully converged and no code remediation remains.

Green:

Red — but non-blocking (mergeable_state = unstable, i.e. none are required checks) and none caused by this diff:

Check Nature Verdict
gitleaks (working tree) square-access-token match on uv.lock:5129 — a sha256: lockfile hash (entropy 3.88). uv.lock is not touched by this PR (only aws_rekognition.py + its test), so it reproduces on main. Pre-existing false positive; fix is a .gitleaks.toml allowlist entry in a separate maintenance PR.
Agent completion enforcement missing_trusted_publication — governance gate awaiting the trusted-publisher "Agent Lock" check. Policy gate, not code-fixable in this diff.
Vercel Canceled from the Vercel Dashboard. Human/dashboard cancellation, not a test failure.

Publish gate is human-by-default. No automerge label, main is protected, and this automated run has no standing consent to merge — so I am not merging. Staged command for a maintainer:

gh pr merge 1205 --repo groupthinking/EventRelay --squash

Generated by Claude Code

@groupthinking
groupthinking merged commit 823b469 into main Aug 1, 2026
46 of 52 checks passed
@groupthinking
groupthinking deleted the perf/rekognition-offloop branch August 1, 2026 23:50
@linear-code

linear-code Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

GRV-234

groupthinking added a commit that referenced this pull request Aug 2, 2026
…1233)

* perf: read local image bytes off the event loop in vision providers

Azure `_prepare_image_input` and Google `analyze_image` read local image
files with a synchronous `open().read()` inside `async def`, blocking the
event loop for the duration of the disk read. Every other coroutine on the
loop stalls until the read completes.

Both now delegate to a module-level `_read_file_bytes` helper via
`asyncio.to_thread`, matching the fix already merged for the AWS
Rekognition sibling in #1205. This completes that cross-provider work so
all three providers share one contract.

The URL branches are untouched: Azure returns None so the SDK fetches the
URL itself, and Google still sets `image.source.image_uri`.

Also drops three now-redundant function-local `import asyncio` statements
in azure_vision.py, made dead by the new module-level import. The lazy
Azure SDK imports beside them are left in place.

Regression tests assert off-loop execution by thread identity rather than
wall-clock timing, which is flaky under CI load. Both new tests fail
against the unpatched sources with "read on the event loop thread".

Refs #1232

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* test(vision): record read-thread, not open-thread, in off-loop guards

Copilot review on #1233 flagged that _ThreadRecordingOpen recorded the
thread that called open(), not the thread that performed handle.read().
A regression offloading only open() while reading bytes back on the event
loop would still pass, so the test did not prove #1232's required property.

Wrap the returned handle in _ThreadRecordingHandle and record the calling
thread on read() instead. Behaviour-preservation tests (URL branch, missing
file) are unchanged; all off-loop guards still pass, and the recorder now
fails on an open-offloaded/read-on-loop regression.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A6r6TyHudJkb6mn4HUNF5Y

* test(google): pin missing-file CloudAIError wrapper contract

CodeRabbit review on #1233 flagged that TestGoogleCloudImageReadOffEventLoop
had no missing-file test, while GoogleCloudAI.analyze_image catches
FileNotFoundError in its broad `except Exception` and re-raises CloudAIError
— unlike Azure's private _prepare_image_input, which propagates
FileNotFoundError.

Add a Google test asserting the CloudAIError wrapper so moving the read off
the event loop cannot silently change how a missing local image is reported.
Closes the coverage gap; documents that the two providers differ at the
tested surface (public analyze_image vs private helper).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A6r6TyHudJkb6mn4HUNF5Y

* test(google): assert FileNotFoundError survives in the exception chain

Strengthen the wrapper-contract guard added in e0961b2: asserting only
pytest.raises(CloudAIError) would still pass if the underlying cause were
swallowed or the message went generic. Also assert the original
FileNotFoundError is preserved on __context__ (the provider re-raises
without 'from e', so chaining is implicit) and that the path error text
reaches the caller.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

perf(cloud-ai): AWS Rekognition blocks the event loop on 14 synchronous boto3 calls

3 participants