Skip to content

feat!: Add relic adapter based zig std.http and zio - #390

Merged
nielsenko merged 32 commits into
serverpod:mainfrom
nielsenko:feat-relic-native
Oct 21, 2026
Merged

nielsenko merged 32 commits into
serverpod:mainfrom
nielsenko:feat-relic-native

Conversation

@nielsenko

@nielsenko nielsenko commented Oct 5, 2026 •

Copy link
Copy Markdown
Collaborator

Adds relic_native (an HTTP adapter backed by zio and bits from zig's std.http), and relic_headers (a headers abstraction that is more amenable for native inter-op than<String, List<String>>{}

An adapter now hands over AdapterExchange objects, Headers reads through a HeaderStore, and the core frames responses and WebSockets for any adapter.

The PR uses a tweaked version of zio that allows a foreign thread to drive the event loop (to avoid a thread jump over FFI). These changes have not been upstreamed yet.

Fixes: #316

Breaking: Adapter.requests and AdapterRequest are gone, and Headers is no longer a Map.

@nielsenko nielsenko self-assigned this Oct 5, 2026
@coderabbitai

coderabbitai Bot commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

Important

Review skipped

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

⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: b85f47a3-21f0-4a21-97ef-97166d2c9f47

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

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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

@nielsenko
nielsenko force-pushed the feat-relic-native branch 8 times, most recently from addc698 to 8e30e71 Compare October 9, 2026 01:49
@codecov

codecov Bot commented Oct 9, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.27935% with 77 lines in your changes missing coverage. Please review.
✅ Project coverage is 92.15%. Comparing base (f91592b) to head (d672b6f).

Files with missing lines Patch % Lines
...lic_core/lib/src/web_socket/framed_web_socket.dart 79.01% 17 Missing ⚠️
packages/relic_core/lib/src/headers/headers.dart 85.93% 9 Missing ⚠️
...es/relic_core/lib/src/headers/header_accessor.dart 66.66% 8 Missing ⚠️
packages/relic_core/lib/src/relic_server.dart 94.11% 7 Missing ⚠️
...es/relic_core/lib/src/headers/mutable_headers.dart 81.48% 5 Missing ⚠️
...core/lib/src/web_socket/relic_web_socket_base.dart 94.87% 4 Missing ⚠️
packages/relic_core/lib/src/context/request.dart 86.95% 3 Missing ⚠️
.../relic_core/lib/src/headers/byte_header_store.dart 96.05% 3 Missing ⚠️
...elic_core/lib/src/web_socket/web_socket_frame.dart 98.31% 3 Missing ⚠️
packages/relic_core/lib/src/adapter/adapter.dart 81.81% 2 Missing ⚠️
... and 11 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #390      +/-   ##
==========================================
- Coverage   92.50%   92.15%   -0.36%     
==========================================
  Files         117      126       +9     
  Lines        5260     6093     +833     
  Branches     2634     3037     +403     
==========================================
+ Hits         4866     5615     +749     
- Misses        394      478      +84     
Flag Coverage Δ
relic_core 92.58% <94.27%> (+<0.01%) ⬆️
relic_io 88.21% <ø> (-3.75%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@nielsenko

nielsenko commented Oct 9, 2026 •

Copy link
Copy Markdown
Collaborator Author

Some comparative performance numbers from my trusty old 2,4 GHz 8-Core Intel Core i9 macbook pro (2019).
This is for hello. That is a GET request towards root, with a plain text response ("Hello"), but we do use the trie based router, and do not pre-cook the reply.

server rps p50 ms p99 ms server cpu user us sys us total us
relic_native, 1 isolate 97644 1.26 1.80 100% 4.0 6.3 10.3
relic_native, 2 isolates 138311 0.91 1.42 199% 5.1 9.3 14.4
relic_native, 4 isolates 143083 0.83 1.27 358% 10.4 14.6 25.0
relic_io, 1 isolate 18404 6.71 9.66 137% 48.8 25.7 74.5
relic_io, 4 isolates 41288 3.08 4.27 449% 74.2 34.6 108.8
shelf on dart:io, 1 isolate 10608 11.84 15.87 126% 85.8 32.8 118.6
shelf on dart:io, 4 isolates 30383 4.11 6.13 444% 107.1 39.0 146.2
hyper 1.7, 1 thread 115200 1.08 1.57 100% 2.9 5.7 8.7
hyper, 4 threads 158985 0.79 1.09 330% 7.5 13.3 20.7
deno 2.9 Deno.serve 107483 1.16 1.65 100% 3.5 5.8 9.3
deno 2.9 deno serve --parallel, 4 threads 165521 0.76 1.03 384% 11.0 12.2 23.2
go 1.27 net/http, GOMAXPROCS=1 53160 2.36 3.93 100% 11.8 7.0 18.9
go 1.27 net/http, GOMAXPROCS=4 125166 0.92 2.78 389% 19.8 11.3 31.1
node 26 http, 1 process 31224 3.84 7.27 101% 23.9 8.6 32.5

@nielsenko
nielsenko requested a review from SandPod October 9, 2026 07:28
@nielsenko
nielsenko force-pushed the feat-relic-native branch 7 times, most recently from 14c4386 to b9aefcb Compare October 13, 2026 12:12
- prefer_final_parameters is off, trailing_commas is automate
- doc/site/versioned_docs is excluded
- dart fix adds const, braces, @OverRide and sorted directives in
  relic_core, relic_io and benchmark
- The examples ignore avoid_print, and packages/examples declares http
  and web_socket
- HeaderName with the interned standard names, generated by
  tool/generate_header_names.dart from tool/header_names.txt
- HeaderStore with names and values abstract, MutableHeaderStore with
  set, add, remove and clear, and MapHeaderStore as the reference store
- ByteHeaderSource for a store that keeps the wire bytes of its fields
- Adapter.start takes an ExchangeSink. AdapterExchange carries respond,
  hijack, abort, cancelled and done, and the core guards every call
- The adapter conformance suite moves into test_utils and runs on
  relic_io, and relic_server_ssl_test.dart holds the TLS tests

BREAKING CHANGE: Adapter.requests and AdapterRequest are gone. An
adapter implements start(ExchangeSink) and hands over AdapterExchange
objects.
- Headers reads through accessors over a HeaderStore, ByteHeaderStore
  decodes an indexed head as a ByteHeaderSource, and int headers decode
  from the wire bytes
- Headers and MutableHeaders share one read path, Headers.entries is a
  deprecated extension, and an empty typed value clears the header
- RequestTarget keeps the path and query as bytes, and Body.fromBytes
  keeps its bytes

BREAKING CHANGE: Headers is no longer a Map. Read it with accessors or
operator [], and write it through Headers.build or transform.
- WebSocketFrameDecoder, WebSocketMessageAssembler and FramedWebSocket
  implement RFC 6455 over a StreamChannel of bytes, with a 16 MiB
  message limit, pings, a bounded flush and a bounded close handshake
- A missed pong closes the socket at once, and a frame over the message
  limit is refused before its payload
- Hijack gives the handler a StreamChannel<Uint8List>
- A WebSocketUpgrade on an adapter without its own upgrade runs the
  handshake and a FramedWebSocket over that channel
- RelicServer checks the handshake before any adapter upgrade runs and
  hands the accept key to AdapterExchange.upgradeWebSocket
- ExchangeEndState records how an exchange ended and backs done
- Request.cancelled completes when the peer goes away
- generate_header_names.dart writes relic_native/src/header_names.zig
- The Zig table holds the ids, names and a case-folded lookup
- HeaderSlot carries name_id, from header_names.zig or -1, in place of
  name_len
- parseHead switches on the id for the framing fields, answers 400 to
  Transfer-Encoding beside Content-Length, and reads Connection as a
  token list
- ByteHeaderStore matches a known name by id through HeaderName.known
  and interns an unknown one up to the colon
- Remove HeaderName.lookupBytes and _eqBytes from header_name.dart
@nielsenko
nielsenko marked this pull request as ready for review October 15, 2026 13:12
@nielsenko

Copy link
Copy Markdown
Collaborator Author

Commits with important interface additions / changes

Hash Contract
b782db0c7 Adapter, AdapterExchange, ExchangeSink, ExchangeEnd, Listener
83866f264 HeaderName, HeaderStore, MutableHeaderStore, MapHeaderStore
6c81850d7 Headers over a store, ByteHeaderStore, Request from target and authority
e1a5ab53f ResponseFraming, Body.ofRequest, typed Body reads
cbce22475 Hijack as StreamChannel<Uint8List>
ad3060045, 5af3d14ac RelicWebSocket, FramedWebSocket, RelicWebSocketBase
980645200 NativeAdapter.bind, NativeOptions

@nielsenko
nielsenko requested review from a team and FXschwartz October 19, 2026 09:57

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

Interactive review using Claude Code to inspect the code.

Focused mostly on the interface and behavioral changes.
Trust that our test coverage handles the implementation details.

Comment thread CHANGELOG.md Outdated
Comment thread CHANGELOG.md Outdated
Comment thread packages/relic_core/lib/src/context/request.dart
Comment thread packages/relic_core/lib/src/accessor/accessor.dart
Comment thread packages/relic_core/lib/src/middleware/context_property.dart
Comment thread packages/relic_core/lib/src/adapter/adapter.dart
Comment thread packages/relic_core/lib/src/headers/headers.dart
Comment thread packages/relic_core/lib/src/headers/mutable_headers.dart
Comment thread packages/relic_core/lib/src/relic_server.dart Outdated
Comment thread packages/relic_core/lib/src/adapter/response_framing.dart
- IOAdapter.close(force: true) completes a graceful close still waiting
- NativeAdapter.close stops the reactor before it waits for a drain turn
- The conformance suite covers a forced close during a graceful one and
  a hijacked peer that never drains

BREAKING CHANGE: RelicServer.close() waits with no deadline. Bound it
with close().timeout(grace, onTimeout: () => close(force: true)).
- Body.of takes a ContentTypeHeader and bytes or a stream
- Headers.contentType is a public accessor
- IOAdapter and NativeAdapter read the request Content-Type with tryGet

BREAKING CHANGE: Body.ofRequest(headers, ...) is
Body.of(headers.tryGet(Headers.contentType), ...).
- _createFileBody takes no isHeadRequest
- The Unreleased section of the root, relic_core, relic_io and relic
  CHANGELOGs
@nielsenko

nielsenko commented Oct 20, 2026 •

Copy link
Copy Markdown
Collaborator Author

Despite all the breaking changes, both serverpod 4.0.4 and 4.1.0-beta.3 works with this branch. Tested with dart analyze and by running selected tests.

Tag analyze serverpod unit tests HTTP and websocket integration tests
4.0.4 clean, 2 infos 312 pass 286 pass, 2 skipped
4.1.0-beta.3 clean, 2 infos 330 pass 289 pass, 2 skipped

The info's are down to the deprecation of Headers.entries that exists exactly not to break serverpod.

Tested on macos and linux (via docker) - not windows.

@nielsenko

Copy link
Copy Markdown
Collaborator Author

@coderabbitai Create an issue to upstream zio changes

@coderabbitai

coderabbitai Bot commented Oct 20, 2026

Copy link
Copy Markdown
Contributor

Tip

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n -C 2 'zio|foreign thread' --glob '!*.lock' --glob '!**/vendor/**' --glob '!**/vendored/**' . | head -240
gh api repos/serverpod/relic/issues/390/comments --paginate --jq '.[] | select(.user.login == "nielsenko") | {html_url, body}'

Length of output: 22443


🏁 Script executed:

#!/bin/bash
set -euo pipefail
gh issue create \
  --repo serverpod/relic \
  --title "Upstream the zio changes used by relic_native" \
  --assignee nielsenko \
  --body "$(cat <<'EOF'
## Summary

Upstream the zio changes introduced for `relic_native` in PR #390, then migrate Relic to an upstream zio revision that includes the required support.

The adapter currently uses a modified zio version so a foreign thread can drive the event loop. This lets the Dart isolate drive its reactor without an extra thread handoff across FFI. These changes have not yet been upstreamed, as described in the PR.

## Required work

- Inventory the local zio changes against the upstream base and separate the reusable changes from Relic-specific integration.
- Propose and submit the required changes to [zio/zio](https://github.com/zio/zio). Document the externally driven event-loop API, ownership rules, and thread-safety constraints.
- Address thread migration: Dart can move an isolate between VM threads. The native build documents that an io_uring ring configured with `SINGLE_ISSUER` cannot be used this way. Include the relevant configuration support in the upstream work, if it is not already available.
- Add upstream tests or examples for externally driven execution, wakeups, shutdown, and sequential driving from different OS threads where supported.
- Once the changes are available upstream, update Relic's zio dependency and adapt the integration to the accepted upstream API.

## Affected areas

- `packages/relic_native/src/relic_native.zig`: per-isolate reactors and the Dart-driven zio runtime integration.
- `packages/relic_native/build.zig`: zio dependency configuration, including the io_uring thread constraint.
- The zio dependency declaration and pinned revision for `relic_native`.

## Acceptance criteria

- [ ] The required zio changes are inventoried and linked to upstream submissions.
- [ ] Upstream includes the support required by Relic, with documented execution and threading contracts.
- [ ] Relic uses an upstream revision without requiring the current private modifications.
- [ ] Native HTTP and WebSocket integration tests pass on macOS and Linux.
- [ ] Windows support is verified separately, or its limitations are explicitly documented.

## Context

- Source PR: https://github.com/serverpod/relic/pull/390
- Request: https://github.com/serverpod/relic/pull/390#issuecomment-6290596864
- Requested by: @nielsenko
EOF
)"

Length of output: 2638


@nielsenko Created #394: Upstream the zio changes used by relic_native and assigned it to you.

The issue covers upstream submissions, threading constraints, tests, and migration to an upstream zio revision.

You are interacting with an AI system.

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

Given the last comment is added, Relic is ready for LIGHTSPEED 🚀

- Add a conformance test that a hijacked channel starts with the unread
  body of a request with a Content-Length
@nielsenko nielsenko changed the title feat: Add relic adapter based zig std.http and zio feat!: Add relic adapter based zig std.http and zio Oct 21, 2026
@nielsenko
nielsenko enabled auto-merge (squash) October 21, 2026 13:19
@nielsenko
nielsenko disabled auto-merge October 21, 2026 13:21
@nielsenko
nielsenko merged commit 4cf3995 into serverpod:main Oct 21, 2026
28 checks passed
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.

[Performance] Transition to io_uring ,from dart:io ,only on Linux

2 participants