Skip to content

Commit

Permalink
feat(exit): add exit signalling (#172)
Browse files Browse the repository at this point in the history
  • Loading branch information
Tieske authored Aug 29, 2024
1 parent 20f1cf7 commit 14a60e7
Show file tree
Hide file tree
Showing 4 changed files with 125 additions and 3 deletions.
2 changes: 2 additions & 0 deletions docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ <h2><a name="history"></a>History</h2>
<li>Change: Copas no longer requires LuaSocket, if no sockets are needed, LuaSystem will be enough as a fallback.</li>
<li>Feat: added <code>copas.gettime()</code>, which transparently maps to either LuaSockets or
LuaSystems implementation, ensuring independence of the availability of either one of those.</li>
<li>Feat: Controlled exit of the Copas loop. Adding <code>copas.exit()</code>, <code>copas.exiting()</code>, and
<code>copas.waitforexit()</code>.</li>
</ul></dd>

<dt><strong>Copas 4.7.1</strong> [9/Mar/2024]</dt>
Expand Down
40 changes: 38 additions & 2 deletions docs/reference.html
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,12 @@ <h3>Getting started examples</h3>

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()
</pre>
Expand Down Expand Up @@ -180,6 +184,29 @@ <h3>Copas dispatcher main functions</h3>
truthy.</p>
</dd>

<dt><strong><code>copas.exit()</code></strong></dt>
<dd>
<p>Sets a flag that the application is intending to exit. After calling
this function <code>copas.exiting()</code> will be returning <code>true</code>, and
all threads blocked on <code>copas.waitforexit()</code> will be released.</p>

<p>Copas itself will call this function when <code>copas.finished()</code> returns
<code>true</code>.</p>
</dd>

<dt><strong><code>bool = copas.exiting()</code></strong></dt>
<dd>
<p>Returns a flag indicating whether the application is supposed to exit.
Returns <code>false</code> until after <code>copas.exit()</code> has been called,
after which it will start returning <code>true</code>.</p>

<p>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
<code>copas.waitforexit()</code>. Clients should cancel pending work and close sockets
when an exit is announced, otherwise Copas will not exit.
</p>
</dd>

<dt><strong><code>bool = copas.finished()</code></strong></dt>
<dd>
<p>Checks whether anything remains to be done.</p>
Expand Down Expand Up @@ -304,6 +331,15 @@ <h3>Copas dispatcher main functions</h3>
currently running coroutine.</p>
</dd>

<dt><strong><code>copas.waitforexit()</code></strong></dt>
<dd>
<p>This will block the calling coroutine until the <code>copas.exit()</code> 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 <code>copas.exiting()</code> flag.
Clients should cancel pending work and close sockets when an exit is announced, otherwise
Copas will not exit.</p>
</dd>

<dt><strong><code>skt = copas.wrap(skt [, sslparams] )</code></strong></dt>
<dd>
<p>Wraps a LuaSocket socket and returns a Copas socket that implements LuaSocket's API
Expand Down
43 changes: 42 additions & 1 deletion src/copas.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down
43 changes: 43 additions & 0 deletions tests/exittest.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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")

0 comments on commit 14a60e7

Please sign in to comment.