luv: Unable to `cancel` a `luv_work_ctx_t` handle

RE: #629

If you create new work via the uv.new_work function and then queue it with the queue_work function, how do you go about “killing” said work?

Some example code to show what I mean

local luv = require("luv")
local work_fn = function()
    local count = 0
    while count > 0 do
        count = count + 1
    end
    return
end
local after_work_fn = function()
    print("Work Complete!")
end
local work = luv.new_work(work_fn, after_work_fn)
work:queue()

As we see in the above code, work_fn will never “exit” and thus run infinitely. I don’t see anything in the docs about how to stop “rogue” work processes. Of course, one isn’t going to intentionally create code that will run forever, but it is certainly possible that code could end up doing so. For my use case, I am looking more from the angle of “code is running too long and user wants it to stop”. I don’t see how I would go about doing that.

Am I missing something?

About this issue

  • Original URL
  • State: open
  • Created a year ago
  • Reactions: 1
  • Comments: 16 (9 by maintainers)

Most upvoted comments

If you cannot guarantee that the work will ever finish, you’ll either need to delegate it to its own thread (at which point the os scheduler will handle it behaving poorly) or instrument it with your own code that will preempt it if it runs for too long or something similar. For the latter, you can use debug.sethook.

Well that’s not the answer I was hoping for lol. I appreciate the direction. It appears that the request to get cancel implemented is still needed as it doesn’t exist in the luv implementation of libuv, however it also sounds like getting that implemented won’t address my specific issue. Should I close this thread out or does the luv team want this to stay open to track the luv_work_ctx_t cancel work?

It seems a bit odd to me that a framework that provides an event loop doesn’t have some sort of protection against runaway work being done in its loop.

It’s not all that uncommon, stopping an in-flight work request requires preempting, which is quite difficult to do (nearly impossible at Libuv’s level).

As far as I’m aware Libuv doesn’t provide any way to stop, kill, or otherwise halt a spawned thread; much less do the same for an individual work request (which will be running on one of Libuv’s threadpool threads).

If you cannot guarantee that the work will ever finish, you’ll either need to delegate it to its own thread (at which point the os scheduler will handle it behaving poorly) or instrument it with your own code that will preempt it if it runs for too long or something similar. For the latter, you can use debug.sethook.

Little bit of diving into the libuv docs, I dug up this tidbit

Initializes a work request which will run the given work_cb in a thread from the threadpool. Once work_cb is completed, after_work_cb will be called on the loop thread.

    This request can be cancelled with uv_cancel.

However, when modifying the above code to try and use uv.cancel, I get the following error

Error executing luv callback:
/tmp/test.lua:35: bad argument #1 to 'cancel' (uv_req expected, got userdata)
stack traceback:
        [C]: in function 'cancel'
        /tmp/test.lua:35: in function </tmp/test.lua:34>

Note, updated code

local luv = require("luv")
local work_fn = function()
    local count = 0
    while count > 0 do
        count = count + 1
    end
    return
end
local after_work_fn = function()
    print("Work Complete!")
end
local work = luv.new_work(work_fn, after_work_fn)
work:queue()
local timer = luv.new_timer()
timer:start(100, 0, function()
    luv.cancel(work)
end)

Digging deeper still, the cancel doc actually says

Cancel a pending request. Fails if the request is executing or has finished executing.

    Returns 0 on success, or an error code < 0 on failure.

    Only cancellation of uv_fs_t, uv_getaddrinfo_t, uv_getnameinfo_t, uv_random_t and uv_work_t requests is currently supported.

    Cancelled requests have their callbacks invoked some time in the future. It’s not safe to free the memory associated with the request until the callback is called.

    Here is how cancellation is reported to the callback:

        A uv_fs_t request has its req->result field set to UV_ECANCELED.

        A uv_work_t, uv_getaddrinfo_t, uv_getnameinfo_t or uv_random_t request has its callback invoked with status == UV_ECANCELED.

If I am reading that correctly, does that mean that you can’t cancel a work_ctx_t handle? Noting specifically that the new_work calls out that you do not get a work_t handle.

uv.new_work(work_callback, after_work_callback)

Parameters:

    work_callback: function
        ...: threadargs passed to/from uv.queue_work(work_ctx, ...)
    after_work_callback: function
        ...: threadargs returned from work_callback

Creates and initializes a new luv_work_ctx_t (not **uv_work_t**). Returns the Lua userdata wrapping it.