Skip to content

lunatik: spawn percpu as per-CPU pinned worker threads - #686

Open
lneto wants to merge 8 commits into
claude_percpu_enforcementfrom
claude_percpu_spawn
Open

lunatik: spawn percpu as per-CPU pinned worker threads#686
lneto wants to merge 8 commits into
claude_percpu_enforcementfrom
claude_percpu_spawn

Conversation

@lneto

@lneto lneto commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Resolves #681, stacked on #678. spawn <script> percpu now starts one runtime and one kernel thread per CPU id, each thread bound to the CPU of its instance, so a worker sees a stable lunatik.cpu() and owns per-CPU state without locking — the ksoftirqd shape, in Lua. The refusal #676 added was the placeholder for this.

Two bugs surfaced on the way and are fixed first, each on its own commit:

thread:task() reported task->on_cpu, which tells whether the task is executing, not where: it answered 1 on every call. With task_cpu() it reports the CPU, which is also what lets the test prove the binding.

runner.spawn keyed env.threads by the untrimmed script name while runner.stop looked it up trimmed, so spawning <script>.lua left the thread behind. Fixed as part of making the keys consistent for percpu.

kthread_bind must run between creation and the first wake up, so thread.run splits kthread_run into kthread_create + bind + wake_up_process. The cpu argument is optional and a plain spawn takes the old path.

Test: each thread records whether it is executing on the CPU its instance claims; the check asserts all of them did. Removing the bind makes it fail with "the thread of CPU 0 ran on another CPU". The percpu suite's refusal assertion became "a running percpu script is neither run nor spawned twice".

Tested on 6.8.0-136: full suite twice, pass:94 fail:0, clean dmesg; builds clean against 5.15 and 6.8.

🤖 Generated with Claude Code

lneto and others added 8 commits August 6, 2026 13:37
The runner passes the instance id to lunatik.runtime(), which stores
it in the state's extraspace before the script is loaded, so load-time
registrations can read it; lunatik.cpu() exposes it to the script, on
the stub library as well since percpu scripts are usually softirq.
runner.run no longer forwards its vararg tail: with cpu as the third
argument of lunatik.runtime(), a stray extra would silently claim an
instance id for a plain runtime.

Groundwork for the registration-point enforcement (#675).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A hook registered by a percpu instance captures the instance CPU id
and, at dispatch, accepts packets being processed on any other CPU, so
each packet is handled by exactly one instance: per-CPU hook state with
no locking, at the cost of N-1 early-outs per packet. The foreign
instances return NF_ACCEPT explicitly, not the policy: their
transparency is a contract of the affinity design (#675).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A constructor whose registration is global would be registered once
per instance by a percpu script: N chardevs, N notifier callbacks, N
kprobes on the same symbol. Refuse at load, with an error naming
percpu, through a shared helper adopted by device, notifier, probe and
hid; the eBPF attach and the per-runtime objects stay untouched, and
netfilter has affinity instead (#675).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
`task->on_cpu` tells whether the task is currently executing, not where:
`task()` reported 1 on every call. The CPU id comes from `task_cpu()`.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
`thread.run` takes an optional cpu and pins the thread to it, which
kthread_bind requires between creation and the first wake up, so the
call is split out of kthread_run.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
`spawn <script> percpu` starts one runtime and one thread per CPU id,
each thread bound to the CPU of its instance, so a worker sees a stable
`lunatik.cpu()` and owns per-CPU state without locking. Stopping by
name drops every thread and runtime, and a thread that fails to start
rolls back what the script already created.

spawn now trims the script name before registering the thread: it was
keying `env.threads` by the untrimmed name while stop looked it up
trimmed, so spawning `<script>.lua` left the thread behind.

Closes #681.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@lneto
lneto force-pushed the claude_percpu_enforcement branch 3 times, most recently from 40bdd3a to e26a81e Compare August 8, 2026 10:54
@lneto
lneto force-pushed the claude_percpu_enforcement branch from e26a81e to 2786065 Compare August 9, 2026 13:43
lneto added a commit that referenced this pull request Aug 9, 2026
#686 (per-CPU CPU-pinned kthreads for `spawn`, sleepable) reshapes the
per-CPU authenticator: the handshake's keying and PBKDF2 can run inline in
a per-CPU worker, so the softirq two-tier split is a fallback, not the
premise. Rework the per-CPU sections and open questions to lead with the
spawned worker + AF_PACKET fanout candidate, keeping EAPOL visibility and
STA affinity as the open, spike-answered questions.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@lneto
lneto force-pushed the claude_percpu_enforcement branch 2 times, most recently from fdb5ccd to 9e474bc Compare August 9, 2026 15:11
@lneto lneto added the on hold label Aug 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant