Skip to content

Implement libhv-based asyncio event loop (M1–M4)#1

Merged
xiispace merged 11 commits into
mainfrom
codex/refactor-native-io
Jul 2, 2026
Merged

Implement libhv-based asyncio event loop (M1–M4)#1
xiispace merged 11 commits into
mainfrom
codex/refactor-native-io

Conversation

@xiispace

@xiispace xiispace commented Jul 2, 2026

Copy link
Copy Markdown
Owner

Rewrites hvloop as a single-compilation-unit Cython extension over libhv's event
engine, targeting web-server workloads (FastAPI/ASGI under uvicorn, WebSocket,
TLS). Runs on Linux (epoll), macOS (kqueue), Windows (wepoll).

Milestones

Per docs/plans/2026-06-13-hvloop-tech-design.md:

  • M1 — self-driven loop (no hloop_run), scheduling, timers, executors,
    getaddrinfo/getnameinfo, exception handling, lifecycle.
  • M2TCPTransport, create_connection/create_server, Server,
    add_reader/add_writer, Unix signal handling.
  • M3 — uvicorn + FastAPI HTTP/WebSocket integration, graceful shutdown.
  • M4 — TLS via stdlib asyncio.sslproto, sock_* family, cibuildwheel CI,
    benchmarks.

Status

  • 149 tests passing locally (macOS arm64, Python 3.14).
  • CLAUDE.md documents the architecture and the core invariants (loop status
    forcing, wakeup-fd creation, reference discipline, fd ownership, shared-readbuf
    copy).

Validation this PR's CI is meant to close

Everything above was verified only on macOS arm64 / py3.14 locally. This PR
exercises the parts that could not be checked on that machine:

  • Windows (wepoll backend, signal/sock_*/add_reader win32 gating) — code
    self-checked only, never run on real hardware.
  • Linux (epoll) and the Python 3.10 sslproto-gated path.
  • The cibuildwheel matrix (manylinux2014/musllinux x86_64, linux aarch64,
    macOS arm64+x86_64, Windows AMD64; cp310–314).

Not in scope (see plan / README)

UDP (create_datagram_endpoint), subprocess and pipe transports, native
sendfile.

Summary by CodeRabbit

  • New Features
    • Introduced an asyncio-compatible event loop with new_event_loop, install/uninstall, and run.
    • Added a runnable FastAPI example and HTTP/TCP benchmarking scripts.
  • Documentation
    • Reworked the README with updated install/usage, supported transports, and integration guidance.
    • Added design notes, repo guidance, and TLS test certificate documentation.
  • Tests
    • Added/expanded end-to-end ASGI, loop core, TCP, TLS, socket, fd watching, Unix signals, and interoperability test suites.
  • Chores
    • Overhauled CI: full test matrix, cross-platform wheel builds, sdist smoke test, and trusted publishing.

john and others added 7 commits October 4, 2025 12:08
- Updated to latest stable version v1.3.3
- Includes performance improvements and bug fixes
- Enhanced cmake configuration with better platform support
- Added new features: MQTT support, improved HTTP handling

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
- Add proper imports in .pxd files (htimer_t, hio_t, Loop, Server)
- Remove nested cdef: blocks in with gil statements
- Add _wakeup method declaration in loop.pxd
- Remove hv. prefix from types in loop.pyx (use direct imports)
…ore 0.10

Breaking changes:
- Drop support for Python 3.8 and 3.9
- Add support for Python 3.13

Build system upgrades:
- cython: >=3.0.0 -> >=3.1.0
- scikit-build-core: >=0.5.0 -> >=0.10.0
- CMake: 3.21 -> 3.25
- pytest: >=7.0 -> >=8.0

Version bump: 0.1.0 -> 0.2.0
Rewrite hvloop as a single-compilation-unit Cython extension over libhv's
event engine, targeting web-server workloads (FastAPI/ASGI under uvicorn,
WebSocket, TLS). Runs on Linux (epoll), macOS (kqueue), Windows (wepoll).

Milestones from docs/plans/2026-06-13-hvloop-tech-design.md:
- M1: self-driven loop (no hloop_run), scheduling, timers, executors,
  getaddrinfo/getnameinfo, exception handling, lifecycle.
- M2: TCPTransport, create_connection/create_server, Server,
  add_reader/add_writer, Unix signal handling.
- M3: uvicorn + FastAPI HTTP/WebSocket integration, graceful shutdown.
- M4: TLS via stdlib asyncio.sslproto, sock_* family, cibuildwheel CI,
  benchmarks.

149 tests passing. See CLAUDE.md for architecture and core invariants
(loop status forcing, wakeup-fd creation, reference discipline, fd
ownership, shared readbuf copy).
@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f669a372-f68a-4fb9-b5e0-1a56cf03b587

📥 Commits

Reviewing files that changed from the base of the PR and between 7ff1233 and 3265bed.

📒 Files selected for processing (4)
  • .github/workflows/build.yml
  • Makefile
  • src/hvloop/hvloop_shim.h
  • tests/test_smoke.py
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/hvloop/hvloop_shim.h
  • .github/workflows/build.yml
  • tests/test_smoke.py

📝 Walkthrough

Walkthrough

hvloop is rewritten around a native Cython/libhv event loop with a CMake/scikit-build build, new public asyncio wrappers, refreshed docs/examples, and expanded tests covering loop behavior, sockets, TCP, TLS, and ASGI integration.

Changes

Build, packaging, and release

Layer / File(s) Summary
Build and release pipeline
pyproject.toml, CMakeLists.txt, Makefile, .github/workflows/build.yml, .gitignore, vendor/libhv, requirements.dev.txt, setup.py, MANIFEST.in
Adds project metadata, native build wiring, CI matrix jobs, trusted-release publishing, developer targets, vendored libhv pin, and packaging cleanup.
Native headers and shim
src/hvloop/hvloop_shim.h, src/hvloop/includes/hv.pxd, src/hvloop/includes/python.pxd
Adds the Cython/libhv declarations and inline helpers needed by the native loop core.

Public API, docs, and examples

Layer / File(s) Summary
Python API, docs, and examples
src/hvloop/__init__.py, CLAUDE.md, docs/plans/2026-06-13-hvloop-tech-design.md, README.md, examples/fastapi_app.py, benchmarks/bench_http.py, benchmarks/bench_tcp_echo.py
Introduces the new asyncio-facing Python API and updates the contributor guide, design doc, README, example app, and benchmark scripts.

Core behavior tests

Layer / File(s) Summary
Loop and signal tests
tests/conftest.py, tests/test_loop.py, tests/test_fd_signal.py, tests/test_smoke.py
Adds the shared loop fixture and tests for scheduling, shutdown, exception routing, fd watchers, signal handling, and basic asyncio compatibility.

Transport and socket tests

Layer / File(s) Summary
Socket and TCP transport tests
tests/test_sock.py, tests/test_tcp.py
Covers sock_* APIs plus TCP connection, server, backpressure, EOF, fd ownership, and transport lifecycle behavior.

ASGI and TLS integration

Layer / File(s) Summary
ASGI and TLS integration tests
tests/test_asgi.py, tests/test_tls.py, tests/certs/*
Adds end-to-end uvicorn/FastAPI coverage, TLS handshake and HTTPS tests, and committed test certificate material.

Estimated code review effort: 5 (Critical) | ~120 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the core change: implementing the libhv-based asyncio event loop across M1–M4.
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 codex/refactor-native-io

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

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🧹 Nitpick comments (1)
tests/certs/server.key (1)

1-29: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Consider generating the test key/cert at test-collection time instead of committing it.

The static-analysis hint correctly identifies a private key, but per tests/certs/README.md this is intentionally a long-lived, non-sensitive, self-signed test fixture, so it's not a real leak. That said, committing a raw PEM private key still risks tripping repo-wide/CI secret scanners (e.g. GitHub secret scanning) even though it's benign, generating alert noise or false "leaked credential" incidents down the line.

Since the README's stated goal is avoiding an openssl CLI dependency at test time, an alternative that keeps both properties is to generate the self-signed cert/key in a conftest.py/session-scoped fixture using the cryptography package (pure Python, no CLI, cacheable to a tmp dir for the test session) rather than committing the key material to the repo.

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

In `@tests/certs/server.key` around lines 1 - 29, Replace the committed PEM
fixture with runtime generation in the test setup, since the issue is the static
private key in the cert fixture. Update the test certificate flow so a
session-scoped fixture or conftest-generated helper creates the self-signed
key/cert on demand using the existing test helpers referenced by
tests/certs/README.md, preserving the no-openssl requirement while avoiding
checked-in private key material. Ensure the generated artifacts are cached to a
temp location for the test session and that any tests currently reading
server.key/server cert use the new generator path.

Source: Linters/SAST tools

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

Inline comments:
In @.github/workflows/build.yml:
- Line 1: The Build and Test workflow is using default GitHub token permissions
and persists checkout credentials in multiple jobs, so tighten the workflow
security settings. Add least-privilege permissions at the workflow or job level
for the affected jobs, and update each checkout step in the build/test job
sections to disable persisted credentials. Apply the change consistently across
the jobs referenced in the workflow so all checkouts use the hardened
configuration.
- Around line 63-65: The macOS Intel runner label in the build workflow is
invalid, which can break the wheel job before it starts. Update the
`macos-x86_64` job in the workflow to use a currently supported Intel macOS
runner label, and keep the `archs: x86_64` setting unchanged so the job still
targets Intel builds.

In `@Makefile`:
- Around line 1-2: The Makefile’s .PHONY declaration is missing the ci target,
so add ci to the existing phony list alongside help, install, test, lint, and
the other targets. Update the .PHONY entry that defines the build/tooling
targets so make ci is always treated as a command and cannot be shadowed by a
real ci file or directory.
- Around line 27-29: The default SKBUILD_PROJECT_VERSION is outdated and should
match the version declared elsewhere. Update the Makefile’s
SKBUILD_PROJECT_VERSION default from 0.1.0 to 0.2.0 so dev builds stay aligned
with the package version used by pyproject.toml and the existing build setup.

In `@src/hvloop/hvloop_shim.h`:
- Around line 91-116: The hvloop_sockaddr_info helper returns a non-negative
family for unsupported sockaddr types without populating the ip buffer, which
can leave stale data in callers; update the non-AF_INET/AF_INET6 path in
hvloop_sockaddr_info to ensure ip is set to a safe empty value (and keep
port/flowinfo/scope_id initialized) before returning the family so
get_extra_info callers never read uninitialized address text.

In `@tests/test_smoke.py`:
- Around line 89-94: The task created in the smoke test is not retained, so it
may be garbage-collected before `setter()` completes and calls `ev.set()`.
Update the `asyncio.ensure_future(setter())` usage in the test to keep a strong
reference to the created task and await or otherwise manage it explicitly, using
the `setter` helper and the surrounding event-wait logic to locate the change.

---

Nitpick comments:
In `@tests/certs/server.key`:
- Around line 1-29: Replace the committed PEM fixture with runtime generation in
the test setup, since the issue is the static private key in the cert fixture.
Update the test certificate flow so a session-scoped fixture or
conftest-generated helper creates the self-signed key/cert on demand using the
existing test helpers referenced by tests/certs/README.md, preserving the
no-openssl requirement while avoiding checked-in private key material. Ensure
the generated artifacts are cached to a temp location for the test session and
that any tests currently reading server.key/server cert use the new generator
path.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: bf54d419-5fd6-44ce-b10f-8fd7e53d85d2

📥 Commits

Reviewing files that changed from the base of the PR and between b698d45 and 6486d28.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (48)
  • .github/workflows/build.yml
  • .gitignore
  • CLAUDE.md
  • CMakeLists.txt
  • MANIFEST.in
  • Makefile
  • README.md
  • benchmarks/bench_http.py
  • benchmarks/bench_tcp_echo.py
  • docs/plans/2026-06-13-hvloop-tech-design.md
  • examples/fastapi_app.py
  • hvloop/__init__.py
  • hvloop/handle.pxd
  • hvloop/handle.pyx
  • hvloop/includes/hv.pxd
  • hvloop/includes/python.pxd
  • hvloop/loop.pxd
  • hvloop/loop.pyx
  • hvloop/requests.pyx
  • hvloop/server.pxd
  • hvloop/server.pyx
  • hvloop/transport.pxd
  • hvloop/transport.pyx
  • pyproject.toml
  • requirements.dev.txt
  • setup.py
  • src/hvloop/__init__.py
  • src/hvloop/_core.pyx
  • src/hvloop/hvloop_shim.h
  • src/hvloop/includes/__init__.py
  • src/hvloop/includes/hv.pxd
  • src/hvloop/includes/python.pxd
  • tests/__init__.py
  • tests/certs/README.md
  • tests/certs/server.crt
  • tests/certs/server.key
  • tests/conftest.py
  • tests/test_asgi.py
  • tests/test_create_connection.py
  • tests/test_create_datagram_endpoint.py
  • tests/test_create_server.py
  • tests/test_fd_signal.py
  • tests/test_loop.py
  • tests/test_smoke.py
  • tests/test_sock.py
  • tests/test_tcp.py
  • tests/test_tls.py
  • vendor/libhv
💤 Files with no reviewable changes (18)
  • MANIFEST.in
  • requirements.dev.txt
  • tests/test_create_datagram_endpoint.py
  • hvloop/server.pxd
  • hvloop/loop.pxd
  • hvloop/transport.pxd
  • hvloop/init.py
  • tests/test_create_server.py
  • hvloop/handle.pxd
  • hvloop/transport.pyx
  • setup.py
  • tests/test_create_connection.py
  • hvloop/handle.pyx
  • hvloop/requests.pyx
  • hvloop/includes/python.pxd
  • hvloop/includes/hv.pxd
  • hvloop/server.pyx
  • hvloop/loop.pyx

Comment thread .github/workflows/build.yml
Comment thread .github/workflows/build.yml
Comment thread Makefile Outdated
Comment thread Makefile Outdated
Comment thread src/hvloop/hvloop_shim.h
Comment thread tests/test_smoke.py
xiispace added 4 commits July 2, 2026 13:19
…, 3.10 test)

Three distinct failures surfaced only under CI (local dev was macOS/py3.14):

- Linux: static libhv linked into the _core shared object needs
  position-independent code, else "ld: final link failed: bad value".
  Set CMAKE_POSITION_INDEPENDENT_CODE ON before add_subdirectory(vendor/libhv).
- Windows: the cython-cmake fallback prepended a backslash Windows path to
  CMAKE_MODULE_PATH, which broke the compiler-ABI try_compile (re-triggered by
  libhv's project()) with "Syntax error in cmake code ... (set)". Normalize
  with file(TO_CMAKE_PATH).
- Python 3.10: loop.create_task(context=...) is 3.11+ (asyncio parity; our impl
  correctly raises on 3.10). Gate test_create_task_with_context with skipif.
…trol

Windows CI now builds and runs; 6 remaining failures were test-level:

- os.fstat() doesn't work on Windows socket handles (not CRT fds); it raised
  WinError 6 in 4 fd-ownership tests. Replace with sock.getsockname() as the
  portable "still open" check (liveness is also proven by the send/recv that
  follows).
- Two write flow-control tests assume a large write to a paused peer stays
  queued (get_write_buffer_size() > 0); on Windows loopback the payload is
  absorbed by larger default socket buffers so the buffer reads 0. Mark them
  xfail(strict=False) on win32 and document the unverified Windows backpressure
  behaviour in CLAUDE.md as an open question for a Windows maintainer.

Linux and macOS remain fully green; local (macOS/3.14) suite unaffected.
The macos-13 (Intel) runners are retired: the Wheels: macos-x86_64 job sat in
'queued' forever across three consecutive runs (no runner ever picked it up).
Build x86_64 wheels on macos-latest (arm64) instead — cibuildwheel
cross-compiles and runs the wheel tests under Rosetta 2.

Also add a concurrency group so superseded runs are cancelled instead of
piling up behind a stuck queued job.
- ci: least-privilege workflow permissions (contents: read) and
  persist-credentials: false on all checkouts.
- Makefile: add ci to .PHONY, sync default SKBUILD_PROJECT_VERSION to 0.2.0,
  and fix the stale extension target name (loop -> _core) in build-ext/help.
- shim: hvloop_sockaddr_info now null-terminates the ip buffer up front so
  non-INET/INET6 families can't leak stale stack bytes to a caller that only
  checks for -1.
- tests: hold a strong reference to the ensure_future task in the smoke test
  (loops track tasks weakly; RUF006).
@xiispace
xiispace merged commit 444514d into main Jul 2, 2026
14 checks passed
@xiispace
xiispace deleted the codex/refactor-native-io branch July 2, 2026 08:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant