Skip to content

feat: SentTaskHandle and ReceivedTaskHandle - #2636

Open
nicolas-grekas wants to merge 3 commits into
php:mainfrom
nicolas-grekas:bgworker-tasks
Open

nicolas-grekas wants to merge 3 commits into
php:mainfrom
nicolas-grekas:bgworker-tasks

Conversation

@nicolas-grekas

@nicolas-grekas nicolas-grekas commented Sep 7, 2026 •

Copy link
Copy Markdown
Contributor

Stacked on #2617 and #2635: review the last commit.

Tasks for background workers, the task half of #2319. A worker declares the namespaces it receives, and a sender names the namespace:

worker {
	file images.php
	background
	num 4
	tasks image.resize image.thumbnail
}
// anywhere
$task = new FrankenPHP\SentTaskHandle('image.resize', ['file' => 'photo.jpg']);
while (null !== $update = $task->read()) {
}

// the background worker
while ($handle->tick(null)) {
    while ($task = $handle->receive()) {
        $task->update(['progress' => 50]);
        $task->complete(resize($task->getNamespace(), $task->getPayload()));
    }
}
  1. Constructing a SentTaskHandle queues the task and returns. $pickupTimeout runs from the construction: once it passes, the task leaves the queue and read() throws.
  2. A pool shares one queue for all its namespaces, and each task wakes one waiting thread: in tick() when it was given a timeout, or on its WorkerHandle for a loop waiting there, so the loops of feat: background workers and FrankenPHP\WorkerHandle #2617 don't change. receive() only dequeues.
  3. Both task handles are Io\Poll\Handle with a getStream(), like the worker's. abandon(), or dropping the handle, gives up on the task: it leaves the queue if no thread took it yet, and the worker learns it from update() returning false otherwise. On the worker's side, abort(), or dropping the handle without complete(), makes the sender's next read() throw.
  4. Nothing waits but tick() with a timeout, read() and the waits on handles: the constructor, update() and complete() return at once, so both sides fit in an event loop and neither can hold the other. The price: FrankenPHP doesn't push back, the queue and the unread updates grow with what the scripts do.
  5. Waits sleep on a mailbox per thread, a futex on Linux, which a thread first watches for about as long as its own wake-ups take, while that pays off. A task only gets descriptors when its handle goes to stream_select() or a poll context: an eventfd on Linux, a socket pair elsewhere, closed with the task. On a GitHub runner with 4 vCPUs, a task goes round in 5.5 µs without a syscall, and 4 senders on 4 threads pass 110k tasks/s.

The metrics follow: frankenphp_busy_workers and frankenphp_worker_queue_depth count tasks, plus frankenphp_worker_task_count{outcome} and frankenphp_worker_task_time for a Metrics implementation that also implements TaskMetrics.

@nicolas-grekas

Copy link
Copy Markdown
Contributor Author

Pushed 23d3094 after benchmarking the task path in the Docker builder image (harness kept out of the PR). Three changes: a send wakes one parked thread instead of every thread of the pool; the sender waits for the pickup in the kernel, on the task's socket, instead of in a Go select (waking a thread parked inside a cgo callback costs the scheduler a P hand-off); and the queue mutex is no longer held across a syscall nor taken twice per wake-up. One thread: 549 to 285µs per task. A pool of 8: 1745 to 320µs. 8 senders on 8 threads: 2k to 32k tasks/s. Absolute numbers are that VM's, the ratios are the point.

@nicolas-grekas
nicolas-grekas force-pushed the bgworker-tasks branch 5 times, most recently from 4c5af56 to 2b77a5b Compare September 9, 2026 17:53
@nicolas-grekas
nicolas-grekas force-pushed the bgworker-tasks branch 5 times, most recently from e0be4ac to fd177fc Compare September 14, 2026 06:16
@nicolas-grekas
nicolas-grekas force-pushed the bgworker-tasks branch 11 times, most recently from a8f1a7f to a9d72df Compare September 18, 2026 16:58
@nicolas-grekas nicolas-grekas changed the title feat: frankenphp_send_task(), frankenphp_receive_task() and friends feat: SentTaskHandle and ReceivedTaskHandle Sep 18, 2026

This branch has not been deployed

No deployments
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