Skip to content

Add an API to discard pending jobs - #1745

Open
undy-aeon wants to merge 3 commits into
quickjs-ng:masterfrom
tzfm:discard-pending-jobs-upstream
Open

undy-aeon wants to merge 3 commits into
quickjs-ng:masterfrom
tzfm:discard-pending-jobs-upstream

Conversation

@undy-aeon

@undy-aeon undy-aeon commented Sep 23, 2026 •

Copy link
Copy Markdown

Hosts cancelling an evaluation need to release queued jobs without running more callbacks or destroying the runtime.

Add JS_DiscardPendingJobs, returning the discarded count and releasing each job's retained arguments and queue entry. Document runtime ownership, exclusive access, finalizer restrictions, and the effect on promises.

API tests cover empty and repeated discard, retained-value cleanup, callbacks remaining uncalled, and subsequent jobs executing in the same runtime.

Validation: API tests (also under AddressSanitizer on both the current upstream and rquickjs 0.14 engine baselines), JavaScript tests (115 on the compatible engine baseline and 116 on current upstream), C/C++ header checks, and strict value-type checks on macOS. The strict check uses CC='clang -Wno-unused-command-line-argument' for Apple's clang.

Dependent bindings: DelSkayn/rquickjs#768.

Comment thread quickjs.c Outdated
JSJobEntry *e;
int i;

while (!list_empty(&rt->job_list)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why not splice the list, set an empty one, and then operate on the spliced list to avoid the risk of any finalizers from ever calling enqueue job?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Here is code you can use to move rt->job_list into a temporary list_head:

diff --git a/list.h b/list.h
index b8dd716..779af5d 100644
--- a/list.h
+++ b/list.h
@@ -86,6 +86,27 @@ static inline int list_empty(struct list_head *el)
     return el->next == el;
 }
 
+static inline void list_split(struct list_head *src,
+                              struct list_head *el,
+                              struct list_head *dst)
+{
+    dst->prev = src->prev;
+    dst->prev->next = dst;
+    dst->next = el;
+    src->prev = el->prev;
+    src->prev->next = src;
+    el->prev = dst;
+}
+
+static inline void list_move(struct list_head *src, struct list_head *dst)
+{
+    if (list_empty(src)) {
+        init_list_head(dst);
+    } else {
+        list_split(src, src->next, dst);
+    }
+}
+
 #define list_for_each(el, head) \
   for(el = (head)->next; el != (head); el = el->next)
 

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Thanks, I've used list_move / list_split to detach the queue before releasing any arguments. Jobs enqueued by native finalizers now remain pending, and the returned count covers only the original queue. I've updated the header contract and added tests for singleton and multi-entry queues, argument finalization, and executing the surviving jobs afterward. The API tests pass, including under ASan.

Comment thread quickjs.h Outdated
JS_EXTERN int JS_EnqueueJob(JSContext *ctx, JSJobFunc *job_func,
int argc, JSValueConst *argv);

/* Discard all queued jobs without executing their callbacks. Releases the

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Sound a bit like a loaded gun, any chance of making it safer? I left a suggestion above.

Keep owned job arguments as JSValue and borrow them when enqueuing. This avoids struct-to-struct casts rejected by MSVC while preserving JS_CHECK_JSVALUE compatibility.
@bnoordhuis

Copy link
Copy Markdown
Contributor

Thinking about this more...

Hosts cancelling an evaluation need to release queued jobs without running more callbacks or destroying the runtime.

That "or destroying the runtime" won't fly. Canceling jobs at the wrong time leaves the runtime in an irreparable state. Example: dynamic import jobs, e.g., await import(url) - hangs forever when canceled.

With that in mind, I don't think this API is a good addition. It won't work for what you want to use it, plus, it's very footgunny.

@undy-aeon

Copy link
Copy Markdown
Author

Thanks, you were right about the lifetime issue. I reproduced it with queued jobs and pending top-level-await modules, where clearing the queue alone does not restore existing asynchronous state. I opened #1751 to address the underlying job, context, and module lifetimes separately. Once that behavior is settled, I’ll revisit the scope and contract of this PR.

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.

3 participants