Skip to content

perf: stop rebuilding the guarded wrapper on every request - #161

Merged
bagowix merged 1 commit into
mainfrom
perf/155-transport-hot-path
Aug 12, 2026
Merged

perf: stop rebuilding the guarded wrapper on every request#161
bagowix merged 1 commit into
mainfrom
perf/155-transport-hot-path

Conversation

@bagowix

@bagowix bagowix commented Aug 12, 2026

Copy link
Copy Markdown
Owner

Summary

Every guarded request through an HTTP integration rebuilt the same wrapper from scratch: breaker(fn) re-ran the coroutine-function detection, allocated a functools.wraps closure, and threw it away one call later — on the hottest path a fault-tolerance library has. Registry.get then took the registry lock even for a plain cache hit, serialising readers that had nothing to create.

Two additions remove the work instead of caching its result (the issue's option 1 would have added a second cache to invalidate — a new entity for a problem that disappears once the caller states what it already knows):

  • CircuitBreaker.call_sync / call_async — the same protected path, no dispatch, no decoration. The caller declares the callable's nature, so nothing is detected and nothing is wrapped. call_sync never awaits (a coroutine function passed there is recorded as an immediate success — documented); call_async awaits whatever the callable returns, so it also fits handlers that are not coroutine functions, which is exactly the shape an aiohttp middleware chain hands over. call is unchanged and stays the dispatching front door.
  • Lock-free cache hit in Registry.get — a hit reads the dict without the lock; a miss falls through to the existing locked create, now with a double-check. A breaker is never replaced or removed once cached, so a reader that races a creation can at worst take the slow path. One name still yields exactly one breaker; there is a test that loses the race deliberately and asserts the winner is returned, not replaced.

All four transports (httpx, httpx2, requests, aiohttp) now call through the new methods. aiohttp additionally loses its per-request async def _send() closure — the handler goes straight to call_async.

Beyond the issue's scope, the same defect lives in the core: CircuitBreakerStrategy in pipeline.py decorated the next strategy on every execution although it statically knows whether it is on the sync or the async path. Fixed in the same way; noted separately in CHANGELOG.md.

Measurements

Stub transport answering from memory, three passes, same venv (a temporary worktree at main for the baseline):

Path Before After Stub floor
sync request ~10.5 µs ~7.5 µs ~3.55 µs
async request ~9.9 µs ~8.0 µs ~3.55 µs

Discounting the stub itself, the integration's own overhead drops roughly 40%. benchmarks/test_transport.py is new so CodSpeed guards the wrapped-transport path from now on (wrapped and unwrapped, sync and async).

Semantics are untouched: same classification, same exception propagation, same cancellation handling in the engine, same lock scope. griffe check is clean — the two methods are purely additive, so no breaking-change label.

Checklist

  • Tests added or updated (suite stays at 100% coverage)
  • uv run ruff format --check and uv run ruff check pass
  • uv run mypy, uv run pyright and uv run pyrefly check pass
  • Docs updated (docs/) for user-facing changes
  • CHANGELOG.md [Unreleased] updated
  • Commits follow Conventional Commits

Tests first: each integration got a test that fails on the old code by making re-decoration observable (CircuitBreaker.__call__ patched to raise), plus the behavioural cases for the new methods — call_sync not awaiting a coroutine function, call_async accepting a non-coroutine callable returning an awaitable, both respecting CircuitOpenError and the sync-storage guard. tests/typing_surface.py pins the inferred return types of both. The registry got the lock-free-hit assertion (a counting lock double) and the lost-race test.

Related issues

Closes #155

Added

  • Added CircuitBreaker.call_sync() and CircuitBreaker.call_async() to the interlock API.
  • Added transport benchmarks for the httpx extra.
  • Added tests for explicit call paths, registry races, and transport breaker reuse.

Fixed

  • Reduced per-request overhead in the httpx, httpx2, requests, and aiohttp extras.
  • Preserved awaiting of awaitable handler results in the aiohttp extra.
  • Preserved stable breaker identity during concurrent registry creation.

Changed

  • Registry.get() now returns cache hits without acquiring the registry lock.
  • Updated documentation and typing coverage for the new call methods.

Every request through the httpx2/httpx transports, the requests adapter and
the aiohttp middleware re-ran the breaker's sync/async detection and built a
fresh functools.wraps closure before the request could start — per-request
overhead with no behavioural value, paid exactly when a service is busiest.

- Add CircuitBreaker.call_sync() / call_async(): the same protected path with
  the dispatch removed, for callers that already know their own nature.
  call_async awaits whatever the callable returns, so an awaitable-returning
  callable that is not a coroutine function (an aiohttp handler) works too —
  which is what let the middleware drop its per-request wrapper coroutine.
- Route all four HTTP integrations through them.
- Serve a Registry cache hit without taking the registry lock; creation stays
  on the locked, double-checked path, so one name still yields one breaker.
- Apply the same fix to CircuitBreakerStrategy, which decorated the next
  pipeline layer per call although it statically knows its nature.
- Add benchmarks/test_transport.py covering the wrapped transport path.

Against a stubbed transport, an httpx request through the wrapper falls from
~10.5 to ~7.5 us (sync) and ~9.9 to ~8.0 us (async); ~3.5 us of that is the
stub itself, so the integration's own overhead drops by roughly 40%.

Closes #155
@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Changes

The PR adds explicit CircuitBreaker.call_sync() and call_async() methods. HTTP integrations and pipeline strategies use these methods directly. Registry cache hits bypass locking. Tests, benchmarks, and documentation cover the new behavior.

Breaker execution and typing

Layer / File(s) Summary
Breaker call APIs
interlock/breaker.py, tests/test_breaker.py, tests/typing_surface.py, docs/getting-started.md, docs/reference.md, docs/llms-full.txt, CHANGELOG.md
call_sync() executes without coroutine detection or awaiting. call_async() awaits awaitable results. Tests and typing checks cover success, failures, open circuits, and coroutine handling.

Integration request paths

Layer / File(s) Summary
Direct breaker invocation
interlock/integrations/aiohttp.py, interlock/integrations/httpx.py, interlock/integrations/httpx2.py, interlock/integrations/requests.py, interlock/pipeline.py
Integrations call call_sync() or call_async() directly instead of creating guarded wrappers per request.
Integration regression coverage and benchmarks
tests/test_aiohttp.py, tests/test_httpx.py, tests/test_httpx2.py, tests/test_requests.py, benchmarks/test_transport.py, CHANGELOG.md
Tests verify breaker reuse without redecorating. HTTPX benchmarks compare guarded and unwrapped synchronous and asynchronous transport calls.

Registry lookup

Layer / File(s) Summary
Lock-free cache hits
interlock/registry.py, tests/test_registry.py, docs/reference.md, docs/llms-full.txt
Registry.get() returns cached breakers before locking. Missing breakers use locked double-checked creation. Tests cover cache hits, races, and creation locking.

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

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Integration
  participant Registry
  participant CircuitBreaker
  participant Transport
  Client->>Integration: send request
  Integration->>Registry: get breaker
  Registry-->>Integration: cached breaker
  Integration->>CircuitBreaker: call_sync or call_async
  CircuitBreaker->>Transport: execute request
  Transport-->>Client: return response
Loading

Possibly related PRs

Suggested labels: enhancement

🚥 Pre-merge checks | ✅ 9
✅ Passed checks (9 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title follows Conventional Commits and accurately describes the primary performance change.
Linked Issues check ✅ Passed The changes satisfy #155 with explicit call paths, lock-free cache hits, stable breaker reuse, four optimized integrations, and benchmarks.
Out of Scope Changes check ✅ Passed The changes remain within scope and directly support the issue through implementation, tests, benchmarks, documentation, and changelog updates.
Zero-Dependency Core ✅ Passed HEAD^..HEAD adds no imports to core files; pyproject.toml has dependencies = []; interlock/init.py has no interlock.integrations re-export.
Changelog Entry ✅ Passed CHANGELOG.md was modified and adds multiple user-visible bullets under ## [Unreleased], including the new call APIs and integration behavior.
Docs And Llm Mirror ✅ Passed The PR adds public CircuitBreaker methods and updates docs/getting-started.md and docs/reference.md; reproduced generator output matches docs/llms-full.txt, and no new docs page was added.
Tests Accompany Behaviour Change ✅ Passed Production .py behavior changed and tests/ changed. Added HTTPX regression test patches CircuitBreaker.call; the pre-change path calls breaker(...), so it fails without the production change.
Public Api Surface ✅ Passed Diff shows no changes to interlock/init.py; exported names and public signatures in interlock/pipeline.py are unchanged. Pipeline edits only replace internal wrapper calls.
✨ 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/155-transport-hot-path

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

@codspeed-hq

codspeed-hq Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will improve performance by 35.24%

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

⚡ 5 improved benchmarks
✅ 19 untouched benchmarks
🆕 4 new benchmarks

Performance Changes

Benchmark BASE HEAD Efficiency
test_registry_get_cached 40.4 µs 26.3 µs +53.7%
test_pipeline_decorator 345 µs 248.9 µs +38.63%
test_pipeline_breaker_only 332.9 µs 255 µs +30.54%
test_pipeline_sync_success 388.1 µs 302.5 µs +28.29%
test_pipeline_sync_fallback 354.5 µs 279.6 µs +26.81%
🆕 test_baseline_unwrapped_transport N/A 291.5 µs N/A
🆕 test_baseline_unwrapped_transport_async N/A 548.4 µs N/A
🆕 test_transport_request N/A 408.4 µs N/A
🆕 test_transport_request_async N/A 707.8 µs N/A

Tip

Curious why this is faster? Comment @codspeedbot explain why this is faster on this PR, or directly use the CodSpeed MCP with your agent.


Comparing perf/155-transport-hot-path (c44f5fe) with main (90f2afd)

Open in CodSpeed

@codecov

codecov Bot commented Aug 12, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ All tests successful. No failed tests found.

📢 Thoughts on this report? Let us know!

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

🧹 Nitpick comments (1)
interlock/integrations/httpx.py (1)

234-234: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Cache transport callables outside the request path.

Each of the four request paths performs a bound-method lookup for every request, creating a transient bound-method object. Capture each transport callable during initialization and pass the cached callable to call_sync or call_async.

🤖 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 `@interlock/integrations/httpx.py` at line 234, Cache the transport callables
during initialization instead of performing bound-method lookups in each request
path. Update the four affected call sites—interlock/integrations/httpx.py lines
234-234 and 327-327, and interlock/integrations/httpx2.py lines 234-234 and
328-328—to pass the corresponding cached callable to breaker.call_sync or
breaker.call_async, preserving existing request behavior.
🤖 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.

Nitpick comments:
In `@interlock/integrations/httpx.py`:
- Line 234: Cache the transport callables during initialization instead of
performing bound-method lookups in each request path. Update the four affected
call sites—interlock/integrations/httpx.py lines 234-234 and 327-327, and
interlock/integrations/httpx2.py lines 234-234 and 328-328—to pass the
corresponding cached callable to breaker.call_sync or breaker.call_async,
preserving existing request behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 1cbed504-99ec-4689-bb4b-836870b67992

📥 Commits

Reviewing files that changed from the base of the PR and between 90f2afd and c44f5fe.

📒 Files selected for processing (19)
  • CHANGELOG.md
  • benchmarks/test_transport.py
  • docs/getting-started.md
  • docs/llms-full.txt
  • docs/reference.md
  • interlock/breaker.py
  • interlock/integrations/aiohttp.py
  • interlock/integrations/httpx.py
  • interlock/integrations/httpx2.py
  • interlock/integrations/requests.py
  • interlock/pipeline.py
  • interlock/registry.py
  • tests/test_aiohttp.py
  • tests/test_breaker.py
  • tests/test_httpx.py
  • tests/test_httpx2.py
  • tests/test_registry.py
  • tests/test_requests.py
  • tests/typing_surface.py
📜 Review details
⏰ Context from checks skipped due to timeout. (5)
  • GitHub Check: Platform smoke (macos-latest, Python 3.11)
  • GitHub Check: Platform smoke (macos-latest, Python 3.14)
  • GitHub Check: quality (3.14t)
  • GitHub Check: quality (3.13)
  • GitHub Check: Run benchmarks
⚠️ CI failures not shown inline (2)

GitHub Actions: Code scanning AI findings on PR #161 / 0_github-advanced-security.txt: Code scanning AI findings on PR #161

Conclusion: failure

View job details

##[group]Run set -euo pipefail
 �[36;1mset -euo pipefail�[0m
 �[36;1mecho "RUNNER_TEMP=$RUNNER_TEMP"�[0m
 �[36;1mfind "$RUNNER_TEMP" -maxdepth 1 -type f -name 'git-credentials-*.config' -print -delete�[0m
 �[36;1m�[0m
 �[36;1m# Generate a unique token and stop processing workflow commands to prevent the runtime from injecting commands�[0m
 �[36;1mSTOP_***REDACTED_SECRET_ASSIGNMENT*** /proc/sys/kernel/random/uuid)�[0m
 �[36;1m�[0m
 �[36;1m# Use a trap to ensure we always resume command processing and check for�[0m
 �[36;1m# fallback error annotations, even if the runtime exits with a non-zero code�[0m
 �[36;1m# (which would otherwise cause set -e to abort the shell before we get here).�[0m
 �[36;1m# The trap preserves the original exit code.�[0m
 �[36;1mcopilot_cleanup() {�[0m
 �[36;1m  �[0m
 �[36;1m  if [ -n "${GIT_PROXY_PID:-}" ] && kill -0 "$GIT_PROXY_PID" 2>/dev/null; then�[0m
 �[36;1m    echo "Stopping git-proxy (pid=$GIT_PROXY_PID)..."�[0m
 �[36;1m    kill "$GIT_PROXY_PID" 2>/dev/null || true�[0m
 �[36;1m    for _ in {1..25}; do�[0m
 �[36;1m      if ! kill -0 "$GIT_PROXY_PID" 2>/dev/null; then break; fi�[0m
 �[36;1m      sleep 0.2�[0m
 �[36;1m    done�[0m
 �[36;1m    if kill -0 "$GIT_PROXY_PID" 2>/dev/null; then�[0m
 �[36;1m      echo "git-proxy did not stop gracefully; forcing termination."�[0m
 �[36;1m      kill -KILL "$GIT_PROXY_PID" 2>/dev/null || true�[0m
 �[36;1m    fi�[0m
 �[36;1m    wait "$GIT_PROXY_PID" 2>/dev/null || true�[0m
 �[36;1m  fi�[0m
 �[36;1m  �[0m
 �[36;1m  echo "::$STOP_***REDACTED_SECRET_ASSIGNMENT***
 �[36;1m  FALLBACK_FILE="${RUNNER_TEMP}/copilot-fallback-error.txt"�[0m
 �[36;1m  if [ -f "$FALLBACK_FILE" ]; then�[0m
 �[36;1m    FALLBACK_MSG=$(head -c 500 "$FALLBACK_FILE" | tr -d '\n\r')�[0m
 �[36;1m    echo "::error title=Copilot Error::${FALLBACK_MSG}"�[0m

GitHub Actions: Code scanning AI findings on PR #161 / github-advanced-security: Code scanning AI findings on PR #161

Conclusion: failure

View job details

##[group]Run set -euo pipefail
 �[36;1mset -euo pipefail�[0m
 �[36;1mecho "RUNNER_TEMP=$RUNNER_TEMP"�[0m
 �[36;1mfind "$RUNNER_TEMP" -maxdepth 1 -type f -name 'git-credentials-*.config' -print -delete�[0m
 �[36;1m�[0m
 �[36;1m# Generate a unique token and stop processing workflow commands to prevent the runtime from injecting commands�[0m
 �[36;1mSTOP_***REDACTED_SECRET_ASSIGNMENT*** /proc/sys/kernel/random/uuid)�[0m
 �[36;1m�[0m
 �[36;1m# Use a trap to ensure we always resume command processing and check for�[0m
 �[36;1m# fallback error annotations, even if the runtime exits with a non-zero code�[0m
 �[36;1m# (which would otherwise cause set -e to abort the shell before we get here).�[0m
 �[36;1m# The trap preserves the original exit code.�[0m
 �[36;1mcopilot_cleanup() {�[0m
 �[36;1m  �[0m
 �[36;1m  if [ -n "${GIT_PROXY_PID:-}" ] && kill -0 "$GIT_PROXY_PID" 2>/dev/null; then�[0m
 �[36;1m    echo "Stopping git-proxy (pid=$GIT_PROXY_PID)..."�[0m
 �[36;1m    kill "$GIT_PROXY_PID" 2>/dev/null || true�[0m
 �[36;1m    for _ in {1..25}; do�[0m
 �[36;1m      if ! kill -0 "$GIT_PROXY_PID" 2>/dev/null; then break; fi�[0m
 �[36;1m      sleep 0.2�[0m
 �[36;1m    done�[0m
 �[36;1m    if kill -0 "$GIT_PROXY_PID" 2>/dev/null; then�[0m
 �[36;1m      echo "git-proxy did not stop gracefully; forcing termination."�[0m
 �[36;1m      kill -KILL "$GIT_PROXY_PID" 2>/dev/null || true�[0m
 �[36;1m    fi�[0m
 �[36;1m    wait "$GIT_PROXY_PID" 2>/dev/null || true�[0m
 �[36;1m  fi�[0m
 �[36;1m  �[0m
 �[36;1m  echo "::$STOP_***REDACTED_SECRET_ASSIGNMENT***
 �[36;1m  FALLBACK_FILE="${RUNNER_TEMP}/copilot-fallback-error.txt"�[0m
 �[36;1m  if [ -f "$FALLBACK_FILE" ]; then�[0m
 �[36;1m    FALLBACK_MSG=$(head -c 500 "$FALLBACK_FILE" | tr -d '\n\r')�[0m
 �[36;1m    echo "::error title=Copilot Error::${FALLBACK_MSG}"�[0m
🧰 Additional context used
📓 Path-based instructions (13)
**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

**/*.py: Support Python 3.11 and newer; use Python 3.11+ features where required.
Keep the core zero-dependency and use only the standard library; external dependencies must be isolated behind optional integrations.
Use injected Clock instances for all time access; do not call time.monotonic() or sleep() directly in logic.
Implement the core as an I/O-free state machine with a single threading.Lock around the await-free critical section, never held across the protected call.
Use Protocols for extension points: Clock, SlidingWindow, Storage, FailureClassifier, and EventListener; do not inherit from internal classes.
Expose one public CircuitBreaker class for sync and async operation, with separate internal paths selected by coroutine detection.
Expose the public API through the package __init__.py; keep helpers underscore-prefixed and hidden.
Use absolute imports, placed at the top of the file, ordered as standard library, third-party, then local imports with blank lines between groups.
Use a maximum line length of 100 characters, single-quoted strings, f-strings, and pathlib.Path instead of os.path.
Annotate every parameter and return value; use modern generic syntax and X | None instead of Optional[X].
Use StrEnum or module-level constants instead of magic constants.
When a constructor or function has three or more arguments, pass them by keyword.
Keep functions focused on one job, generally no longer than 20–30 lines, with minimal side effects and extracted repeated loop logic.
Use async/await for I/O-bound work, asyncio.TaskGroup instead of asyncio.gather, and asyncio.to_thread or ProcessPoolExecutor for CPU-bound work.
Do not mix sync and async in one function; never await a sync callable or block on an async callable.
Fail fast on invalid input or state by raising immediately; do not continue with partial results or invented defaults.
Catch only expected exceptions, log them with context, and re-raise; do not use ...

Files:

  • interlock/integrations/aiohttp.py
  • tests/typing_surface.py
  • interlock/integrations/httpx.py
  • tests/test_httpx.py
  • interlock/registry.py
  • tests/test_requests.py
  • interlock/integrations/httpx2.py
  • interlock/pipeline.py
  • interlock/breaker.py
  • interlock/integrations/requests.py
  • tests/test_httpx2.py
  • tests/test_breaker.py
  • tests/test_registry.py
  • tests/test_aiohttp.py
  • benchmarks/test_transport.py
{interlock/**/*.py,docs/**/*.md,docs/llms-full.txt,docs/llms.txt}

📄 CodeRabbit inference engine (Custom checks)

When a change affects user-facing behaviour through the public API, integrations, or configuration options, update the relevant page under docs/ and regenerate docs/llms-full.txt; when adding a new documentation page, list it under ## Docs in docs/llms.txt.

Files:

  • interlock/integrations/aiohttp.py
  • interlock/integrations/httpx.py
  • interlock/registry.py
  • docs/reference.md
  • docs/getting-started.md
  • docs/llms-full.txt
  • interlock/integrations/httpx2.py
  • interlock/pipeline.py
  • interlock/breaker.py
  • interlock/integrations/requests.py
interlock/**/*.py

📄 CodeRabbit inference engine (Custom checks)

Every production behaviour change in interlock/ must be accompanied by a change under tests/; changes limited to docstrings, comments, or type annotations are exempt. Bug fixes must include at least one regression test that fails without the production fix.

Files:

  • interlock/integrations/aiohttp.py
  • interlock/integrations/httpx.py
  • interlock/registry.py
  • interlock/integrations/httpx2.py
  • interlock/pipeline.py
  • interlock/breaker.py
  • interlock/integrations/requests.py

⚙️ CodeRabbit configuration file

Core rules (AGENTS.md is authoritative): (1) Zero-dependency core — anything under interlock/ except interlock/integrations/ may import stdlib only. Flag every third-party import as a blocking issue. (2) No fallbacks, no silent excepts, no a or b or c for required config or data, no hidden retries. Invalid input or state raises immediately. interlock/_notify.py is the one sanctioned swallow (listener hooks are observability, logged with traceback, BaseException still propagates) — do not suggest generalising or "fixing" it. (3) Time comes only from the injected Clock protocol; direct time.monotonic()/time.sleep() in library logic is a bug. (4) Style: 100-char lines, single quotes, f-strings, pathlib, full annotations, X | None never Optional[X], keyword arguments for calls with 3+ arguments, no magic constants (StrEnum or module constants), functions under ~30 lines. (5) Extension points are Protocols (Clock, SlidingWindow, Storage, FailureClassifier, EventListener) — do not propose inheriting internal classes. (6) Sync and async live in one CircuitBreaker with separate internal paths; never propose Sync*/Async* twins and never mix the two paths in one function. (7) Public API is exported from interlock/init.py; everything else is underscore-prefixed. New public symbols need __all__ and a docstring. (8) Python 3.11 is the floor — no 3.12+ syntax or stdlib.

Files:

  • interlock/integrations/aiohttp.py
  • interlock/integrations/httpx.py
  • interlock/registry.py
  • interlock/integrations/httpx2.py
  • interlock/pipeline.py
  • interlock/breaker.py
  • interlock/integrations/requests.py
interlock/integrations/**/*.py

⚙️ CodeRabbit configuration file

Optional extras. The third-party import must stay inside this package, must never be re-exported from interlock/init.py, and a missing extra must fail with a clear install hint rather than a fallback. Wrap the dependency behind the project's own types so its objects do not leak into core signatures. Check that the extra is declared in pyproject.toml [project.optional-dependencies] and documented under docs/integrations/.

Files:

  • interlock/integrations/aiohttp.py
  • interlock/integrations/httpx.py
  • interlock/integrations/httpx2.py
  • interlock/integrations/requests.py
tests/**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

tests/**/*.py: Use pytest functions rather than test classes, with names formatted as test__unit_of_work__state_under_test__expected_behavior.
Mirror package layout in test filenames, use Arrange-Act-Assert, and create fixtures for repeated setup.
Use injected Clock instances for deterministic tests; do not use sleep() in tests.
Use pytest-asyncio and @pytest.mark.asyncio for asynchronous tests, and use pytest-mock to isolate external dependencies.
Use Hypothesis property-based tests for the state machine and cover all transitions and races.
Write the reproducing test before a bug fix and specify the required behavior before implementing a feature.

Files:

  • tests/typing_surface.py
  • tests/test_httpx.py
  • tests/test_requests.py
  • tests/test_httpx2.py
  • tests/test_breaker.py
  • tests/test_registry.py
  • tests/test_aiohttp.py

⚙️ CodeRabbit configuration file

pytest functions only, never test classes. Names follow test__unit_of_work__state_under_test__expected_behavior in lower case. One behaviour per test, Arrange-Act-Assert. Time is the injected fake Clock — any real sleep or wall-clock read is flakiness, flag it. Async tests use @pytest.mark.asyncio; state-machine work carries hypothesis property tests. Coverage must stay at 100%: point out uncovered branches the diff introduces. Tests run under -n auto, so anything relying on ordering or shared global state is a bug.

Files:

  • tests/typing_surface.py
  • tests/test_httpx.py
  • tests/test_requests.py
  • tests/test_httpx2.py
  • tests/test_breaker.py
  • tests/test_registry.py
  • tests/test_aiohttp.py
{interlock/*.py,interlock/!(integrations)/**/*.py,pyproject.toml}

📄 CodeRabbit inference engine (Custom checks)

Keep the core zero-dependency: files under interlock/ outside interlock/integrations/ may import only the standard library or other interlock modules; [project] dependencies in pyproject.toml must remain empty; and interlock/__init__.py must not re-export from interlock.integrations.

Files:

  • interlock/registry.py
  • interlock/pipeline.py
  • interlock/breaker.py
**/*.md

📄 CodeRabbit inference engine (AGENTS.md)

Document user-facing changes in English Markdown documentation and keep generated documentation mirrors synchronized.

Files:

  • docs/reference.md
  • CHANGELOG.md
  • docs/getting-started.md
docs/**/*.md

⚙️ CodeRabbit configuration file

User-facing documentation. Check that code samples match the current public API and would actually run. A new page must also be listed in docs/llms.txt under ## Docs. Keep the existing voice: short sentences, no marketing.

Files:

  • docs/reference.md
  • docs/getting-started.md
CHANGELOG.md

📄 CodeRabbit inference engine (AGENTS.md)

Add every change to the [Unreleased] section under Added, Fixed, or Changed, explaining user impact rather than only symbol movement.

Files:

  • CHANGELOG.md

⚙️ CodeRabbit configuration file

Keep a Changelog format. New entries go under ## [Unreleased] in Added / Fixed / Changed. An entry describes what a user could not do before and can now, not which symbol moved. Only the release commit dates a section and updates the link references.

Files:

  • CHANGELOG.md
docs/llms-full.txt

⚙️ CodeRabbit configuration file

Generated artefact — produced by uv run python scripts/build_llms_full.py. Do not review its content or suggest edits; only confirm it was regenerated together with the docs/ changes in the same PR.

Files:

  • docs/llms-full.txt
{interlock/__init__.py,interlock/pipeline.py}

📄 CodeRabbit inference engine (Custom checks)

Do not remove or change signatures of symbols exported from interlock/__init__.py or interlock/pipeline.py—including removed or reordered positional parameters, narrowed types, or renamed public names—unless the PR has the breaking-change label and CHANGELOG.md includes a migration note. CI enforces this with uv run griffe check.

Files:

  • interlock/pipeline.py
benchmarks/**/*.py

📄 CodeRabbit inference engine (CONTRIBUTING.md)

benchmarks/**/*.py: Add a benchmark when you touch a hot path: the call paths, the state machine,
the sliding windows or the pipeline.

Files:

  • benchmarks/test_transport.py
benchmarks/**

⚙️ CodeRabbit configuration file

CodSpeed hot-path benchmarks, run only by .github/workflows/codspeed.yml and never by uv run pytest. Judge them for measurement validity (no setup inside the timed region, no dead code the optimiser can drop), not for test-suite conventions.

Files:

  • benchmarks/test_transport.py
🧠 Learnings (7)
📓 Common learnings
Learnt from: CR
Repo: bagowix/interlock PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-08-07T15:34:32.493Z
Learning: Applies to **/*.py : Expose one public `CircuitBreaker` class for sync and async operation, with separate internal paths selected by coroutine detection.
Learnt from: CR
Repo: bagowix/interlock PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-08-07T15:34:32.493Z
Learning: Applies to CHANGELOG.md : Add every change to the `[Unreleased]` section under `Added`, `Fixed`, or `Changed`, explaining user impact rather than only symbol movement.
📚 Learning: 2026-08-07T15:34:32.493Z
Learnt from: CR
Repo: bagowix/interlock PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-08-07T15:34:32.493Z
Learning: Applies to **/*.py : Expose one public `CircuitBreaker` class for sync and async operation, with separate internal paths selected by coroutine detection.

Applied to files:

  • interlock/integrations/aiohttp.py
  • tests/typing_surface.py
  • interlock/integrations/httpx.py
  • tests/test_httpx.py
  • docs/reference.md
  • CHANGELOG.md
  • docs/getting-started.md
  • docs/llms-full.txt
  • tests/test_requests.py
  • interlock/integrations/httpx2.py
  • interlock/pipeline.py
  • interlock/breaker.py
  • interlock/integrations/requests.py
  • tests/test_httpx2.py
  • tests/test_breaker.py
  • tests/test_aiohttp.py
📚 Learning: 2026-08-07T15:34:32.493Z
Learnt from: CR
Repo: bagowix/interlock PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-08-07T15:34:32.493Z
Learning: Applies to tests/**/*.py : Use `pytest-asyncio` and `pytest.mark.asyncio` for asynchronous tests, and use `pytest-mock` to isolate external dependencies.

Applied to files:

  • tests/test_httpx.py
  • tests/test_httpx2.py
  • tests/test_breaker.py
  • tests/test_aiohttp.py
📚 Learning: 2026-08-07T15:34:59.595Z
Learnt from: CR
Repo: bagowix/interlock PR: 0
File: coderabbit-custom-pre-merge-checks-unique-id-file-non-traceable-F7F2B60C-1728-4C9A-8889-4F2235E186CA.txt:0-0
Timestamp: 2026-08-07T15:34:59.595Z
Learning: Applies to {interlock/**/*.py,docs/**/*.md,docs/llms-full.txt,docs/llms.txt} : When a change affects user-facing behaviour through the public API, integrations, or configuration options, update the relevant page under `docs/` and regenerate `docs/llms-full.txt`; when adding a new documentation page, list it under `## Docs` in `docs/llms.txt`.

Applied to files:

  • docs/llms-full.txt
📚 Learning: 2026-08-07T15:34:59.595Z
Learnt from: CR
Repo: bagowix/interlock PR: 0
File: coderabbit-custom-pre-merge-checks-unique-id-file-non-traceable-F7F2B60C-1728-4C9A-8889-4F2235E186CA.txt:0-0
Timestamp: 2026-08-07T15:34:59.595Z
Learning: Applies to {interlock/__init__.py,interlock/pipeline.py} : Do not remove or change signatures of symbols exported from `interlock/__init__.py` or `interlock/pipeline.py`—including removed or reordered positional parameters, narrowed types, or renamed public names—unless the PR has the `breaking-change` label and `CHANGELOG.md` includes a migration note. CI enforces this with `uv run griffe check`.

Applied to files:

  • interlock/pipeline.py
📚 Learning: 2026-08-07T15:34:32.493Z
Learnt from: CR
Repo: bagowix/interlock PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-08-07T15:34:32.493Z
Learning: Applies to **/*.py : Do not mix sync and async in one function; never await a sync callable or block on an async callable.

Applied to files:

  • interlock/breaker.py
📚 Learning: 2026-08-07T15:34:59.595Z
Learnt from: CR
Repo: bagowix/interlock PR: 0
File: coderabbit-custom-pre-merge-checks-unique-id-file-non-traceable-F7F2B60C-1728-4C9A-8889-4F2235E186CA.txt:0-0
Timestamp: 2026-08-07T15:34:59.595Z
Learning: Applies to interlock/**/*.py : Every production behaviour change in `interlock/` must be accompanied by a change under `tests/`; changes limited to docstrings, comments, or type annotations are exempt. Bug fixes must include at least one regression test that fails without the production fix.

Applied to files:

  • tests/test_breaker.py
  • tests/test_registry.py

@bagowix
bagowix merged commit 34d4fa7 into main Aug 12, 2026
21 of 22 checks passed
@bagowix
bagowix deleted the perf/155-transport-hot-path branch August 12, 2026 16:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Transport integrations rebuild the guarded wrapper on every request

1 participant