diff --git a/docs/index.html b/docs/index.html index e4a29e2..6b15268 100644 --- a/docs/index.html +++ b/docs/index.html @@ -100,6 +100,12 @@
copas.cancelall(). It cancels pending work to force exit the loop.
+ Intended use is for testing, to force a loop exit, even if a test fails.copas.future class. Wraps a thread in a future, allowing other
diff --git a/docs/reference.html b/docs/reference.html
index 52ae18b..7f970f8 100644
--- a/docs/reference.html
+++ b/docs/reference.html
@@ -1062,6 +1062,35 @@ copas.cancelall()This will clear pending work (client sockets, server sockets, timers) to force the copas + loop to exit immediately. This is useful for testing and debugging. +
+ +Here's an example which, upon an error, cancels all pending work to ensure the test + ends immediately instead of waiting for the long sleep to end: +
+
+copas(function())
+ copas.seterrorhandler(function(err)
+ print(err)
+ copas.cancelall() -- cancel all pending work to force exit
+ end, true) -- true -> set default errorhandler
+
+ copas.addthread(function()
+ print("sleeping long, would block exit")
+ copas.pause(3600) -- one hour
+ end)
+
+ copas.addthread(function()
+ copas.pause(1)
+ error("something went wrong")
+ end)
+end)
+
+ copas.debug.start([logger] [, core])This will internally replace coroutine handler functions to provide log output to diff --git a/src/copas.lua b/src/copas.lua index fc1442e..9f95e01 100644 --- a/src/copas.lua +++ b/src/copas.lua @@ -43,6 +43,7 @@ local gettime = (socket or system).gettime local block_sleep = (socket or system).sleep local ssl -- only loaded upon demand +local core_timer_thread local WATCH_DOG_TIMEOUT = 120 local UDP_DATAGRAM_MAX = (socket or {})._DATAGRAMSIZE or 8192 local TIMEOUT_PRECISION = 0.1 -- 100ms @@ -312,6 +313,12 @@ local _sleeping = {} do heap:remove(co) end + function _sleeping:cancelall() + while heap:size() > 0 do heap:pop() end + heap:insert(gettime() + TIMEOUT_PRECISION, core_timer_thread) + -- lethargy is weak; copas's idle GC sweeps will clean it within a few steps + end + -- @param tos number of timeouts running function _sleeping:done(tos) -- return true if we have nothing more to do @@ -1376,17 +1383,16 @@ end do local timeout_register = setmetatable({}, { __mode = "k" }) - local time_out_thread local timerwheel = require("timerwheel").new({ now = gettime, precision = TIMEOUT_PRECISION, ringsize = math.floor(60*60*24/TIMEOUT_PRECISION), -- ring size 1 day err_handler = function(err) - return _deferror(err, time_out_thread) + return _deferror(err, core_timer_thread) end, }) - time_out_thread = copas.addnamedthread("copas_core_timer", function() + core_timer_thread = copas.addnamedthread("copas_core_timer", function() while true do copas.pause(TIMEOUT_PRECISION) timerwheel:step() @@ -1706,6 +1712,45 @@ local resetexit do end +--- Forcibly cancels all pending work and signals exit. +-- Intended for test teardown only. Abandons all registered threads and sockets +-- without giving them a chance to clean up. After this call copas.finished() +-- will return true and the loop will exit. The module is left in a clean state +-- ready for the next copas.loop() call. +function copas.cancelall() + -- 1. clear resumable queue + _resumable:clear_resumelist() + + -- 2. drain sleeping heap + _sleeping:cancelall() + + -- 3. close and drain reading sockets + while _reading[1] do + copas.close(_reading[1]) + _reading:remove(_reading[1]) + end + + -- 4. close and drain writing sockets + while _writing[1] do + copas.close(_writing[1]) + _writing:remove(_writing[1]) + end + + -- 5. remove all servers + while _servers[1] do + copas.removeserver(_servers[1]) + end + + -- 6. clear non-weak ancillary tables + _closed = {} + _reading_log = {} + _writing_log = {} + + -- 7. signal exit + copas.exit() +end + + local _getstats do local _getstats_instrumented, _getstats_plain diff --git a/tests/cancelall.lua b/tests/cancelall.lua new file mode 100644 index 0000000..c531017 --- /dev/null +++ b/tests/cancelall.lua @@ -0,0 +1,66 @@ +-- make sure we are pointing to the local copas first +package.path = string.format("../src/?.lua;%s", package.path) + +local copas = require "copas" +local socket = require "socket" +local timer = copas.timer + +-- Test 1: cancelall() exits a loop that has a server, a sleeping thread, and a +-- thread blocked on a socket. + +copas.loop(function() + -- register a server that will never get a connection + local srv = socket.tcp() + assert(srv:bind("127.0.0.1", 0, 5)) + copas.addserver(srv, function() + copas.pause(60*60) -- don't handle incoming connections, let them wait + end) + + -- register a thread that sleeps forever + copas.addthread(function() + copas.pause(60*60) + end) + + -- register a thread that waits on a socket read (connect to our own server) + local _, port = srv:getsockname() + copas.addthread(function() + local client = copas.wrap(socket.tcp()) + client:connect("127.0.0.1", port) + client:receive("*l") -- blocks waiting for data that never arrives + end) + + -- fire cancelall after a short delay + timer.new({ + delay = 0.5, + callback = function() + copas.cancelall() + end, + }) + + -- set up a timeout timer to make sure the loop exits within a reasonable time + timer.new({ + delay = 5, + callback = function() + print("loop 1 did not exit within 5 seconds") + os.exit(1) -- exit with error code to indicate failure + end, + }) +end) + +print("loop 1 exited ok") + +-- Test 2: after cancelall(), a fresh copas.loop() works correctly. + +local loop2_done = false + +copas.loop(function() + copas.addthread(function() + copas.pause(0.05) + loop2_done = true + end) +end) + +assert(loop2_done, "loop 2 did not complete its task") +print("loop 2 exited ok") + +print("all tests passed")