Skip to content

v1.1.7

Latest

Choose a tag to compare

@M-Chris M-Chris released this 15 Sep 22:31

A correctness release with one transport improvement. Nothing on the wire
changes, nothing in the API is removed or renamed, and the one new
capability is additive and feature-detected through probe().capabilities.
A 1.8.x/1.9.x MoroJS keeps working on this binary; a MoroJS built for this
release keeps working on 1.1.6.

Fixed — callbacks now drain the microtask queue

Every trampoline into JS (onRequest, onRequestBatch, onAborted,
onWritable, onWsOpen, onWsMessage, onWsClose) runs inside a Node
callback scope, the same scope Node opens around its own I/O callbacks.
When the outermost scope closes, Node runs the process.nextTick queue and
drains V8's microtask queue.

Before this release the engine invoked callbacks with a plain V8 call, so a
continuation queued during dispatch — an await on an already-settled
promise, a .then chain, a framework's asynchronous not-found path — stayed
queued until some later Node-managed callback ran. On a quiet server that
was whatever timer fired next: a response sent from such a continuation
took seconds to leave (measured: 5–29 s for a 404 or a Promise.then
completion, and under load zero such responses completed). Completions that
arrived through setImmediate, timers or sockets were never affected, which
is why the bug hid behind real I/O.

  • capabilities.callbackScope: true advertises it. A framework that armed
    its own setImmediate drain as a workaround can skip it when this flag is
    set (MoroJS does).
  • Cost per callback: an async-context push/pop and, with an empty queue, one
    check. No measurable change in the raw hello-world numbers.
  • Test: test/callback-scope.test.mjs — a response sent from a microtask, a
    settled await and a nextTick each leave in well under a second; the
    old binary fails the test by timing out.

io_uring — DEFER_TASKRUN behind a registered eventfd

The 1.1.6 measurements found io_uring halving the syscalls per request but
spending more CPU per completion than libuv, because under COOP_TASKRUN
every completion is its own task-work round trip. The ring now has two
modes (src/uring.h, "Ring modes"):

  • defer-taskrun (first choice): completions stay queued as local task
    work until the engine's own io_uring_enter(GETEVENTS) runs them as one
    batch. Such a ring posts nothing to its fd, so the loop waits on a
    registered eventfd, which the kernel signals when local work is queued —
    one extra read per wake, many completions per wake.
  • coop-taskrun (the 1.1.6 mode): kept as the fallback, and selectable
    with MORO_ENGINE_URING_TASKRUN=coop for A/B runs
    (MORO_ENGINE_URING_TASKRUN=defer pins the new one with no fallback).

The probe proves the wake behaviourally for whichever mode it selects,
exactly as it already proved the ring-fd wake, and probe().transportMode
reports 'uv', 'defer-taskrun' or 'coop-taskrun'.

Measured on the same VM as the 1.1.6 numbers (Docker Desktop, linuxkit 6.12,
arm64, server pinned to 2 cores, zrk closed-loop over keep-alive, two
rounds): defer beats coop by 13–30% in every cell with lower p99, and lands
at parity with libuv overall (ahead at 64 connections, behind at 256, split
at 512) where coop was 10–25% behind. libuv stays the default; io_uring
stays opt-in via MORO_ENGINE_TRANSPORT=uring until a run on the reference
hardware flips the go/no-go in docs/DESIGN.md.

Two follow-ups listed in the 1.1.6 design notes are closed without code:
ring-batched sends were already in place (a SEND is only prepared at issue
time; the reap loop's next enter submits the whole round), and
RECVSEND_BUNDLE cannot help a one-request-in-flight keep-alive shape.

Build

  • The Linux build is warning-free from the source again: the six
    (void)Function::Call(...) sites, which gcc flagged regardless of the
    cast, consume the result explicitly.

Compatibility

  • No wire changes. No removed or renamed exports (tools/check-exports.mjs:
    no drift).
  • New: capabilities.callbackScope, probe().transportMode,
    MORO_ENGINE_URING_TASKRUN.
  • Kernels without DEFER_TASKRUN or whose eventfd wake the probe cannot
    prove get coop-taskrun; kernels or sandboxes without io_uring get libuv,
    silently, as before. Verified: libuv, io_uring in both modes, and the
    seccomp-blocked lane (test/ci/seccomp-block-io_uring.json) — 207 suite
    tests each.