From 14a60e7ea085792d7784b3bd4fe3d5c64c4086dc Mon Sep 17 00:00:00 2001 From: Thijs Schreijer Date: Thu, 29 Aug 2024 22:28:53 +0200 Subject: [PATCH] feat(exit): add exit signalling (#172) --- docs/index.html | 2 ++ docs/reference.html | 40 ++++++++++++++++++++++++++++++++++++++-- src/copas.lua | 43 ++++++++++++++++++++++++++++++++++++++++++- tests/exittest.lua | 43 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 125 insertions(+), 3 deletions(-) diff --git a/docs/index.html b/docs/index.html index 95d6090..25ec34a 100644 --- a/docs/index.html +++ b/docs/index.html @@ -104,6 +104,8 @@

History

  • Change: Copas no longer requires LuaSocket, if no sockets are needed, LuaSystem will be enough as a fallback.
  • Feat: added copas.gettime(), which transparently maps to either LuaSockets or LuaSystems implementation, ensuring independence of the availability of either one of those.
  • +
  • Feat: Controlled exit of the Copas loop. Adding copas.exit(), copas.exiting(), and + copas.waitforexit().
  • Copas 4.7.1 [9/Mar/2024]
    diff --git a/docs/reference.html b/docs/reference.html index 6795a20..e0d0123 100644 --- a/docs/reference.html +++ b/docs/reference.html @@ -81,8 +81,12 @@

    Getting started examples

    end -copas.addserver(server_socket, copas.handler(connection_handler, - ssl_params), "my_TCP_server") +copas.addthread(function() + copas.addserver(server_socket, copas.handler(connection_handler, + ssl_params), "my_TCP_server") + copas.waitforexit() + copas.removeserver(server_socket) +end) copas() @@ -180,6 +184,29 @@

    Copas dispatcher main functions

    truthy.

    +
    copas.exit()
    +
    +

    Sets a flag that the application is intending to exit. After calling + this function copas.exiting() will be returning true, and + all threads blocked on copas.waitforexit() will be released.

    + +

    Copas itself will call this function when copas.finished() returns + true.

    +
    + +
    bool = copas.exiting()
    +
    +

    Returns a flag indicating whether the application is supposed to exit. + Returns false until after copas.exit() has been called, + after which it will start returning true.

    + +

    Clients should check whether they are to cease their operation and exit. They + can do this by checking this flag, or by registering a task waiting on + copas.waitforexit(). Clients should cancel pending work and close sockets + when an exit is announced, otherwise Copas will not exit. +

    +
    +
    bool = copas.finished()

    Checks whether anything remains to be done.

    @@ -304,6 +331,15 @@

    Copas dispatcher main functions

    currently running coroutine.

    +
    copas.waitforexit()
    +
    +

    This will block the calling coroutine until the copas.exit() function + is called. Clients should check whether they are to cease their operation and exit. They + can do this by waiting on this call, or by checking the copas.exiting() flag. + Clients should cancel pending work and close sockets when an exit is announced, otherwise + Copas will not exit.

    +
    +
    skt = copas.wrap(skt [, sslparams] )

    Wraps a LuaSocket socket and returns a Copas socket that implements LuaSocket's API diff --git a/src/copas.lua b/src/copas.lua index 19a195c..77699d3 100644 --- a/src/copas.lua +++ b/src/copas.lua @@ -1666,6 +1666,38 @@ function copas.finished() return #_reading == 0 and #_writing == 0 and _resumable:done() and _sleeping:done(copas.gettimeouts()) end + +local resetexit do + local exit_semaphore, exiting + + function resetexit() + exit_semaphore = copas.semaphore.new(1, 0, math.huge) + exiting = false + end + + -- Signals tasks to exit. But only if they check for it. By calling `copas.exiting` + -- they can check if they should exit. Or by calling `copas.waitforexit` they can + -- wait until the exit signal is given. + function copas.exit() + if exiting then return end + exiting = true + exit_semaphore:destroy() + end + + -- returns whether Copas is in the process of exiting. Exit can be started by + -- calling `copas.exit()`. + function copas.exiting() + return exiting + end + + -- Pauses the current coroutine until Copas is exiting. To be used as an exit + -- signal for tasks that need to clean up before exiting. + function copas.waitforexit() + exit_semaphore:take(1) + end +end + + local _getstats do local _getstats_instrumented, _getstats_plain @@ -1756,8 +1788,17 @@ function copas.loop(initializer, timeout) timeout = initializer or timeout end + resetexit() copas.running = true - while not copas.finished() do copas.step(timeout) end + while true do + copas.step(timeout) + if copas.finished() then + if copas.exiting() then + break + end + copas.exit() + end + end copas.running = false end diff --git a/tests/exittest.lua b/tests/exittest.lua index 5d4ad67..c8850be 100644 --- a/tests/exittest.lua +++ b/tests/exittest.lua @@ -72,3 +72,46 @@ copas.loop() assert(testran == 6, "Test 6 was not executed!") print("6) success") +print("7) Testing exiting releasing the exitsemaphore (implicit, no call to copas.exit)") +copas.addthread(function() + print("","7 running...") + copas.addthread(function() + copas.waitforexit() + testran = 7 + end) +end) +copas.loop() +assert(testran == 7, "Test 7 was not executed!") +print("7) success") + +print("8) Testing schduling new tasks while exiting (explicit exit by calling copas.exit)") +testran = 0 +copas.addthread(function() + print("","8 running...") + copas.addthread(function() + while true do + copas.pause(0.1) + testran = testran + 1 + print("count...") + if testran == 3 then -- testran == 3 + print("initiating exit...") + copas.exit() + break + end + end + end) + copas.addthread(function() + copas.waitforexit() + print("exit signal received...") + testran = testran + 1 -- testran == 4 + copas.addthread(function() + print("running new task from exit handler...") + copas.pause(1) + testran = testran + 1 -- testran == 5 + print("new task from exit handler done!") + end) + end) +end) +copas.loop() +assert(testran == 5, "Test 8 was not executed!") +print("8) success")