Skip to content

ChannelTask can be terminated - #22

Open
Roba1993 wants to merge 2 commits into
paberr:mainfrom
Roba1993:terminate-worker
Open

ChannelTask can be terminated#22
Roba1993 wants to merge 2 commits into
paberr:mainfrom
Roba1993:terminate-worker

Conversation

@Roba1993

Copy link
Copy Markdown
Contributor

ChannelTask can be terminated and the whole worker is closed and removed from the pool. The pool will open a new worker afterwards. One ChannelTask is only allowed per WebWorker, so termination has no effect on other tasks.

This is needed for cases where the ChannelTask is running complex logic and spawn additional asynchronous functions endlessly or we don't want to answer to request anymore and close the ChannelTask.

Roba1993 added 2 commits June 12, 2026 14:18
ChannelTask can be terminated and the whole worker is closed and removed from the pool. The pool will open a new worker afterwards. One ChannelTask is only allowed per WebWorker, so termination has no effect on other tasks.
Termination only needs read var access, because the controlling moved to a cloneable struct. This allows to terminate the whole webworker from any place at any time.

@paberr paberr left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks a lot @Roba1993!
Generally solid design and implementation.
I added a mix of comments (some from my own review, some that Claude spotted).

Comment thread src/webworker/worker.rs
ChannelTask::new(channel, result_rx)
let worker = self.worker.clone();
let open_tasks = Rc::clone(&self.open_tasks);
let on_terminate = Box::new(move || {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking: for a direct (non-pool) WebWorker, this terminates the underlying JS worker while the caller still holds the WebWorker handle. Any later run() posts to a dead worker and hangs forever (the oneshot sender inserted into open_tasks by send_request is never resolved). Since ChannelTask::drop auto-terminates, merely dropping an unfinished direct-worker task now silently bricks the worker — previously this was harmless. Suggestion: set a terminated flag on WebWorker and fail fast on subsequent use, or only auto-terminate on drop for pool tasks (where the lease/replacement machinery needs it).

Comment thread src/webworker/worker.rs
}

/// Terminate this worker and fail all tasks currently assigned to it.
pub(crate) fn terminate(&self) {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking: "One ChannelTask is only allowed per WebWorker" is only enforced for pools. On a direct WebWorker, regular tasks can run concurrently with a channel task, and clearing open_tasks here drops their result senders — pending run() futures then panic via expect_throw("WebWorker gone") in send_request. Either enforce exclusivity for direct workers too, or document that terminating a direct-worker channel task fails all in-flight tasks on that worker.

Comment thread src/channel_task.rs
.expect("WebWorker result sender dropped");
from_bytes(&bytes)
.take()
.ok_or(TaskError::ResultAlreadyConsumed)?;

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this variant is unreachable.
result(mut self) consumes self, so result_rx can only be taken once (the Option exists only because Drop is implemented). Consider .expect("result_rx is only taken here") and dropping ResultAlreadyConsumed from the public TaskError enum, so callers don't have to handle an impossible error.

Comment thread src/channel_task.rs
}
}

impl<R> Drop for ChannelTask<R> {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dropping a task whose function already completed (result unconsumed) still kills and replaces a healthy worker. A result_rx.try_recv() check here could avoid the wasted replacement.

Comment thread src/pool/mod.rs
.slots
.iter()
.map(|slot| match &*slot.borrow() {
WorkerSlot::Active { worker, .. } if worker.current_load() == 0 => Some(0),

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this requires a fully idle worker, so channel tasks queue behind each other instead of sharing workers as before. This is a behavior change for existing users: apps running more concurrent channel tasks than num_workers can now block where they previously worked.

@paberr

paberr commented Aug 14, 2026

Copy link
Copy Markdown
Owner

Hey, I just revisited the PR today. I'm not yet fully happy with the motivation and the complexity this introduces.
However, I already ported the most important parts (and fixes into #28) and will that merge first.

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.

2 participants