Skip to content

feat(tasks): task_detach — fire-and-forget tasks reap their handle slot (#530)#532

Merged
InauguralPhysicist merged 1 commit into
mainfrom
task-detach-530
Jul 10, 2026
Merged

feat(tasks): task_detach — fire-and-forget tasks reap their handle slot (#530)#532
InauguralPhysicist merged 1 commit into
mainfrom
task-detach-530

Conversation

@InauguralPhysicist

Copy link
Copy Markdown
Collaborator

Closes #530

What

task_detach of id → 1 (0 for main/unknown). Marks a task fire-and-forget (the pthread-detach precedent): it is reaped — task_free + handle-slot release — the moment it finishes, immediately if already finished, and at task_kill. A reaped id reads as unknown afterwards (task_join null, task_alive 0). A task may detach itself via task_self.

Why

Finished tasks were never removed from the 255-slot handle table before process exit, so task_spawn failed with EK_LIMIT after 255 spawns per process lifetime — even when every task was joined (minimal repro in #530). The cap now bounds concurrent tasks, as its message always claimed.

Also fixed: ready-queue poisoning (found by the new tests)

A task killed while READY left its stale entry in the fixed 256-slot ready queue; the trampoline skips stale ids, but enough of them fill the queue, whose push then silently drops real wakeups — a spurious deadlock. task_kill now removes the victim's pending entry (sched_ready_remove). liferaft's crash teardown kills pending deliverers in bulk, so this was load-bearing for the same workload.

#493 guarantee preserved

A detached task's uncaught death still prints its trace and still fails the process at exit: the err_unobserved flag moves to a scheduler-level counter before the slot frees. New exit-contract case task_exit_detached_death.eigs (rc 1) pins it.

Tests

tests/test_tasks.eigs (+8, 77/77): 300 detached spawns (impossible pre-fix), detach-of-finished reaps immediately, join/alive of a reaped id, 300 detach+kill cycles (deadlocked pre-queue-fix), self-detach via task_self, main/unknown refused. Plus the exit-contract case above.

Docs: BUILTINS.md row, SPEC.md Cooperative tasks paragraph, CHANGELOG. Discoverability gate green (213 builtins).

Validation

  • Full suite, release: 2704/2704
  • Full suite, ASan + detect_leaks=1 (rebased on main): 2704/2704, leak tally 0

Unblocks #523 (liferaft chaos runs spawn tens of thousands of short-lived deliverer tasks).

🤖 Generated with Claude Code

…ot (#530)

Finished tasks were never removed from the 255-slot handle table, so
task_spawn failed after 255 spawns per process — a LIFETIME cap even
when every task was joined. task_detach of id (pthread-detach
precedent) marks a task nobody will join: reaped at finish, at
immediate-detach of an already-finished task, and at task_kill. An
uncaught death still prints and still fails the process (#493 — the
flag survives the reap in a scheduler-level counter). A task may
detach itself via task_self.

Also fixes ready-queue poisoning surfaced by the same workload: a task
killed while READY left a stale entry; enough of them filled the fixed
256-slot queue, whose push then silently dropped real wakeups — a
spurious deadlock. task_kill now removes the victim's pending entry.

Surfaced by the #523 liferaft migration (deliverer task per in-flight
message; crash teardown kills pending deliverers in bulk).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@InauguralPhysicist InauguralPhysicist merged commit 8f42831 into main Jul 10, 2026
17 checks passed
@InauguralPhysicist InauguralPhysicist deleted the task-detach-530 branch July 10, 2026 02:34
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.

task layer: finished tasks are never reaped — 255-task LIFETIME cap per process (task_detach)

1 participant