Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 41 additions & 12 deletions scripts/docs-symbol-citations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,18 +37,6 @@ export function continuationLines(text: string): number[] {
}

const ALLOW: Record<string, number> = {
"binding-and-validation.md": 1,
"context-api-reference.md": 1,
"engines.md": 20,
"middleware-auth.md": 1,
"middleware-content.md": 25,
"middleware.md": 21,
"net-http-interop.md": 16,
"observability.md": 31,
"routing.md": 28,
"sse.md": 26,
"streaming.md": 15,
"testing.md": 17,
};

function pages(): string[] {
Expand Down Expand Up @@ -105,3 +93,44 @@ test("the continuation detector leaves listen addresses and plain prose alone",
expect(continuationLines(lonely)).toEqual([]);
for (const text of clean) expect(continuationLines(text)).toEqual([]);
});

// Item 10: a celeris source file is always named with its celeris/ prefix and
// its full repository path. Two path-only shapes slip past the locator count:
// a path-qualified file without the prefix (`swagger/config.go`, also the
// shorthand `etag/config.go` after a full path) and a bare parenthesised file
// name ((server.go)).
const UNPREFIXED = /(?<=[`( ])(?!celeris\/)[a-z][\w-]*\/[\w./-]+\.go\b|\([a-z_]+\.go\)/;

export function unprefixedLines(text: string): number[] {
const hits: number[] = [];
text.split("\n").forEach((line, i) => {
if (UNPREFIXED.test(line)) hits.push(i + 1);
});
return hits;
}

test("no page names a celeris source file without the celeris/ prefix", () => {
const hits: string[] = [];
for (const f of pages()) {
for (const n of unprefixedLines(readFileSync(join(DOCS, f), "utf8"))) hits.push(`${f}:${n}`);
}
expect(hits).toEqual([]);
});

test("the unprefixed-path detector flags both shapes and spares prefixed paths", () => {
for (const text of [
"Source: `swagger/config.go`, `swagger/swagger.go`.",
"lives in `middleware/store/kv.go`.",
"(`driver/redis/options.go`):",
"`celeris.New(cfg)` (server.go) only allocates",
]) {
expect(unprefixedLines(text)).not.toEqual([]);
}
for (const text of [
"Source: `celeris/middleware/swagger/config.go` (`Config`).",
"Create a `main.go`:",
"`celeris.New(cfg)` (`celeris/server.go`) only allocates",
]) {
expect(unprefixedLines(text)).toEqual([]);
}
});
2 changes: 1 addition & 1 deletion src/content/docs/binding-and-validation.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ or validating input. Use `c.Bind*` plus your own field checks, as shown above.
**The package is EXPERIMENTAL.** Its package documentation says so, and it is
outside the compatibility promise in the Compatibility section of celeris's
[GOVERNANCE.md](https://github.com/goceleris/celeris/blob/main/GOVERNANCE.md#compatibility)
(`celeris/validation/doc.go:22-31`): the counter names, the `Counters` fields, the
(`celeris/validation/doc.go`): the counter names, the `Counters` fields, the
socket's JSON and the functions may change or go away in a minor release, and the
release notes say when they do.

Expand Down
2 changes: 1 addition & 1 deletion src/content/docs/context-api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -453,7 +453,7 @@ func handler(c *celeris.Context) error {

## The `Cookie` type

`SetCookie` takes a `*celeris.Cookie` (`celeris/types.go:48-71`):
`SetCookie` takes a `*celeris.Cookie` (`celeris/types.go` (`Cookie`)):

| Field | Type | Notes |
| ----- | ---- | ----- |
Expand Down
18 changes: 9 additions & 9 deletions src/content/docs/core-concepts.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ func main() {

### `New` is cheap and non-blocking

`celeris.New(cfg)` (server.go) only allocates the `Server` struct, creates the
`celeris.New(cfg)` (`celeris/server.go`) only allocates the `Server` struct, creates the
router, seeds the per-route async default from `Config.AsyncHandlers`, and — unless
`Config.DisableMetrics` is set — eagerly constructs the metrics collector so that
`Server.Collector()` returns non-nil before `Start`. **It does not bind a socket,
Expand All @@ -65,7 +65,7 @@ constructs the CPU monitor, and installs the engine. Because it runs exactly
once:

- The **second** call to any `Start*` method returns `celeris.ErrAlreadyStarted`
(server.go). A `Server` is single-use; you cannot restart it after `Shutdown`.
(`celeris/server.go`). A `Server` is single-use; you cannot restart it after `Shutdown`.
- Configuration validation errors and engine-initialization errors surface from
the **first** `Start*` call, not from `New`.

Expand Down Expand Up @@ -120,7 +120,7 @@ See [Configuration](/docs/configuration) for the full `Config` reference and
## 2. The handler contract

Every handler and every piece of middleware in Celeris has the **same** type
(types.go):
(`celeris/types.go`):

```go
type HandlerFunc func(*Context) error
Expand Down Expand Up @@ -170,7 +170,7 @@ You install global middleware with `Server.Use`, per-group middleware with
## 3. The Context is pooled

The `*Context` your handler receives is **obtained from a `sync.Pool`**
(context.go) and is reset and returned to that pool after your handler chain
(`celeris/context.go`) and is reset and returned to that pool after your handler chain
completes. This is central to Celeris's allocation profile, and it imposes one
rule you must internalise:

Expand Down Expand Up @@ -198,12 +198,12 @@ the request (a Go `string` copy is just `x := c.Param("id")`).
The two methods documented for cross-request lifetimes are explicit about it in
source:

- `c.BodyCopy()` (context_request.go): "returns a copy of the request body that
- `c.BodyCopy()` (`celeris/context_request.go`): "returns a copy of the request body that
is safe to retain after the handler returns. Use this instead of `Body()` when
the body must outlive the request lifecycle (e.g., for async processing or
logging)." `Body()` itself warns the slice "must not be modified or retained
after the handler returns."
- `c.RequestHeaders()` (context_request.go): "returns all request headers as
- `c.RequestHeaders()` (`celeris/context_request.go`): "returns all request headers as
key-value pairs. The returned slice is a copy safe for concurrent use."

```go
Expand Down Expand Up @@ -241,7 +241,7 @@ classic footgun.

### Protocol — what's on the wire

`celeris.Protocol` (config.go) is **cleartext only** — Celeris does not terminate
`celeris.Protocol` (`celeris/config.go`) is **cleartext only** — Celeris does not terminate
TLS itself; put it behind a TLS-terminating proxy or load balancer.

| Protocol | Meaning |
Expand All @@ -256,7 +256,7 @@ the protocol — enabled for `Auto`, disabled for `HTTP1` and `H2C`.

### Engine — how bytes move

`celeris.EngineType` (config.go) selects the I/O implementation. **The engine is
`celeris.EngineType` (`celeris/config.go`) selects the I/O implementation. **The engine is
an I/O strategy, not a protocol** — every engine serves every supported protocol;
they differ only in how they talk to the kernel.

Expand Down Expand Up @@ -304,7 +304,7 @@ understanding before you build.
I/O worker, you get goroutine-per-connection parallelism, matching `net/http`'s
model.

The trade-off, per `Config.AsyncHandlers` go-doc (config.go): async costs a
The trade-off, per `Config.AsyncHandlers` go-doc (`celeris/config.go`): async costs a
goroutine spawn (~100ns) plus scheduler overhead per request — a measured ~3–5%
regression on a pure static-response benchmark. So:

Expand Down
38 changes: 19 additions & 19 deletions src/content/docs/engines.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ difference between good and exceptional throughput.

You select an engine with one field, `Config.Engine` (`celeris/config.go`). The
zero value is the right answer on almost every box: **Adaptive on Linux, Std
everywhere else** (`celeris/internal/resource/config.go:13-19`). This page explains the four
everywhere else** (`celeris/internal/resource/config.go` (`defaultEngine`)). This page explains the four
engines, the adaptive controller that picks between them, how the engine relates
to protocol and to async dispatch, and the introspection surface for observing it
all at runtime.
Expand Down Expand Up @@ -75,8 +75,8 @@ modern Linux (epoll predates all supported kernels) and the safest native choice
when io_uring is unavailable or you want to pin behaviour. Epoll is at throughput
parity with io_uring for most request/response workloads — you are not trading
latency for compatibility by choosing it. Epoll is also the engine that implements
zero-copy `sendfile(2)` for static-file responses (`celeris/internal/engine/engine.go:73-99`,
`celeris/internal/engine/capability.go:38-44`).
zero-copy `sendfile(2)` for static-file responses (`celeris/internal/engine/engine.go` (`SendfileCapable`),
`celeris/internal/engine/capability.go` (`CapabilityProfile.Sendfile`)).

### IOUring

Expand Down Expand Up @@ -134,7 +134,7 @@ io_uring, epoll, and adaptive depend on Linux kernel facilities, so they cannot
elsewhere. The distinction worth internalising:

- **Leaving `Engine` unset off Linux silently selects Std.** The default resolves
per-platform (`celeris/internal/resource/config.go:13-19`). This is the intended fallback —
per-platform (`celeris/internal/resource/config.go` (`defaultEngine`)). This is the intended fallback —
your code runs unchanged on a Mac.
- **Explicitly setting a native engine off Linux is a validation error.** It is
*not* silently downgraded. `Config.Validate` returns `engine <name> requires
Expand Down Expand Up @@ -170,8 +170,8 @@ and let Adaptive/Std resolve automatically.
| Accept control (Pause/Resume) | Yes | Yes | — |
| Driver event-loop colocation | Yes | Yes | — |

Sources: `celeris/internal/engine/capability.go`, `celeris/internal/engine/engine.go:38-99`,
`celeris/server.go:736-861`, `celeris/context_response.go:1423-1446`.
Sources: `celeris/internal/engine/capability.go`, `celeris/internal/engine/engine.go` (`AcceptController`, `SwitchFreezer`, `SendfileCapable`),
`celeris/server.go` (`Server.EventLoopProvider`), `celeris/context_response.go` (`Context.EngineSupportsAsyncDetach`).

## The adaptive controller

Expand Down Expand Up @@ -206,7 +206,7 @@ Established connections now transplant between engines on a switch (see above),
the **start** engine no longer fixes the keep-alive throughput ceiling — but the
steady-state concurrency is still unknowable when the server binds, and a good
start avoids an unnecessary early switch. `WorkloadHint`
(`celeris/config.go:45-62`) is the config-level way to bias that start decision. It
(`celeris/config.go` (`WorkloadHint`)) is the config-level way to bias that start decision. It
affects **nothing but the Adaptive engine's start choice**; on Epoll, IOUring, and
Std it is ignored.

Expand Down Expand Up @@ -236,7 +236,7 @@ Adaptive falls back to starting on epoll.
Every time the adaptive controller changes strategy it increments a counter you can
read from the metrics collector. `Server.Collector().Snapshot()` returns a
`Snapshot` whose `EngineSwitches` field counts the switches since start
(`celeris/observe/collector.go:34-51`).
(`celeris/observe/collector.go` (`Snapshot`)).

```go
snap := s.Collector().Snapshot()
Expand Down Expand Up @@ -321,7 +321,7 @@ underneath a still-running goroutine.
Celeris exposes this difference so streaming middleware can adapt rather than break.
`Context.EngineSupportsAsyncDetach()` reports whether the active engine can keep the
connection alive after the handler returns
(`celeris/context_response.go:1423-1446`):
(`celeris/context_response.go` (`Context.EngineSupportsAsyncDetach`)):

```go
func streamHandler(c *celeris.Context) error {
Expand Down Expand Up @@ -357,7 +357,7 @@ your own streaming transport.
> WebSocket has a deeper engine integration than SSE: native engines provide an
> *engine-integrated* upgrade path (`UpgradeWebSocket`, `WSReadPauser`,
> `WSRawWriteFn`) with TCP-level backpressure; on Std these return false/nil and the
> middleware falls back to `Context.Hijack` (`celeris/context_response.go:1289-1410`).
> middleware falls back to `Context.Hijack` (`celeris/context_response.go` (`Context.Hijack`)).
> The middleware handles this fallback transparently.

## Engine selection in practice
Expand Down Expand Up @@ -415,7 +415,7 @@ The running engine exposes a read-only surface for observability and control.
### `EngineInfo` and `EngineType`

`Server.EngineInfo()` returns the active engine's type and a metrics snapshot, or
`nil` before `Start` (`celeris/server.go:802-812`). On Adaptive, `Type` reflects the
`nil` before `Start` (`celeris/server.go` (`Server.EngineInfo`)). On Adaptive, `Type` reflects the
engine that is *currently active*, so you can see which sub-engine the controller
has selected.

Expand All @@ -434,7 +434,7 @@ if info := s.EngineInfo(); info != nil {
| `Metrics` | `EngineMetrics` | A point-in-time snapshot of the counters below. |

`EngineType` has a `String()` method that returns `"io_uring"`, `"epoll"`,
`"adaptive"`, or `"std"` (`celeris/internal/engine/enginetype.go:20-33`).
`"adaptive"`, or `"std"` (`celeris/internal/engine/enginetype.go` (`EngineType.String`)).

### `EngineMetrics` fields

Expand All @@ -460,7 +460,7 @@ These are the counters the adaptive controller reads to derive its load signals
error rate (see [The adaptive controller](#the-adaptive-controller)). They are also
re-exported on the metrics `Snapshot` as `EngineMetrics`, alongside `RequestsTotal`,
`ErrorsTotal`, `ActiveConns`, `EngineSwitches`, latency buckets, and CPU
utilisation (`celeris/observe/collector.go:34-51`).
utilisation (`celeris/observe/collector.go` (`Snapshot`)).

A request rate is not one of the counters: take two snapshots and divide the
`RequestCount` difference by the time between them.
Expand All @@ -482,9 +482,9 @@ if m.RequestCount > 0 {

To stop accepting new connections while continuing to serve existing ones — useful
for graceful load shedding or coordinated draining — call `Server.PauseAccept()`
and later `Server.ResumeAccept()` (`celeris/server.go:814-861`). These work on the
and later `Server.ResumeAccept()` (`celeris/server.go` (`Server.PauseAccept`)). These work on the
native engines. The **std engine does not support accept control**: both return
`celeris.ErrAcceptControlNotSupported` (`celeris/errors.go:29-31`), as does calling
`celeris.ErrAcceptControlNotSupported` (`celeris/errors.go` (`ErrAcceptControlNotSupported`)), as does calling
them before `Start`.

```go
Expand All @@ -501,7 +501,7 @@ _ = s.ResumeAccept()

`Server.EventLoopProvider()` returns the engine's per-worker event-loop provider,
or `nil` if the engine does not expose one — which is the case for the **std**
fallback (`celeris/server.go:736-758`). This is the integration point that lets
fallback (`celeris/server.go` (`Server.EventLoopProvider`)). This is the integration point that lets
Celeris database and cache drivers register their own sockets on the *same* worker
event loops as the HTTP path, so a DB round-trip is driven by the very thread that
owns the request's connection — no cross-thread handoff, NUMA-local buffers.
Expand All @@ -518,7 +518,7 @@ lists them as not covered).

The provider exposes `NumWorkers()` and `WorkerLoop(n)`; the per-worker
`WorkerLoop` surface (`RegisterConn`, `UnregisterConn`, `Write`, `CPUID`) is
documented in `celeris/internal/engine/provider.go:32-104`. You normally don't call this
documented in `celeris/internal/engine/provider.go` (`EventLoopProvider`, `WorkerLoop`). You normally don't call this
yourself — a Celeris driver opened `WithEngine(srv)` consumes it for you. When the
provider is `nil` (std engine), drivers fall back to a standalone mini event loop.
A nil check and a call such as `p.NumWorkers()` need no name for the type:
Expand All @@ -534,7 +534,7 @@ if p := s.EventLoopProvider(); p != nil {
> For drivers to pick their fast netpoll-park path, the server's *effective* async
> state must be on (server `AsyncHandlers: true`, or routes marked `.Async()` /
> `.UsesDriver()` registered **before** the driver is opened). `Server.AsyncHandlers()`
> reports the effective state (`celeris/server.go:760-800`). See
> reports the effective state (`celeris/server.go` (`Server.AsyncHandlers`)). See
> [Routing](/docs/routing#dispatch-mode-async-sync-usesdriver) for the ordering rule.

## Common pitfalls
Expand All @@ -557,7 +557,7 @@ if p := s.EventLoopProvider(); p != nil {
it fails to start.
- **Forgetting to call `done()` after `Detach`.** The returned function *must* run
(typically `defer done()` in the streaming goroutine) or the `*Context` leaks from
its pool permanently (`celeris/context_response.go:1448-1505`).
its pool permanently (`celeris/context_response.go` (`Context.Detach`)).

## FAQ

Expand Down
2 changes: 1 addition & 1 deletion src/content/docs/middleware-auth.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ config shape so you can swap or stack them without relearning the surface.
### One sentinel across all stacks: `ErrUnauthorized`

Every auth package re-exports the same canonical 401 error,
`celeris.ErrUnauthorized` (defined in `celeris/errors.go:44`). `basicauth.ErrUnauthorized`,
`celeris.ErrUnauthorized` (defined in `celeris/errors.go` (`ErrUnauthorized`)). `basicauth.ErrUnauthorized`,
`keyauth.ErrUnauthorized`, and `jwt.ErrUnauthorized` are all aliases of it, so a
single `errors.Is` check matches a failure from *any* layer of a mixed stack:

Expand Down
Loading
Loading