Skip to content

perf: offload cache-directory scan off the event loop (#1231) - #1237

Merged
groupthinking merged 4 commits into
mainfrom
perf/status-cache-glob-offload-1231
Aug 5, 2026
Merged

perf: offload cache-directory scan off the event loop (#1231)#1237
groupthinking merged 4 commits into
mainfrom
perf/status-cache-glob-offload-1231

Conversation

@groupthinking

@groupthinking groupthinking commented Aug 2, 2026

Copy link
Copy Markdown
Owner

Canonical issue

Closes #1231

Outcome

get_processing_status() no longer stalls the asyncio event loop while counting cache entries.

The status endpoint (real_api_endpoints.py:324 awaits this method) previously ran two blocking syscall sequences directly on the loop thread:

cached_files = len(list(self.cache_dir.glob("*_processed.json"))) if self.cache_dir.exists() else 0
  1. .exists() → a stat() syscall
  2. .glob(...) wrapped in list() → an eager opendir/readdir walk of the entire cache directory

Because this is on a live HTTP path, every status request froze all concurrently-served requests for the duration of the directory walk. The stall grows linearly with the number of cached videos and is unbounded — the cache directory has no eviction policy in this code path.

After this change the walk runs on a worker thread via asyncio.to_thread, so the loop stays free to schedule other requests. The count is also accumulated lazily (sum(1 for _ in …)) instead of materializing the full listing through list(), since only the total is ever consumed.

Why this issue was not already closed

#1231 as literally written asks for two things, and both shipped in merged PR #1228:

#1231 asks for Status on main before this PR
_load_from_cache off the loop Done — asyncio.to_thread(self._read_cache_file, …)
_save_to_cache off the loop Done — asyncio.to_thread(self._write_cache_file, …)
Atomic cache writes Done — tempfile.mkstemp + os.replace + unlink-on-failure

Rather than closing #1231 as a duplicate, I scanned every blocking-I/O primitive in the file grouped by enclosing def. That surfaced a third call site #1228 missed — get_processing_status — which is the same defect class the issue names, on a live endpoint. Closing #1231 as a pure duplicate would have discarded a real, endpoint-reachable bug.

Scope

  • Included:
    • New _count_cached_files blocking @staticmethod on RealVideoProcessor, awaited through asyncio.to_thread from get_processing_status.
    • Two regression tests in tests/unit/test_real_processors.py asserting the scan is offloaded.
  • Explicitly excluded:
    • __init__'s mkdir (L62). A synchronous constructor is not on the event loop in the same way; changing it would alter the public construction contract for no measured win.
    • Cache eviction / size bounding. Real, but a separate concern from this issue.
    • Any change to _load_from_cache / _save_to_cache, which are already correct.

Risk

  • Risk level: low
  • Failure mode: asyncio.to_thread requires a running loop. get_processing_status is already async def and every caller awaits it, so a loop is guaranteed present. The helper is pure (takes cache_dir explicitly, returns an int, mutates nothing), so moving it to a worker thread introduces no shared-state hazard. Worst realistic case is the count being momentarily stale relative to a concurrent write — which was equally true of the inline version and is not load-bearing, since the value is display-only in the status payload.
  • Rollback: Revert the single commit. The change is two hunks in one source file plus additive tests; there is no schema, config, or API-shape change to unwind.

Two deliberate design choices reduce risk further:

  • exists() and glob() stay in one hop. Two separate to_thread calls would reintroduce a stat/scan race — the same rationale already documented for _read_cache_file in this file.
  • The tests do not name asyncio.to_thread, so they remain honest if the offload mechanism is later changed.

Verification

All results below are on head e18af3c888331a3699233430a9866312b55f3d83.

RED → GREEN

Result
Before fix 2 failed, 3 passed (11.24s — the responsiveness test times out, exactly the predicted failure mode)
After fix 5 passed (0.35s)
All five related test files 326 passed, 0 failed
Same five files, fix reverted 2 failed / 324 passed — only the two new tests

Negative controls (mutate the source, confirm the tests catch it):

Mutation Result
NC-1 — restore the original inline glob 2 failed ✅
NC-2 — keep the helper but call it inline, no to_thread 2 failed ✅
NC-3 — helper always returns 0 3 failed ✅
Restored 5 passed, source byte-identical ✅

NC-2 is the load-bearing control: it proves the tests assert offloading rather than merely that a method was extracted. A suite that passed NC-2 would be rewarding the refactor and locking in nothing.

Why the existing tests did not catch this. TestGetProcessingStatus already had three tests, but they only assert the count is correct (cached_videos == 2) — nothing asserted where the scan runs. The blocking behaviour was entirely unlocked, which is how it survived #1228. The two new tests attack it from independent angles:

  1. Thread identity — record the thread the scan runs on; assert the loop thread is never among them. Deterministic, not timing-dependent.
  2. Loop responsiveness — the scan blocks on a threading.Event that only a coroutine on the loop can set. If the scan were inline, that coroutine could never be scheduled, so asyncio.wait_for would time out.

Both carry explicit anti-vacuity guards (assert scan_threads, "cache directory was never scanned"); without them loop_thread not in [] would pass trivially if the glob were never reached.

Lint — the exact CI gate (ruff check src/youtube_extension/backend/ src/youtube_extension/main.py --ignore …) reports 2 errors both with and without this change, both pre-existing and in unrelated files (deploy/__init__.py, data_service.py). Zero new violations introduced.

  • Focused tests — PYTHONPATH=src pytest tests/unit/test_real_processors.py → 5 passed for TestGetProcessingStatus
  • Required CI — all checks green on e18af3c8; mergeStateStatus: CLEAN
  • Review threads resolved

Production evidence

Not applicable — no production surface changes.

This PR touches one Python backend method and its unit tests. It ships no frontend change, so the Vercel preview for apps/web is byte-identical to main and carries no signal. There is no schema migration, no config change, no API-shape change, and no new dependency — asyncio is already imported in this module.

The runtime evidence that is meaningful for this change is the loop-responsiveness proof, which is captured deterministically in test_cache_scan_does_not_stall_the_event_loop rather than requiring a deployed environment: the test fails by timeout if and only if the scan runs inline on the loop. That is a stronger and more repeatable signal than a manual latency observation against a preview deployment.

Agent handoff

  • One canonical issue is linked
  • No competing PR implements the same issue
  • Acceptance criteria are satisfied
  • Required checks pass on the current head
  • Human decision is requested only for product, security, irreversible infrastructure, or production approval

get_processing_status is an async def, but counted cache entries inline:

    cached_files = len(list(self.cache_dir.glob("*_processed.json"))) if self.cache_dir.exists() else 0

That is two blocking syscall sequences on the event loop thread - a stat(),
then an eager opendir/readdir walk of the whole cache directory. It is awaited
by a live HTTP endpoint (real_api_endpoints.py:324), so every status request
stalled all concurrently-served requests for the duration of the walk, growing
with the number of cached videos.

Add a _count_cached_files static helper and await it via asyncio.to_thread,
matching the convention established for _read_cache_file/_write_cache_file in
this file. The existence check and the walk stay in one hop so the directory
cannot disappear between them, and the count is accumulated lazily instead of
materializing the full listing.

Tests assert the offload from two independent angles - thread identity, and
loop responsiveness while the scan is in flight - and neither names
asyncio.to_thread, so they stay honest if the mechanism changes. Both carry
explicit anti-vacuity guards.

Verified: RED 2 failed/3 passed -> GREEN 5/5, 326 passed across all five
related test files. Three negative controls all discriminate, including one
that keeps the helper but calls it inline, proving the tests assert offloading
rather than rewarding the extraction.

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

vercel Bot commented Aug 2, 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 Ready Ready Preview, v0 Aug 5, 2026 1:06am

@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

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

Next review available in: 44 minutes

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

How can I continue?

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

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

How do review limits work?

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

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

Please refer docs for additional details.

Review details
⚙️ Run configuration

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

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 0a156376-1eb5-4da9-bc89-453b5b5c7bc7

📥 Commits

Reviewing files that changed from the base of the PR and between e18af3c and 059aee4.

⛔ Files ignored due to path filters (1)
  • tests/unit/test_real_processors.py is excluded by !tests/**
📒 Files selected for processing (1)
  • src/youtube_extension/backend/services/real_video_processor.py
📝 Walkthrough

Summary by CodeRabbit

  • Performance Improvements
    • Improved processing-status responsiveness by moving cache scanning to a background worker.
    • Prevented cache checks from blocking other application activity.

Walkthrough

RealVideoProcessor now counts published cache files in a synchronous helper. get_processing_status runs this filesystem scan with asyncio.to_thread, preventing event-loop blocking.

Changes

Processing status cache scan

Layer / File(s) Summary
Threaded cache counting
src/youtube_extension/backend/services/real_video_processor.py
Adds _count_cached_files for missing or existing cache directories. get_processing_status delegates the blocking scan to asyncio.to_thread.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related issues

  • GRV-272 — Addresses the same pattern of moving cache-directory scans out of the event loop.

Possibly related PRs

Suggested labels: copilot-rabbit

Suggested reviewers: copilot

Poem

Cache files count in a thread,
While event loops move ahead.
Missing paths return zero light,
Status checks stay swift and bright.

🚥 Pre-merge checks | ✅ 5 | ❌ 2

❌ Failed checks (2 inconclusive)

Check name Status Explanation Resolution
Enforce Copilot Verification ❓ Inconclusive I am gathering repository and pull-request review evidence before deciding whether GitHub Copilot explicitly approved this pull request. Need verified GitHub pull-request review metadata showing a Copilot-authored approval.
Require Ai Unit Tests ❓ Inconclusive Investigation is still in progress. Need to verify the PR metadata for the copilot-rabbit label and inspect the committed test changes.
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes offloading the cache-directory scan from the event loop.
Description check ✅ Passed The description covers the core template sections with clear scope, risk, verification, production evidence, and handoff details.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch perf/status-cache-glob-offload-1231
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch perf/status-cache-glob-offload-1231

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.

@github-actions github-actions Bot added the python label Aug 2, 2026
@github-actions

github-actions Bot commented Aug 2, 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 059aee4.
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

@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown

Agent Completion Truth Gate: NOT_APPLICABLE

Evidence agrees.

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

Workflow evidence

groupthinking added a commit that referenced this pull request Aug 4, 2026
* perf: offload video-detail cache read, drop the stat probe

GET /api/v2/videos/{video_id} performed its whole cache lookup inline on
the event loop: Path.exists(), then open(), then a full json.load() of the
stored analysis. Only the two syscalls are bounded; the parse scales with
the payload the processor wrote, so a large analysis stalled every other
in-flight request on the worker.

Add a module-level _read_video_analysis_sync() helper and await it through
asyncio.to_thread(), mirroring _collect_processed_videos_sync() from #1288.
_get_cache_path() stays on the loop: it is pure string arithmetic.

The helper opens directly and treats FileNotFoundError as the miss instead
of probing with exists() first. That is one syscall rather than two, and it
closes the window in which the entry could be removed between the check and
the open - a race that previously surfaced as a 500 rather than the correct
404. Every other OSError still propagates, so a directory or an unreadable
entry keeps surfacing as a 500 instead of being reported as a missing video.

Deliberately does not reuse RealVideoProcessor._read_cache_file. That helper
applies a 24-hour TTL and returns None for anything older; this endpoint has
never had a TTL, so reusing it would silently turn every analysis over a day
old into a 404. TestVideoDetailIgnoresProcessorCacheTtl pins that.

real_video_processor.py is left untouched (claimed by open PR #1237).

Verification: 97 passed (88 pre-existing + 9 new). Prove-fail: reverting only
the to_thread delegation, keeping the helper defined, fails exactly the two
off-loop tests (ticks=0, assert 0 >= 5) and passes the other 95.

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

* fix: keep JSON null and null-byte ids off the 404/500 path

Review of the parent commit surfaced two behaviour regressions introduced
by replacing the ``exists()`` + ``open()`` pair with a single ``open()``.

1. A cache entry whose content is the JSON literal ``null`` parses to
   ``None``, which the handler could not distinguish from "no entry".
   ``main`` served it as a 200; the parent commit turned it into a 404.
   Fixed with a module-level ``_CACHE_MISS`` sentinel and an identity
   check, so every falsy payload (``null``, ``{}``, ``[]``, ``""``, ``0``,
   ``false``) keeps its 200.

2. ``Path.exists()`` swallows ``ValueError`` as well as ``OSError``, so a
   ``video_id`` carrying an embedded null byte (``GET /api/v2/videos/%00``)
   used to report the entry as absent and return 404. A bare ``open()``
   let the ``ValueError`` escape and turned that into a 500. Fixed by
   treating ``ValueError`` as a miss alongside ``FileNotFoundError``.
   Every other ``OSError`` still propagates, so the directory case and
   the corrupt-JSON case keep their 500s.

Verified with a four-case version-swap parity probe (null-content entry,
control object, absent entry, %00) showing byte-identical status codes
and response bodies between ``main`` and this branch.

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

* docs: narrow the offload claim to filesystem latency

json.load holds the GIL, so the to_thread hop relocates the parse stall
rather than removing it. Narrow the documented guarantee accordingly and
add a characterisation test so the weaker claim stays honest.

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

* docs: link residual parse stall to issue #1306

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

---------

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

Copy link
Copy Markdown
Owner Author

@linear-code @coderabbitai review

Restructured the PR body to satisfy the PR Governance delivery contract (the previous body predated the ## Risk / ## Verification / ## Production evidence requirement and would have failed the check the moment this left draft). No source changes — head is still e18af3c888331a3699233430a9866312b55f3d83.

Re-verified locally before asking for review: PYTHONPATH=src pytest tests/unit/test_real_processors.py88 passed.

Three judgement calls I want a second opinion on, because each one is a place where a reasonable reviewer could disagree:

1. Keeping exists() and glob() inside a single to_thread hop.
The obvious alternative is two hops, or dropping exists() entirely and letting glob() return empty for a missing directory. I kept them together because splitting them reintroduces a stat/scan race — the same rationale already documented for _read_cache_file in this file. Is the single-hop coupling the right call, or is the race here actually benign enough that the simpler glob()-only form (no exists() at all) would be better? Path.glob on a nonexistent directory yields nothing rather than raising, so the exists() guard may be pure overhead.

2. sum(1 for _ in …) instead of len(list(…)).
This avoids materializing an unbounded listing when only the count is used. It is strictly better on memory, but it is also a second, unrelated change riding along with the offload. Should it stay in this PR or be split out? My read is that it belongs here — the reason the walk is expensive enough to need offloading is the same reason materializing it is wasteful — but I can see the argument for a minimal diff.

3. Whether closing #1231 with this change is honest.
#1231 literally asks for _load_from_cache / _save_to_cache offload, and both already shipped in #1228. This PR instead fixes a third call site in the same file that #1228 missed. I think Closes #1231 is correct because the issue's intent is "this class of defect in this file," and the alternative (close as duplicate, open a fresh issue) discards a real endpoint-reachable bug. But this is a scope-interpretation judgement and I would rather have it challenged now than after merge.

Also worth a skeptical look: the two new tests patch Path.glob globally via patch.object(Path, "glob", …). That is broad. I convinced myself it is safe here because the assertions are scoped to threads observed during this one awaited call, and both tests carry anti-vacuity guards (assert scan_threads, "cache directory was never scanned"). If you see a way this could pass vacuously or leak across tests, please flag it.

@groupthinking
groupthinking marked this pull request as ready for review August 5, 2026 00:50
Copilot AI balanced review requested due to automatic review settings August 5, 2026 00:50
@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor
🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- PR metadata and changed files ---'
gh pr view 1237 --repo groupthinking/EventRelay --json number,title,headRefOid,baseRefName,body,files,url
echo
echo '--- Canonical issue `#1231` ---'
gh issue view 1231 --repo groupthinking/EventRelay --json number,title,state,body,url,closedByPullRequestsReferences
echo
echo '--- Candidate files ---'
fd -a 'real_processors\.py|pyproject\.toml|pytest\.ini|conftest\.py' . | sort
echo
echo '--- Source outline ---'
src_file="$(fd -a '^real_processors\.py$' . | head -n1)"
test_file="$(fd -a '^test_real_processors\.py$' . | head -n1)"
[ -n "$src_file" ] && ast-grep outline "$src_file" --items all --match 'RealVideoProcessor|get_processing_status|_count_cached_files' || true
[ -n "$test_file" ] && ast-grep outline "$test_file" --items all --match 'TestGetProcessingStatus|cache_scan|glob' || true
echo
echo '--- PR diff ---'
gh pr diff 1237 --repo groupthinking/EventRelay
echo
echo '--- Relevant source references ---'
if [ -n "$src_file" ]; then
  rg -n -C 8 'get_processing_status|_count_cached_files|_read_cache_file|_write_cache_file|cache_dir' "$src_file"
fi
echo
echo '--- Relevant test references ---'
if [ -n "$test_file" ]; then
  rg -n -C 12 'TestGetProcessingStatus|Path, "glob"|cache_scan|cached_videos|get_processing_status' "$test_file"
fi
echo
echo '--- Test configuration ---'
for f in $(fd -a 'pyproject\.toml|pytest\.ini|conftest\.py' .); do
  echo "### $f"
  rg -n -C 3 'asyncio|pytest|xdist|parallel' "$f" || true
done

Length of output: 19199


🏁 Script executed:

#!/bin/bash
set -euo pipefail

src='src/youtube_extension/backend/services/real_video_processor.py'
test='tests/unit/test_real_processors.py'
endpoint='src/youtube_extension/backend/real_api_endpoints.py'

echo '--- RealVideoProcessor source structure ---'
ast-grep outline "$src" --items all --match 'RealVideoProcessor|get_processing_status|_count_cached_files|_load_from_cache|_save_to_cache'

echo
echo '--- Relevant processor implementation ---'
sed -n '40,155p' "$src"
sed -n '440,525p' "$src"

echo
echo '--- Status endpoint call context ---'
if [ -f "$endpoint" ]; then
  sed -n '295,345p' "$endpoint"
else
  fd -a '^real_api_endpoints\.py$' . -x sh -c 'echo "--- {} ---"; rg -n -C 8 "get_processing_status" "{}"'
fi

echo
echo '--- Test class, fixtures, and adjacent tests ---'
rg -n -C 15 '^class TestGetProcessingStatus\b|^def _make_video_processor\b|^async def test_.*processing_status|^async def test_cache_scan' "$test"
sed -n '1500,1685p' "$test"

echo
echo '--- All production Path.glob calls and direct status callers ---'
rg -n -C 3 '\.glob\s*\(' src tests -g '*.py' | head -n 500
rg -n -C 5 '\.get_processing_status\s*\(' src tests -g '*.py'

echo
echo '--- Issue `#1231` event timeline and merged PR `#1228` summary ---'
gh api repos/groupthinking/EventRelay/issues/1231/timeline --paginate \
  -H 'Accept: application/vnd.github+json' \
  --jq '.[] | select(.event == "cross-referenced" or .event == "closed" or .event == "reopened") | {event,created_at,source:(.source.issue.html_url // .source.issue.pull_request.url // null)}'
gh pr view 1228 --repo groupthinking/EventRelay --json number,state,mergedAt,title,body,url

Length of output: 13081


@groupthinking I support the worker-thread change.

  1. Keep one asyncio.to_thread call for the complete scan. This is correct.

    Remove Path.exists() from _count_cached_files. The check adds a stat and does not prevent the directory-disappearance race. The directory can disappear after exists() returns. Path.glob() already gives an empty result for a missing directory. This simplifies the helper without changing the status result.

  2. Keep sum(1 for _ in ...) in this PR.

    It is part of the same scan operation. It reduces memory use for an unbounded directory listing. It does not broaden the behavioral scope.

  3. Do not use Closes Offload RealVideoProcessor result cache disk I/O to threads #1231`` for this PR.

    The explicit requirements in #1231 are cache reads, cache writes, and atomic writes. The PR body states that PR #1228 completed those requirements. This PR fixes a separate endpoint-reachable scan. Create a follow-up issue for the status scan, or reference #1231 without closing it. Then close #1231 based on PR #1228 if its original requirements are complete.

  4. Tighten the new tests before merge.

    patch.object(Path, "glob", ...) affects every Path instance in the process. An unrelated Path.glob() call can append an unrelated thread ID in the first test. It can also set scan_started and release the gate in the second test. The anti-vacuity guards only prove that some Path.glob() call occurred.

    Prefer patching RealVideoProcessor._count_cached_files with a wrapper that records or blocks around the real helper. This targets the exact offloaded operation. If the Path.glob patch remains, only record or gate calls where self == proc.cache_dir and pattern == "*_processed.json". Assert that the targeted call occurred.

The production fix is low risk. The test patch scope and issue-closing reference need correction.

✅ 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.

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 cache-directory counting from the asyncio event loop.

Changes:

  • Adds a lazy cache-counting helper.
  • Runs cache scanning through asyncio.to_thread.
  • Adds thread identity and loop-responsiveness tests.

Reviewed changes

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

File Description
real_video_processor.py Offloads cache scanning.
test_real_processors.py Tests non-blocking behavior.

Comment thread src/youtube_extension/backend/services/real_video_processor.py Outdated
The docstring overclaimed that keeping exists() and glob() in one thread
hop prevents the cache directory from disappearing between them. That is
not atomic. Clarify that the race is benign: glob() on a missing
directory yields nothing, so the count degrades to 0 rather than raising.

Addresses Copilot review thread on PR #1237. Docstring-only; no behavior
change.

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

@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

🤖 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/backend/services/real_video_processor.py`:
- Around line 132-149: Update _count_cached_files to catch OSError from the
cache existence check or glob scan, log the filesystem failure with targeted
context using the service’s established logger, then re-raise the original
exception. Preserve the current zero result only when the cache directory does
not exist; do not treat permission or other scan errors as an empty cache.
- Around line 141-148: Remove the cache_dir.exists() guard from the
cache-counting method and let cache_dir.glob("*_processed.json") directly
produce zero matches for a missing directory. Update the method’s docstring to
remove the claim that the existence check and walk occur atomically or prevent
the directory from disappearing.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

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

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 6c287e73-b398-40df-9c27-a80b3db1206c

📥 Commits

Reviewing files that changed from the base of the PR and between 8b73dac and e18af3c.

⛔ Files ignored due to path filters (1)
  • tests/unit/test_real_processors.py is excluded by !tests/**
📒 Files selected for processing (1)
  • src/youtube_extension/backend/services/real_video_processor.py
📜 Review details
⏰ Context from checks skipped due to timeout. (1)
  • GitHub Check: copilot-pull-request-reviewer
🧰 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/backend/services/real_video_processor.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/backend/services/real_video_processor.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/backend/services/real_video_processor.py
**/*.{py,js,ts,tsx}

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

Maintain >80% code coverage for new features

Files:

  • src/youtube_extension/backend/services/real_video_processor.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/backend/services/real_video_processor.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/backend/services/real_video_processor.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/backend/services/real_video_processor.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/backend/services/real_video_processor.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/backend/services/real_video_processor.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/backend/services/real_video_processor.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/backend/services/real_video_processor.py
🔍 Remote MCP GitHub Copilot, Linear

Review-relevant context

  • get_processing_status() is awaited by the live /api/v2/service-status endpoint, so the offload directly affects request responsiveness.
  • PR #1228 established the repository pattern of wrapping blocking cache I/O in one asyncio.to_thread call; PR #1288 applied the same approach to the processed-video listing scan.
  • CodeRabbit identified four concrete follow-ups:
    1. Remove Path.exists(); it adds a stat() call, does not prevent directory disappearance, and glob() already yields no matches for a missing directory.
    2. Keep lazy sum(1 for ...).
    3. Do not close #1231 with this PR; its explicit cache read/write and atomic-write requirements were completed by #1228. Linear records that work as completed under GRV-243.
    4. Narrow the tests’ global Path.glob patch to the processor cache path and expected pattern, or wrap _count_cached_files.
  • Repository search shows many unrelated Path.glob() calls in both production and test code, supporting the concern that the current test patch is broader than necessary.
  • PR #1237 has no formal review threads yet; the Copilot review check is still in progress. Other reported checks, including test, lint, security, and dependency review, are successful.
🔇 Additional comments (2)
src/youtube_extension/backend/services/real_video_processor.py (2)

132-149: 📐 Maintainability & Code Quality

Narrow the regression-test patch to this helper.

Verify that the tests patch _count_cached_files, or filter Path.glob by self.cache_dir and "*_processed.json". A global Path.glob patch can intercept unrelated scans and make the responsiveness test validate the patch instead of production behavior.

Source: MCP tools


496-499: LGTM!

Comment thread src/youtube_extension/backend/services/real_video_processor.py
Comment thread src/youtube_extension/backend/services/real_video_processor.py Outdated

Copy link
Copy Markdown
Owner Author

Shepherd synthesis — merge-ready on correctness; remaining items are optional polish + one tracker call

Verified the two reviews against the code at head e18af3c. The offload itself is correct and covered; required checks (truth-gate not_applicable, Vercel) are green — the only pending status is CodeRabbit's queued re-review.

Copilot's thread (r3717180930) is already satisfied. It asks to drop a "cannot disappear"/atomicity claim, but the head docstring already states the opposite: it explicitly calls the exists()glob() window non-atomic and documents the race as benign (glob on a missing dir yields nothing, so the count degrades to 0 instead of raising). No doc change is needed there — the thread can be resolved as-is.

Your three judgment calls:

  1. exists() + glob() in one to_thread hop — right call to keep them in a single hop (splitting reintroduces the stat/scan race, same rationale as _read_cache_file). Separately, CodeRabbit's narrower point stands: the exists() guard is now pure overhead, since Path.glob() already returns empty for a missing directory. Dropping it is a one-line simplification, not a correctness fix — optional. If you drop it, trim the docstring's exists()-race paragraph too so the two stay consistent.
  2. sum(1 for _ in …) — keep. Both reviews agree; it's part of the same scan, memory-bounded, and doesn't broaden behavioral scope.
  3. Closes #1231 — judgment call, no code impact. Offload RealVideoProcessor result cache disk I/O to threads #1231's literal acceptance criteria (cache read/write offload + atomic writes) shipped in perf: offload video result cache disk I/O to worker threads #1228, so the cleanest tracker hygiene is CodeRabbit's: reference (not close) Offload RealVideoProcessor result cache disk I/O to threads #1231 here, close it against perf: offload video result cache disk I/O to worker threads #1228, and open a follow-up for the status-endpoint scan. Your "issue intent = this defect class in this file" reading is also defensible. Either is fine — not a blocker.

Test patch scope (CodeRabbit item 4) — valid hardening, not a current bug. patch.object(Path, "glob", …) is process-global, but in this path the only Path.glob during the awaited call is the targeted one (cost_monitor is mocked, so it can't glob), and both tests carry anti-vacuity guards — they hold today. To future-proof against a later glob being added to get_processing_status, gate the recorder on self == proc.cache_dir and pattern == "*_processed.json", or patch _count_cached_files directly.

Verdict: green on correctness — none of the above blocks merge. Items 1 and 4 are optional polish; item 3 is a tracker preference. Merge to protected main is a human decision and I will not auto-merge from an unattended run. Staged for when you're satisfied:

gh pr merge 1237 --squash

Generated by Claude Code

Addresses two CodeRabbit findings on _count_cached_files:

- Remove the cache_dir.exists() guard. Path.glob already yields no matches
  for a missing directory, so the check only added a redundant stat() and
  did not make the walk atomic.
- Wrap the walk so a genuine filesystem failure (e.g. a permission error on
  a directory that does exist) is logged via logger.exception and re-raised,
  rather than surfacing as service_status: error with no log entry. A
  missing directory still counts as 0; only real OSErrors propagate.

Behavior is unchanged for the existing-directory and missing-directory
cases; verified across populated/missing/OSError paths.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W82PjcoVBdg9mhd4sdLEZ4
coderabbitai[bot]
coderabbitai Bot previously approved these changes Aug 5, 2026
Addresses the remaining CodeRabbit finding on #1237. The source fix (dropping
the redundant exists() guard and logging/re-raising OSError) already landed in
da5cda8; this scopes the two regression tests' global Path.glob patch to the
cache scan itself (self == cache_dir and pattern == "*_processed.json") so
unrelated Path.glob calls can no longer trip the thread recorder or the
responsiveness gate. Keeps the tests asserting production behavior, not the
patch. Negative control (offload removed) still fails the thread-identity test.

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

Copy link
Copy Markdown
Owner Author

@coderabbitai review

Both actionable findings from the previous review are now addressed:

  1. _count_cached_files error handling (redundant exists() guard + swallowed filesystem errors) — resolved in da5cda8: the exists() probe is dropped (glob already yields nothing for a missing directory), and a genuine OSError is now logged via the service logger and re-raised rather than misreported as an empty cache.
  2. Over-broad Path.glob patch in the regression tests — resolved in 059aee4: both test_cache_scan_runs_off_the_event_loop and test_cache_scan_does_not_stall_the_event_loop now gate the recorder/patch on self == cache_dir and pattern == "*_processed.json", so unrelated Path.glob calls can no longer trip the thread recorder or the responsiveness gate. The tests continue to assert production behavior rather than the patch.

Verification on the new head: focused TestGetProcessingStatus tests pass (5 passed), and a negative control (offload reverted to inline) still fails test_cache_scan_runs_off_the_event_loop, confirming the narrowed patch remains non-vacuous. No new ruff/black findings introduced (the pre-existing E402 import-order notes are unrelated and predate this branch).


Generated by Claude Code

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor
🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- PR head and changed files ---'
gh pr view 1237 --repo groupthinking/EventRelay \
  --json number,headRefOid,files,url \
  --jq '{number, headRefOid, files: [.files[].path], url}'

echo
echo '--- Current PR diff ---'
gh pr diff 1237 --repo groupthinking/EventRelay -- \
  src/youtube_extension/backend/services/real_video_processor.py \
  tests/unit/test_real_processors.py

echo
echo '--- Source structure ---'
ast-grep outline src/youtube_extension/backend/services/real_video_processor.py \
  --items all --match 'RealVideoProcessor|_count_cached_files|get_processing_status'

echo
echo '--- Targeted source implementation ---'
rg -n -C 14 \
  'def _count_cached_files\b|async def get_processing_status\b|logger\.|glob\s*\(' \
  src/youtube_extension/backend/services/real_video_processor.py

echo
echo '--- Targeted test structure ---'
ast-grep outline tests/unit/test_real_processors.py \
  --items all --match 'TestGetProcessingStatus|test_cache_scan_runs_off_the_event_loop|test_cache_scan_does_not_stall_the_event_loop'

echo
echo '--- Targeted test implementation ---'
rg -n -C 22 \
  'test_cache_scan_runs_off_the_event_loop|test_cache_scan_does_not_stall_the_event_loop|patch\.object\(Path, "glob"|cache_dir|_processed\.json' \
  tests/unit/test_real_processors.py

echo
echo '--- Commits stated by the user ---'
git show --stat --oneline da5cda8 059aee4

Length of output: 2055


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- Complete PR diff ---'
gh pr diff 1237 --repo groupthinking/EventRelay

echo
echo '--- Processor outline ---'
ast-grep outline src/youtube_extension/backend/services/real_video_processor.py \
  --items all --match 'RealVideoProcessor|_count_cached_files|get_processing_status'

echo
echo '--- Processor helper and caller ---'
rg -n -C 18 \
  'def _count_cached_files\b|async def get_processing_status\b|asyncio\.to_thread|except OSError|logger\.' \
  src/youtube_extension/backend/services/real_video_processor.py

echo
echo '--- Test outline ---'
ast-grep outline tests/unit/test_real_processors.py \
  --items all --match 'TestGetProcessingStatus|test_cache_scan_runs_off_the_event_loop|test_cache_scan_does_not_stall_the_event_loop'

echo
echo '--- Test patch predicates and assertions ---'
rg -n -C 28 \
  'test_cache_scan_runs_off_the_event_loop|test_cache_scan_does_not_stall_the_event_loop|patch\.object\(Path, "glob"|self == cache_dir|pattern == "\*_processed\.json"|scan_threads|scan_started' \
  tests/unit/test_real_processors.py

echo
echo '--- Stated commits ---'
git show --format=fuller --stat da5cda8 059aee4

Length of output: 25816


🏁 Script executed:

#!/bin/bash
set -euo pipefail

repo='groupthinking/EventRelay'
ref='059aee451ed0519bfe73e0cc872eb83024e4682a'

echo '--- Local candidate test files ---'
fd -a 'test_real_processors\.py$' . || true

echo
echo '--- PR-head test imports and status-test class ---'
gh api "repos/$repo/contents/tests/unit/test_real_processors.py?ref=$ref" \
  --jq '.content' \
  | base64 --decode \
  | nl -ba \
  | sed -n '1,45p;1500,1695p'

echo
echo '--- PR-head status error handling ---'
gh api "repos/$repo/contents/src/youtube_extension/backend/services/real_video_processor.py?ref=$ref" \
  --jq '.content' \
  | base64 --decode \
  | nl -ba \
  | sed -n '125,160p;490,535p'

echo
echo '--- PR commits and parent relationship ---'
gh api "repos/$repo/commits/da5cda8" --jq '{sha, parents: [.parents[].sha], message: .commit.message}'
gh api "repos/$repo/commits/059aee4" --jq '{sha, parents: [.parents[].sha], message: .commit.message}'

Length of output: 1358


🏁 Script executed:

#!/bin/bash
set -euo pipefail

repo='groupthinking/EventRelay'
ref='059aee451ed0519bfe73e0cc872eb83024e4682a'

numbered_range() {
  local start="$1"
  local end="$2"
  awk -v start="$start" -v end="$end" 'NR >= start && NR <= end { printf "%6d  %s\n", NR, $0 }'
}

echo '--- PR-head test imports ---'
gh api "repos/$repo/contents/tests/unit/test_real_processors.py?ref=$ref" \
  --jq '.content' | base64 --decode \
  | numbered_range 1 45

echo
echo '--- PR-head status regression tests ---'
gh api "repos/$repo/contents/tests/unit/test_real_processors.py?ref=$ref" \
  --jq '.content' | base64 --decode \
  | numbered_range 1550 1685

echo
echo '--- PR-head helper and status error handling ---'
gh api "repos/$repo/contents/src/youtube_extension/backend/services/real_video_processor.py?ref=$ref" \
  --jq '.content' | base64 --decode \
  | numbered_range 125 160
gh api "repos/$repo/contents/src/youtube_extension/backend/services/real_video_processor.py?ref=$ref" \
  --jq '.content' | base64 --decode \
  | numbered_range 490 535

echo
echo '--- Stated commits ---'
gh api "repos/$repo/commits/da5cda8" --jq '{sha, parents: [.parents[].sha], message: .commit.message}'
gh api "repos/$repo/commits/059aee4" --jq '{sha, parents: [.parents[].sha], message: .commit.message}'

Length of output: 16638


@groupthinking Reviewed the current head 059aee4.

The two prior actionable findings are addressed.

  • RealVideoProcessor._count_cached_files removes the redundant Path.exists() probe. It counts lazily with sum() and preserves real filesystem failures by logging and re-raising OSError. See Lines 133-153.
  • RealVideoProcessor.get_processing_status still performs the complete scan in one asyncio.to_thread call. See Lines 496-531.
  • The two regression tests now record or gate only the target call for cache_dir and "*_processed.json". Unrelated Path.glob() calls cannot satisfy their assertions or release the event-loop gate. See Lines 1596-1670.

I found no remaining blocking issue in these changes. I verified the source and test code at the PR head. I did not run the test suite because this review sandbox does not execute repository code.

⚠️ Action not completed

Review rate limited.

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

Independent review — red-team of the diff on top of CodeRabbit's review

Ran an independent pass over head 059aee4 rather than taking the green checks at face value. Verification was done in a detached worktree at the PR head, not on my working tree.

Evidence

Check Command Result
Full file, at PR head pytest tests/unit/test_real_processors.py 88 passed in 1.60s
RED proof same file, after git checkout origin/main -- src/.../real_video_processor.py 2 failed, 3 passed

The RED proof is the part that actually matters. With only the source file reverted to main and the new tests left in place, exactly the two new regression tests fail:

FAILED tests/unit/test_real_processors.py::TestGetProcessingStatus::test_cache_scan_runs_off_the_event_loop
FAILED tests/unit/test_real_processors.py::TestGetProcessingStatus::test_cache_scan_does_not_stall_the_event_loop

and they fail with TimeoutError raised out of asyncio/timeouts.py:115 — i.e. the event loop genuinely stalls for longer than the gate allows while the cache scan runs. That confirms the tests are coupled to the defect and not to incidental structure.

Points I deliberately tried to break

  1. Are the tests vacuous? This was CodeRabbit's second finding, and it was a fair one. The concern was that a broad Path.glob patch would be satisfied by unrelated glob() calls elsewhere in the call path, so the assertions could pass without ever exercising the cache scan. 059aee4 narrows the patch to the cache_dir / "*_processed.json" target. I re-checked this by reverting the source only — if the assertions were satisfiable by unrelated calls, they would still have passed. They did not. Resolved.

  2. Does _count_cached_files swallow real filesystem errors? Dropping the Path.exists() probe is only safe if genuine OSErrors still surface. The current implementation logs and re-raises rather than counting zero, so a permissions failure or a vanished directory is not silently reported as "0 cached files". I consider this correctly handled.

  3. Is one to_thread hop enough, or does the scan re-enter the loop mid-way? get_processing_status performs the complete scan inside a single asyncio.to_thread call. A per-file hop would have been worse — it would have multiplied context switches and opened a window for the cache directory to change under the count. One hop is the right granularity here.

Where I disagree with nothing, and what I still flag

I found no remaining blocking issue, which matches CodeRabbit's conclusion on the same head. One honest caveat: CodeRabbit explicitly did not execute the test suite (its sandbox does not run repository code), so its sign-off is a static read. The execution evidence above is mine, and it is what I am relying on. If someone wants to challenge this PR, the RED proof is the claim to attack — it is reproducible with the single git checkout origin/main -- <source file> step shown above.

@linear-code was tagged on the earlier review request and has not posted to this thread. Consistent with #1341 and #1338, which merged under the same conditions, I am not treating that silence as a blocking signal.

@groupthinking
groupthinking merged commit c44494d into main Aug 5, 2026
34 checks passed
@groupthinking
groupthinking deleted the perf/status-cache-glob-offload-1231 branch August 5, 2026 01:13
@linear-code

linear-code Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

GRV-317

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.

Offload RealVideoProcessor result cache disk I/O to threads

3 participants