Skip to content

feat(parallel): let any number of same-class tasks be in flight at once - #25

Merged
lisachenko merged 2 commits into
mainfrom
claude/coroutines-repo-session-he07ux
Aug 16, 2026
Merged

feat(parallel): let any number of same-class tasks be in flight at once#25
lisachenko merged 2 commits into
mainfrom
claude/coroutines-repo-session-he07ux

Conversation

@lisachenko

Copy link
Copy Markdown
Owner

Closes #15 — Option 1, per-instance keys upstream. Depends on lisachenko/php-shared-data-extension#23; CI here stays red until that merges into the substrate's main (this repo requires it as dev-main).

What this does

$a = $runtime->spawnParallel(new RenderJob(1));
$b = $runtime->spawnParallel(new RenderJob(2));   // now just works

The substrate now registers a persisted graph under a name minted from its own root address (persistInstance()), so a second instance of one class is a second entry — never an upsert, never a release under a running worker's feet. With the hazard gone at the source, the runtime deletes its guard rather than keeping a softened version:

  • SharedArena::persist() rides persistInstance(). Each task graph has its own registry entry; any number of one class are live at once.
  • Shared roots are keyed by the declared name, not the class — fixing the adjacent break the class keying caused: two declareShared() roots of one class used to upsert each other's entries. New test pins two SharedCounter roots mutated independently.
  • ArenaTaskDirectory drops the in-flight ledger, the refusal, and releaseInFlight(); WorkerSupervisor drops the dispatched map that existed only to feed it. The honest cost statement replaces the limit in the docblock and in AGENTS/README: an unpublished spawn clones its graph into the arena and that memory lives until family teardown — steady-state workloads publish before the fork, which allocates nothing per spawn.
  • Each panic keeps its own SharedError. Previously a second worker's capture superseded the first worker's registry entry, so a waiter attaching the first error's address after that found nothing. New test drives two concurrently panicking workers and asserts each waiter reads exactly its own message.

Acceptance criteria from #15

  • Two tasks of the same class in flight simultaneously, each await()s its own resulttestTwoTasksOfOneClassAreInFlightAtOnce.phpt pins two napping same-class tasks to two workers so their lifetimes genuinely overlap.
  • Neither task's arena graph is mutated or freed while the other is running — asserted, not assumed — both instances are persisted pre-fork at verified-distinct addresses, and while both tasks run, each graph's string payload is read back through shared memory and still carries its own value.
  • The pre-fork publishTask() route keeps working unchanged — the entire existing suite drives it; no published-route code changed.

Verification

  • Full suite OK (97 tests, 97 assertions) on PHP 8.4 against a vendor tree carrying the substrate branch (94 existing + 3 new).
  • PHPStan level max clean; cs-check clean.
  • The arena-watermark soak's RSS verdict fails identically on unmodified main (24 KiB at 40 rounds, scaling with rounds, arena metrics byte-flat) — a pre-existing parent-side native-memory accumulation, filed as soak-arena-watermark reports a steady parent-side RSS climb the arena metrics do not see #24 rather than folded in here.

🤖 Generated with Claude Code

https://claude.ai/code/session_01U5QRWPfmkZME7hDjU6i5EA


Generated by Claude Code

Two RenderJobs was the most natural thing a user could try, and it was
refused: the substrate's registry keyed a persisted graph by class
name, so a second persist of one class was an upsert that would have
released the graph a running worker was still reading. The refusal was
the only safe behaviour available on this side of the line - and now
the line has moved. The substrate keys graphs per instance
(lisachenko/php-shared-data-extension#23), so the runtime simply
stops refusing:

- SharedArena::persist() rides persistInstance(): each task graph's
  registry entry is named by its own root address, a second instance
  is a second entry, and nothing supersedes anything.
- Shared roots are filed under the name they were declared with, not
  the class - so two declareShared() roots of one class, which the
  class keying quietly broke too, are now two graphs.
- ArenaTaskDirectory drops the in-flight ledger, the refusal and
  releaseInFlight(); WorkerSupervisor drops the dispatched map that
  existed only to feed it. What route 2 costs instead is stated in
  the directory's docblock: arena memory per unpublished spawn, held
  until family teardown - publish tasks for steady-state workloads.

Each panic is now its own SharedError instance graph as well, so two
workers failing near-simultaneously each leave an error their waiter
can still attach - previously the second capture superseded the first
worker's registry entry.

Tests pin all three acceptance criteria of #15: two same-class tasks
in flight at once with each awaiting its own result; both graphs read
back intact through shared memory while both run (asserted, not
assumed); the pre-fork publishTask() route untouched by the whole
existing suite. Plus a test for two same-class roots and one for two
concurrent panics keeping their own errors.

The arena-watermark soak's RSS verdict fails identically on
unmodified main and is tracked separately in #24.

Requires lisachenko/php-shared-data-extension#23; CI on this branch
is red until that merges into the substrate's main.

Closes #15

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U5QRWPfmkZME7hDjU6i5EA

Copy link
Copy Markdown
Owner Author

CI status, verified from the job logs: all four failures (Tests + PHPStan, both minors) are the declared dependency and nothing else — the runners resolved lisachenko/php-shared-data-extension at upstream dev-main 5d170a0, which predates persistInstance(). PHPStan reports exactly the three errors that implies (persistInstance() not found, plus the old class-string signature of persist()).

Unblock path: merge lisachenko/php-shared-data-extension#23 (green, 8/8 jobs), then CI here re-runs against the new dev-main and should match the local result — 97/97 tests, PHPStan level max clean. I'll re-trigger the run as soon as the substrate merges.


Generated by Claude Code

@lisachenko
lisachenko marked this pull request as ready for review August 16, 2026 07:59
@lisachenko
lisachenko merged commit 261d11d into main Aug 16, 2026
4 checks passed
@lisachenko
lisachenko deleted the claude/coroutines-repo-session-he07ux branch August 16, 2026 07: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.

Two concurrent tasks of the same class cannot be in flight at once

1 participant