Skip to content

perf: shut down container services concurrently - #1323

Merged
groupthinking merged 1 commit into
mainfrom
perf/container-shutdown
Aug 4, 2026
Merged

perf: shut down container services concurrently#1323
groupthinking merged 1 commit into
mainfrom
perf/container-shutdown

Conversation

@groupthinking

Copy link
Copy Markdown
Owner

Canonical issue

Closes #1322

Scope

src/youtube_extension/backend/containers/service_container.pyshutdown and a new
_shutdown_service helper. Tests in tests/unit/test_service_container.py.

No service implementation is touched; no public API changes.

Outcome

ServiceContainer.shutdown is the FastAPI @app.on_event("shutdown") handler
(backend/main.py), so it runs inside the SIGTERM grace window on every deploy,
scale-in and restart. It had two defects.

1. Teardown was serial. A for loop awaited each service in turn, so total cost was
the sum of every close() round-trip instead of the slowest one. The services are
independent and no teardown ordering was ever declared or guaranteed. They now tear down
under a single asyncio.gather.

2. Aliased singletons were cleaned up twice. _register_skill_dependency_aliases
registers four aliases whose factories are lambda: self.get_service("<target>").
get_service caches into self._singletons[name], so once both names resolve, the same
instance
sits under two keys — and shutdown iterated self._singletons.items(), calling
cleanup() on that one object twice:

alias target
gemini_service hybrid_processor_service
database_service data_service
analytics_service metrics_service
email_service notification_service

Targets are now deduplicated by id(service) before teardown.

Teardown hooks may also be synchronous, so the result is awaited only when
inspect.isawaitable — the same idiom already used at api_cost_worker.py:235.

Production evidence

Stub container, 8 services at 40ms teardown each plus one alias pointing at an existing
instance, driven through the real ServiceContainer.shutdown:

before after
wall clock 370.3 ms 41.4 ms
vs. parallel lower bound (~40 ms) 9.3x 1.04x
cleanup() calls on the aliased instance 2 1

The serial cost scales with service count: 13 services are registered today, so every
added service extends the grace-window spend. If the total ever exceeds the platform's
grace period the process is SIGKILLed and the remaining services never close at all,
leaking connections server-side.

Risk

Low, but three behavioural deltas are worth stating explicitly.

Teardown ordering is no longer insertion-ordered. gather does not preserve dict
order. There is no declared shutdown dependency graph in the container, and the previous
code already swallowed per-service exceptions and continued to the next service, so it
never guaranteed dependency-safe teardown either. This makes the existing absence of
ordering explicit rather than incidental.

Synchronous closers now succeed. Previously await service.cleanup() ran a sync
cleanup() and then raised TypeError on await None, which was caught and logged as a
spurious shutdown error. Such services now complete cleanly and record no error.

A non-callable cleanup attribute now falls through to close(). The old
if hasattr(cleanup) / elif hasattr(close) chain meant a non-callable cleanup attribute
suppressed close() entirely. Strictly an improvement, but it is a behaviour change.

Fan-out is deliberately unbounded here. Width is capped by the registration count
(13 services + 4 aliases, a compile-time constant), not by request volume or user input,
so no semaphore is warranted — and throttling shutdown would risk the exact grace-window
overrun this PR prevents. return_exceptions=True can yield CancelledError, so results
are checked with isinstance(result, BaseException) rather than Exception.

Verification

Five new tests in TestShutdownConcurrency. Three fail against pre-change source:

FAILED tests/unit/test_service_container.py::TestShutdownConcurrency::test_services_are_torn_down_concurrently
FAILED tests/unit/test_service_container.py::TestShutdownConcurrency::test_aliased_service_is_torn_down_once
FAILED tests/unit/test_service_container.py::TestShutdownConcurrency::test_synchronous_cleanup_is_supported

The concurrency test asserts observed peak in-flight teardowns == 6 rather than relying
solely on a wall-clock threshold, so it does not flake on a loaded host.

The other two cover dedupe-by-identity (distinct-but-equal services are each torn down) and
failure isolation (a raising service is reported in shutdown_errors without blocking the
rest).

After the change, the whole file passes including all five pre-existing TestShutdown tests
— no regression:

$ .venv/bin/python -m pytest tests/unit/test_service_container.py \
    tests/unit/test_backend_main.py tests/integration/test_skill_di.py \
    --override-ini="addopts=" -p no:cacheprovider -q
124 passed, 1 warning in 3.01s

ruff check on both changed files reports only two pre-existing E731 lambda
assignments at test_service_container.py:117 and :131 (both present on origin/main;
this branch adds no lambdas).

Agent handoff

CI is expected to show two pre-existing failures unrelated to this change:
test and Generate and Upload Coverage, both from
tests/unit/test_gh_aw_workflow_governance.py::test_ci_investigator_requires_dedicated_codex_credential
(the workflow file was deleted in 07b8a2ec2 but its test from c2c23f4fb remains).
That failure is already owned by PRs #1317 and #1320 — please do not fix it here.

ServiceContainer.shutdown is the FastAPI shutdown handler, so it runs
inside the SIGTERM grace window on every deploy and restart. It had two
defects.

Teardown was serial: a for loop awaited each service in turn, making
total cost the sum of every close() round-trip rather than the slowest
one. The services are independent and no teardown ordering was declared
or guaranteed, so they now tear down under a single asyncio.gather.
Exceptions are collected via return_exceptions and reported per service,
preserving the previous "log and continue" behaviour.

Aliased singletons were cleaned up twice. _register_skill_dependency_
aliases registers four aliases whose factories delegate to get_service,
which caches into _singletons, so one instance ends up stored under two
names. The loop then called cleanup() on it once per name, double
closing sessions and pools. Targets are now deduplicated by identity.

Teardown hooks may also be synchronous, so the result is awaited only
when inspect.isawaitable, matching the idiom in api_cost_worker. This
removes a spurious "await NoneType" error previously logged for sync
cleanup().

Closes #1322

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

vercel Bot commented Aug 4, 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 4, 2026 2:32am

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

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

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 4046637e-ea61-4c46-9b99-3533f2706f77

📥 Commits

Reviewing files that changed from the base of the PR and between 11be2d1 and 482d7fd.

⛔ Files ignored due to path filters (1)
  • tests/unit/test_service_container.py is excluded by !tests/**
📒 Files selected for processing (1)
  • src/youtube_extension/backend/containers/service_container.py
📜 Recent review details
⏰ Context from checks skipped due to timeout. (3)
  • GitHub Check: Generate and Upload Coverage
  • GitHub Check: trivy
  • GitHub Check: test
🧰 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/containers/service_container.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/containers/service_container.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/containers/service_container.py
**/*.{py,js,ts,tsx}

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

Maintain >80% code coverage for new features

Files:

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

Additional review context

  • shutdown_event() awaits ServiceContainer.shutdown() before database-optimizer shutdown. The container registers 13 core services plus four lazy aliases; only instantiated services enter _singletons.
  • The diff correctly changes teardown to identity-deduplicated asyncio.gather, supports sync/async hooks, and preserves per-service failure isolation.
  • Current implementations show VideoProcessingService.cleanup() closes its private processor, while HybridProcessorService.cleanup() cleans its private Gemini service; no direct cross-singleton ordering dependency was identified.
  • Added tests cover concurrency, alias deduplication, identity-vs-equality, synchronous cleanup, and failure isolation. They do not explicitly cover a synchronous close() fallback or a non-callable cleanup attribute.
  • CodeRabbit reported no production-blocking findings, considered identity deduplication and bounded fan-out safe, and did not execute the test suite.
  • At retrieval time, several CI jobs remained in progress (test, build, coverage, Trivy); commit statuses were otherwise successful.
  • The linked Linear issue GRV-291 remains in Triage and confirms the same three intended outcomes: concurrent teardown, exactly-once alias cleanup, and sync-hook support.
🔇 Additional comments (3)
src/youtube_extension/backend/containers/service_container.py (3)

10-11: LGTM!


447-474: LGTM!


483-507: LGTM!


📝 Walkthrough

Summary by CodeRabbit

  • Bug Fixes
    • Improved application shutdown reliability by ensuring active services are closed cleanly.
    • Shutdown tasks now run concurrently, reducing delays during exit.
    • An error in one service no longer prevents other services from shutting down.
    • Duplicate service instances are handled safely to avoid repeated cleanup.

Walkthrough

Changes

Service shutdown

Layer / File(s) Summary
Teardown hook execution
src/youtube_extension/backend/containers/service_container.py
_shutdown_service prefers cleanup() over close(), supports synchronous and awaitable hooks, skips services without hooks, and logs successful completion.
Deduplicated concurrent shutdown
src/youtube_extension/backend/containers/service_container.py
shutdown deduplicates aliased singleton instances, tears them down concurrently, and records individual errors without stopping other teardowns.

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

Suggested reviewers: copilot

Poem

Services gather, side by side,
Aliases merge; duplicates hide.
Sync hooks finish, async flows,
Errors stay logged as shutdown goes.
Clean exits meet the tide.

🚥 Pre-merge checks | ✅ 5 | ❌ 2

❌ Failed checks (2 inconclusive)

Check name Status Explanation Resolution
Enforce Copilot Verification ❓ Inconclusive Evidence collection has not started. Need verify GitHub PR review metadata showing an explicit GitHub Copilot approval.
Require Ai Unit Tests ❓ Inconclusive I need to inspect the repository and pull-request metadata before assessing the label and committed AI unit tests. Verify the PR label and changed test files.
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly states that container services shut down concurrently, which is the primary change.
Description check ✅ Passed The description covers the outcome, scope, risks, verification, production evidence, and linked issue with detailed implementation context.
Linked Issues check ✅ Passed The reviewed change implements concurrent teardown, identity-based alias deduplication, and support for synchronous and asynchronous teardown hooks required by issue #1322.
Out of Scope Changes check ✅ Passed The reviewed changes are limited to service shutdown behavior and related tests, with no unrelated production changes identified.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ 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/container-shutdown
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch perf/container-shutdown

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 4, 2026
@github-actions

github-actions Bot commented Aug 4, 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 482d7fd.
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

@groupthinking

Copy link
Copy Markdown
Owner Author

@linear-code @coderabbitai review

Two defects in the FastAPI shutdown handler:

  1. Serial teardown — 370ms vs a ~40ms parallel lower bound on an 8-service stub (9.3x).
  2. Aliased singletons cleaned up twice — the four skill-dependency aliases delegate to get_service, which caches into _singletons, so one instance sits under two keys and cleanup() ran twice on it.

Specific review asks:

  • Dedupe by id(service) — safe here because _singletons holds live strong refs for the whole loop, so no id-reuse hazard. Confirm that reasoning holds.
  • Unbounded gather is deliberate: width is the registration count (13 + 4 aliases), a compile-time constant, not request-driven. Adding a semaphore would risk the grace-window overrun this fixes. Agree?
  • Lost teardown ordering is documented in ## Risk. The previous code swallowed per-service exceptions and continued, so it never guaranteed dependency-safe ordering either — but please flag if any service pair actually needs sequencing.

CI will show two pre-existing reds from test_gh_aw_workflow_governance.py (already owned by #1317/#1320) — not from this change.

@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown

Agent Completion Truth Gate: NOT_APPLICABLE

Evidence agrees.

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

Workflow evidence

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor
🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '=== PR diff ==='
git diff --find-renames origin/main...HEAD -- \
  src/youtube_extension/backend/containers/service_container.py \
  tests/unit/test_service_container.py

echo '=== ServiceContainer outline ==='
ast-grep outline src/youtube_extension/backend/containers/service_container.py --items all

echo '=== Relevant ServiceContainer sections ==='
rg -n -C 5 \
  'def (shutdown|_shutdown_service|get_service|register_service|_register_skill_dependency_aliases)\b|_singletons|cleanup|\.close\b' \
  src/youtube_extension/backend/containers/service_container.py

echo '=== Registered services and alias factories ==='
rg -n -C 3 \
  'register_service\(|register_singleton\(|_register_skill_dependency_aliases|lambda: self\.get_service' \
  src/youtube_extension/backend/containers/service_container.py

echo '=== Cleanup/close definitions and cross-service shutdown references ==='
rg -n -i -C 3 \
  'async def (cleanup|close|shutdown)\b|def (cleanup|close|shutdown)\b|await .*\.?(cleanup|close|shutdown)\s*\(|\.(cleanup|close|shutdown)\s*\(' \
  src/youtube_extension/backend --glob '*.py'

echo '=== Test outline and changed test area ==='
ast-grep outline tests/unit/test_service_container.py --items all
git diff --find-renames origin/main...HEAD --unified=80 -- tests/unit/test_service_container.py

Length of output: 66800


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '=== Factory methods and imports ==='
sed -n '1,40p' src/youtube_extension/backend/containers/service_container.py
sed -n '200,375p' src/youtube_extension/backend/containers/service_container.py

echo '=== Factory return statements and constructor dependencies ==='
rg -n -A18 -B3 \
  'def _create_(cache_service|health_monitoring_service|data_service|video_processor_factory|video_processing_service|hybrid_processor_service|websocket_connection_manager|websocket_service|notification_service|metrics_service|pubsub_service|agent_orchestrator|mcp_orchestrator)\b|return [A-Za-z_][A-Za-z0-9_]*\(' \
  src/youtube_extension/backend/containers/service_container.py

echo '=== Locate the unit test file ==='
fd -a '^test_service_container\.py$' .

echo '=== Teardown definitions in likely registered service modules ==='
rg -n -i -C 12 \
  '^\s*(async\s+)?def\s+(cleanup|close)\s*\(' \
  src/youtube_extension/backend/services \
  src/youtube_extension/backend \
  --glob '*.py' \
  --glob '!ai_code_generator.py'

echo '=== Direct container service access inside teardown hooks ==='
rg -n -i -U -P \
  '(?s)^\s*(async\s+)?def\s+(cleanup|close)\s*\([^)]*\):.*?(?:get_service\(|service_container|_container|notification_service|metrics_service|data_service|websocket_service|pubsub_service)' \
  src/youtube_extension/backend --glob '*.py' --glob '!ai_code_generator.py'

Length of output: 62453


🏁 Script executed:

#!/bin/bash
set -u

echo '=== Candidate files for registered service products ==='
fd -a -t f \
  '^(cache_service|health_monitoring_service|data_service|video_processing_service|websocket_service|notification_service|metrics_service|pubsub_service)\.py$|^__init__\.py$' \
  src/youtube_extension | sort

echo '=== WebSocket service lifecycle ==='
ast-grep outline src/youtube_extension/backend/services/websocket_service.py --items all
rg -n -C 12 \
  'class (WebSocketConnectionManager|WebSocketService)\b|def __init__\b|async def (cleanup|close)\b|def (cleanup|close)\b' \
  src/youtube_extension/backend/services/websocket_service.py || true

echo '=== Registered backend service teardown hooks ==='
for f in \
  src/youtube_extension/backend/services/cache_service.py \
  src/youtube_extension/backend/services/health_monitoring_service.py \
  src/youtube_extension/backend/services/data_service.py \
  src/youtube_extension/backend/services/notification_service.py \
  src/youtube_extension/backend/services/metrics_service.py \
  src/youtube_extension/backend/services/pubsub_service.py \
  src/youtube_extension/backend/services/video_processing_service.py
do
  if [ -f "$f" ]; then
    echo "--- $f ---"
    rg -n -C 10 '^\s*(async\s+)?def\s+(cleanup|close)\s*\(' "$f" || true
  fi
done

echo '=== AI and orchestration candidate teardown hooks ==='
rg -n -i -C 10 \
  'class (HybridProcessorService|AgentOrchestrator|MCPOrchestrator)\b|^\s*(async\s+)?def\s+(cleanup|close)\s*\(' \
  src/youtube_extension/services \
  --glob '*.py' || true

echo '=== Container references in all concrete teardown method regions ==='
rg -n -i -C 8 \
  '^\s*(async\s+)?def\s+(cleanup|close)\s*\(|self\.(video_processing_service|connection_manager|cache_service|video_processor_factory)|get_service\(' \
  src/youtube_extension/backend/services/websocket_service.py \
  src/youtube_extension/backend/services/video_processing_service.py \
  src/youtube_extension/services \
  --glob '*.py' || true

Length of output: 61380


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '=== Tracked test paths at HEAD ==='
git ls-tree -r --name-only HEAD | rg '(^|/)test_service_container\.py$' || true

echo '=== Tracked test paths at merge-base ==='
base="$(git merge-base origin/main HEAD)"
git ls-tree -r --name-only "$base" | rg '(^|/)test_service_container\.py$' || true

echo '=== Name-status for test paths ==='
git diff --name-status "$base"...HEAD -- | rg 'test_service_container\.py$' || true

echo '=== Worktree status for the expected path ==='
git status --short -- tests/unit/test_service_container.py
git check-ignore -v tests/unit/test_service_container.py || true

Length of output: 1040


@groupthinking I found no production-blocking issue in this change.

  • id(service) deduplication is safe here. _singletons retains each instance during target collection. targets retains each selected instance through asyncio.gather. Python cannot reuse an object ID while that object remains live.
  • Unbounded asyncio.gather is appropriate. Fan-out is bounded by the container registration set: 13 core services and four aliases. It is not request-driven. A semaphore would increase shutdown latency inside the SIGTERM grace window.
  • I found no registered service pair that requires ordered teardown. VideoProcessingService.cleanup() closes its private processor. HybridProcessorService.cleanup() cleans up its private GeminiService. The other inspected registered services have no teardown hook or no cross-singleton teardown dependency.
  • The helper correctly supports synchronous and asynchronous hooks. It also preserves per-service failure isolation with return_exceptions=True, including CancelledError results.

The changed test path, tests/unit/test_service_container.py, is tracked and modified in the PR tree. I did not execute the test suite in this review environment.

✅ 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

Improves container shutdown performance and prevents duplicate teardown of aliased services.

Changes:

  • Adds concurrent, identity-deduplicated service teardown.
  • Supports synchronous and asynchronous teardown hooks.
  • Adds shutdown concurrency and failure-isolation tests.

Reviewed changes

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

File Description
service_container.py Implements deduplicated concurrent shutdown.
test_service_container.py Tests concurrency, aliases, sync hooks, and failures.

Comment on lines +469 to +471
result = closer()
if inspect.isawaitable(result):
await result

assert peak == 6, f"expected all 6 teardowns in flight together, saw {peak}"
# Serial teardown would take >= 6 * 50ms = 300ms.
assert elapsed < 0.20, f"teardown appears serial ({elapsed * 1000:.0f}ms)"

Copy link
Copy Markdown
Owner Author

Independent verification (head 482d7fd):

CI red is the known pre-existing failure only. The test job's sole failure is tests/unit/test_gh_aw_workflow_governance.py::test_ci_investigator_requires_dedicated_codex_credentialFileNotFoundError for the deleted .github/workflows/eventrelay-ci-investigator.md. Result line: 1 failed, 7947 passed, 6 deselected, 5 xpassed, and this PR's own TestShutdownConcurrency cases are in the passing set. Confirmed unrelated to this diff (matches the description; owned by #1317 / #1320) — no regression introduced here. All other checks (build, lint-python/frontend, bandit, CodeQL, Trivy, python-safety, dependency-review, truth-gate) are green; coverage was still running at scan time.

Both copilot-pull-request-reviewer threads look valid and are worth applying before merge:

  1. service_container.py:471 — a synchronous closer() is invoked inline on the loop thread with no await point before it, so blocking sync cleanups still serialize (and stall concurrent async ones). Wrapping non-coroutine closers in asyncio.to_thread restores the intended "bounded by the slowest service" behavior the PR is going for.
  2. test_service_container.py:414 — the wall-clock/elapsed assertion can flake when a loaded runner deschedules the loop; the peak == 6 assertion already proves overlap, so the timing check (and the time import) can be dropped for a scheduler-independent test.

Not merging from here: main is protected and this PR carries no automerge label, so it stays at the human merge gate pending the two fixes above.


Generated by Claude Code

@groupthinking

Copy link
Copy Markdown
Owner Author

CI verified: 1 failed, 7947 passed. The one failure is test_gh_aw_workflow_governance.py::test_ci_investigator_requires_dedicated_codex_credential — pre-existing on main (workflow file deleted in 07b8a2ec2, test left behind), already owned by #1317/#1320. Same single failure in both test and Generate and Upload Coverage. PR Governance: SUCCESS, CodeQL: SUCCESS, trivy: SUCCESS, lint-python: SUCCESS.

New TestShutdownConcurrency suite passes in CI. Merging — reviewer feedback can be actioned as a follow-up if it arrives.

@groupthinking
groupthinking merged commit 80088e7 into main Aug 4, 2026
34 of 36 checks passed
@groupthinking
groupthinking deleted the perf/container-shutdown branch August 4, 2026 02:39
@linear-code

linear-code Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

GRV-292

groupthinking added a commit that referenced this pull request Aug 4, 2026
…e of #1216) (#1333)

* fix(security): sandbox local media paths in cloud AI providers (#1209)

All three cloud AI providers dispatch `analyze_image(image_url, ...)` on the
string's prefix: `s3://` and `http(s)://` are treated as remote sources, and
anything else fell through to an unguarded `open()`. A caller-supplied
absolute path, `../` traversal, or symlink could therefore read any file
readable by the service account.

The same unguarded sink existed in all three providers, not just the one named
in the issue:

  - aws_rekognition.py  `_prepare_image_input`
  - azure_vision.py     `_prepare_image_input`
  - google_cloud.py     inline `open()` in `analyze_image`

Introduce `cloud_ai/media_paths.py` as the single policy for local reads:

  - Local reads are opt-in via `CLOUD_AI_MEDIA_ROOT`. Unset (the default)
    disables them entirely, restricting providers to `s3://`/`https://`.
    This is fail-closed, and answers the issue's open question.
  - When a root is configured, both root and candidate are fully resolved
    (`Path.resolve()` follows symlinks) and the candidate must be contained by
    the root -- covering symlink escapes, not just lexical `..` segments.
  - Non-regular files (FIFO, device, directory) are rejected, so a FIFO placed
    inside the root cannot pin a `to_thread` worker forever.
  - Rejection raises the new typed `UnsafeMediaPathError(CloudAIError)` instead
    of silently returning empty bytes. Each provider re-raises `CloudAIError`
    subclasses unchanged so the type survives to the caller.
  - Providers read from the resolved path, not the caller string, narrowing the
    check-to-open race.
  - Error messages echo only the caller-supplied value; the resolved path is
    logged server-side for forensics rather than returned.

Adds tests/unit/test_cloud_ai_media_paths.py (44 tests) covering absolute
paths, `../` traversal, symlink escape, non-regular files, the disabled
default, and per-provider propagation. Existing local-file tests now set
`CLOUD_AI_MEDIA_ROOT`. Full cloud AI suite: 505 passed.

Closes #1209

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

* fix(security): reject non-directory CLOUD_AI_MEDIA_ROOT; close review gaps (#1216)

Addresses the three unresolved Copilot review threads on #1216:

1. Fail-closed on a misconfigured root. get_media_root() left resolve()
   non-strict, so CLOUD_AI_MEDIA_ROOT=/etc/passwd (a regular file) was
   accepted as the root; that file then passed its own is_relative_to()
   containment check and was returned as a permitted read. Require the
   resolved root to be an existing directory, raising ConfigurationError
   otherwise. This also surfaces a nonexistent-directory typo loudly
   instead of silently rejecting every candidate.

2. Cover the Google permitted-file branch. AWS/Azure verified successful
   reads but the Google class only had rejection cases, while the PR's
   coverage table claimed the check for all three providers. Add an
   end-to-end analyze_image test asserting the resolved file's bytes are
   assigned to vision.Image().content.

3. Correct the module docstring. Remote-scheme handling is provider-
   specific: only AWS Rekognition recognises s3:// (Azure and Google
   treat it as a local path, rejected while local reads are disabled),
   and all three accept plain http:// as well as https://.

Focused suite: 47 passed (44 + 3 new). Full cloud AI provider suites:
368 passed. ruff/mypy clean; black formatted.

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

* test: set CLOUD_AI_MEDIA_ROOT in read-offload tests added on main

The event-loop offload tests from #1304/#1323 pass raw local paths to
_prepare_image_input/analyze_image; with local reads now fail-closed
behind CLOUD_AI_MEDIA_ROOT, they must opt in via tmp_path, matching the
other pre-existing local-file tests.

Generated with [Linear](https://linear.app/myxstack/issue/GRV-296/land-pr-1216-fixsecurity-sandbox-local-media-paths#agent-session-3138b916)

Co-authored-by: linear-code[bot] <222613912+linear-code[bot]@users.noreply.github.com>

* fix(security): correct s3:// provider guidance in media-path guard

The disabled-reads UnsafeMediaPathError message and .env.example both
suggested s3:// as a recovery scheme for all three cloud AI providers,
but only AWS Rekognition recognizes s3://. Azure Vision and Google
Vision route s3:// through the disabled local-path branch, so following
that guidance just raises UnsafeMediaPathError again.

Reword both to scope s3:// to AWS Rekognition and point Azure/Google
callers at https:// (valid for every provider). No logic change; the
guard behavior is unchanged. Addresses the two Copilot review threads
on this PR.

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

---------

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

perf: ServiceContainer.shutdown tears services down serially and cleans aliases twice

2 participants