Skip to content

Add multi-core serving via SO_REUSEPORT fork-based workers#31

Merged
hellerve merged 1 commit into
mainfrom
claude/serve-with-workers
Jun 14, 2026
Merged

Add multi-core serving via SO_REUSEPORT fork-based workers#31
hellerve merged 1 commit into
mainfrom
claude/serve-with-workers

Conversation

@carpentry-agent

Copy link
Copy Markdown
Contributor

Summary

Addresses #7.

  • Add App.serve-with-workers that forks n child processes, each running its own event loop on the same host:port
  • The kernel distributes incoming connections across workers via SO_REUSEPORT (already set on TcpListener in the sockets library)
  • Falls back to single-process serve when n <= 1
  • Parent process ignores SIGINT/SIGTERM (children handle their own signals), then waits for all children to exit
  • Add (workers N) form to defserver for declarative configuration:
    (defserver "0.0.0.0" 3000
      (workers 4)
      (GET "/" handler))
    
  • Update CHANGELOG

Design

Fork-based isolation avoids all shared-mutable-state concerns — each worker is an independent process with its own ConnState, poll set, and event loop. No threads, no locks, no closure-capture issues. The SO_REUSEPORT socket option (Linux 3.9+, most BSDs) allows multiple processes to bind to the same port; the kernel load-balances accept() calls across them.

Test plan

  • Verify defserver with (workers 4) compiles and spawns 4 workers
  • Verify defserver without (workers N) still calls serve (single-process)
  • Verify serve-with-workers with n=1 falls back to serve
  • Verify graceful shutdown: Ctrl-C stops all workers
  • Verify CI passes (lint + format + existing tests)

Implement App.serve-with-workers that forks n child processes, each
running its own event loop bound to the same host:port. The kernel
distributes incoming connections across workers via SO_REUSEPORT
(already set on TcpListener). Falls back to single-process serve
when n <= 1. Add (workers N) form to defserver for declarative
configuration. Addresses issue #7.

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

Build & Tests

CI: PASS — macos-latest passes (tests + doc generation). The existing test suite (test/web.carp, test/websocket.carp) passes.

The Carp compiler is not available on the review machine (armhf Pi), so the code could not be built or tested locally. Review is based on code reading, CI output, and verification against the Carp stdlib and sockets library source.

No tests exist specifically for serve-with-workers — the function compiles (verified via CI building the test imports), but fork-based behavior isn't exercised. This is understandable given the difficulty of testing fork/signal behavior in Carp's test framework.

Findings

1. Design is sound

Fork-based isolation is the right call for Carp — no shared mutable state, no closure-capture issues, no threading complexity. Each worker is an independent process with its own ConnState, poll set, and event loop.

Verified that SO_REUSEPORT is set in sockets/src/tcp_listener.h (guarded by #ifdef SO_REUSEPORT), so multiple processes binding to the same port is supported at the socket level. The kernel handles load balancing of accept() calls across workers.

2. API usage is correct

Verified against Carp stdlib source (core/System.carp):

  • System.fork returns Int — code correctly checks < 0 (error), = 0 (child), else (parent)
  • System.wait takes (Ptr Int)(Pointer.address &status) is the correct pattern (matches stdlib's own Test.carp usage at core/Test.carp:106)
  • System.signal takes Int and (Fn [Int] ()) — handler signatures match
  • System.exit takes Int(System.exit 0) after serve returns ensures clean child exit

3. Signal handling is correct with a minor note

The parent ignores SIGINT/SIGTERM after all forks complete. Children inherit the pre-fork signal state (default), then serve installs its own handler ((set! App.running false)). When the user presses Ctrl-C, children exit cleanly via the event loop, and the parent unblocks from System.wait.

Minor note: there's a small race window between fork completion and the parent's (System.signal ...) call. If SIGINT arrives in that window, the parent would exit before ignoring signals. In practice this is negligible (the window is a few instructions wide), but worth knowing about.

4. Edge cases handled

  • n <= 1: falls back to single-process serve. Correct.
  • Partial fork failure: prints error, continues. spawned only counts successful forks, so System.wait loops the right number of times. No zombie processes.
  • All forks fail: spawned stays 0, prints "No workers spawned; exiting". Correct.
  • (workers 0) or (workers -1) in defserver: worker-count would be 0 or -1, condition (> worker-count 0) is false, falls back to App.serve. Then serve-with-workers also checks (<= n 1). Double-safe.

5. defserver macro changes are clean

The worker-count extraction correctly:

  • Filters (workers N) forms from the body
  • Extracts N from the first one (or defaults to 0)
  • Removes worker forms before route/setup separation (so workers doesn't leak into setup expressions)
  • Generates either App.serve-with-workers or App.serve based on worker-count

The existing route-form? predicate doesn't include workers, and it doesn't need to — worker forms are filtered out upstream.

6. CHANGELOG updated

The CHANGELOG entry under "Unreleased → Added" accurately describes the feature.

7. Documentation updated

The serve doc string now references serve-with-workers instead of suggesting a TCP load balancer. The defserver doc includes the (workers N) form. Both are accurate.

Verdict: merge

Well-designed feature that fits naturally into the existing architecture. Correct use of system APIs (verified against stdlib source). Edge cases handled. CHANGELOG and docs updated. CI passes. No issues found.

@hellerve hellerve merged commit c93d9ea into main Jun 14, 2026
1 check passed
@hellerve hellerve deleted the claude/serve-with-workers branch June 14, 2026 19:59
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