Skip to content

Commit 14a60e7

Browse files
authored
feat(exit): add exit signalling (#172)
1 parent 20f1cf7 commit 14a60e7

File tree

4 files changed

+125
-3
lines changed

4 files changed

+125
-3
lines changed

docs/index.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,8 @@ <h2><a name="history"></a>History</h2>
104104
<li>Change: Copas no longer requires LuaSocket, if no sockets are needed, LuaSystem will be enough as a fallback.</li>
105105
<li>Feat: added <code>copas.gettime()</code>, which transparently maps to either LuaSockets or
106106
LuaSystems implementation, ensuring independence of the availability of either one of those.</li>
107+
<li>Feat: Controlled exit of the Copas loop. Adding <code>copas.exit()</code>, <code>copas.exiting()</code>, and
108+
<code>copas.waitforexit()</code>.</li>
107109
</ul></dd>
108110

109111
<dt><strong>Copas 4.7.1</strong> [9/Mar/2024]</dt>

docs/reference.html

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,12 @@ <h3>Getting started examples</h3>
8181

8282
end
8383

84-
copas.addserver(server_socket, copas.handler(connection_handler,
85-
ssl_params), "my_TCP_server")
84+
copas.addthread(function()
85+
copas.addserver(server_socket, copas.handler(connection_handler,
86+
ssl_params), "my_TCP_server")
87+
copas.waitforexit()
88+
copas.removeserver(server_socket)
89+
end)
8690

8791
copas()
8892
</pre>
@@ -180,6 +184,29 @@ <h3>Copas dispatcher main functions</h3>
180184
truthy.</p>
181185
</dd>
182186

187+
<dt><strong><code>copas.exit()</code></strong></dt>
188+
<dd>
189+
<p>Sets a flag that the application is intending to exit. After calling
190+
this function <code>copas.exiting()</code> will be returning <code>true</code>, and
191+
all threads blocked on <code>copas.waitforexit()</code> will be released.</p>
192+
193+
<p>Copas itself will call this function when <code>copas.finished()</code> returns
194+
<code>true</code>.</p>
195+
</dd>
196+
197+
<dt><strong><code>bool = copas.exiting()</code></strong></dt>
198+
<dd>
199+
<p>Returns a flag indicating whether the application is supposed to exit.
200+
Returns <code>false</code> until after <code>copas.exit()</code> has been called,
201+
after which it will start returning <code>true</code>.</p>
202+
203+
<p>Clients should check whether they are to cease their operation and exit. They
204+
can do this by checking this flag, or by registering a task waiting on
205+
<code>copas.waitforexit()</code>. Clients should cancel pending work and close sockets
206+
when an exit is announced, otherwise Copas will not exit.
207+
</p>
208+
</dd>
209+
183210
<dt><strong><code>bool = copas.finished()</code></strong></dt>
184211
<dd>
185212
<p>Checks whether anything remains to be done.</p>
@@ -304,6 +331,15 @@ <h3>Copas dispatcher main functions</h3>
304331
currently running coroutine.</p>
305332
</dd>
306333

334+
<dt><strong><code>copas.waitforexit()</code></strong></dt>
335+
<dd>
336+
<p>This will block the calling coroutine until the <code>copas.exit()</code> function
337+
is called. Clients should check whether they are to cease their operation and exit. They
338+
can do this by waiting on this call, or by checking the <code>copas.exiting()</code> flag.
339+
Clients should cancel pending work and close sockets when an exit is announced, otherwise
340+
Copas will not exit.</p>
341+
</dd>
342+
307343
<dt><strong><code>skt = copas.wrap(skt [, sslparams] )</code></strong></dt>
308344
<dd>
309345
<p>Wraps a LuaSocket socket and returns a Copas socket that implements LuaSocket's API

src/copas.lua

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1666,6 +1666,38 @@ function copas.finished()
16661666
return #_reading == 0 and #_writing == 0 and _resumable:done() and _sleeping:done(copas.gettimeouts())
16671667
end
16681668

1669+
1670+
local resetexit do
1671+
local exit_semaphore, exiting
1672+
1673+
function resetexit()
1674+
exit_semaphore = copas.semaphore.new(1, 0, math.huge)
1675+
exiting = false
1676+
end
1677+
1678+
-- Signals tasks to exit. But only if they check for it. By calling `copas.exiting`
1679+
-- they can check if they should exit. Or by calling `copas.waitforexit` they can
1680+
-- wait until the exit signal is given.
1681+
function copas.exit()
1682+
if exiting then return end
1683+
exiting = true
1684+
exit_semaphore:destroy()
1685+
end
1686+
1687+
-- returns whether Copas is in the process of exiting. Exit can be started by
1688+
-- calling `copas.exit()`.
1689+
function copas.exiting()
1690+
return exiting
1691+
end
1692+
1693+
-- Pauses the current coroutine until Copas is exiting. To be used as an exit
1694+
-- signal for tasks that need to clean up before exiting.
1695+
function copas.waitforexit()
1696+
exit_semaphore:take(1)
1697+
end
1698+
end
1699+
1700+
16691701
local _getstats do
16701702
local _getstats_instrumented, _getstats_plain
16711703

@@ -1756,8 +1788,17 @@ function copas.loop(initializer, timeout)
17561788
timeout = initializer or timeout
17571789
end
17581790

1791+
resetexit()
17591792
copas.running = true
1760-
while not copas.finished() do copas.step(timeout) end
1793+
while true do
1794+
copas.step(timeout)
1795+
if copas.finished() then
1796+
if copas.exiting() then
1797+
break
1798+
end
1799+
copas.exit()
1800+
end
1801+
end
17611802
copas.running = false
17621803
end
17631804

tests/exittest.lua

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,3 +72,46 @@ copas.loop()
7272
assert(testran == 6, "Test 6 was not executed!")
7373
print("6) success")
7474

75+
print("7) Testing exiting releasing the exitsemaphore (implicit, no call to copas.exit)")
76+
copas.addthread(function()
77+
print("","7 running...")
78+
copas.addthread(function()
79+
copas.waitforexit()
80+
testran = 7
81+
end)
82+
end)
83+
copas.loop()
84+
assert(testran == 7, "Test 7 was not executed!")
85+
print("7) success")
86+
87+
print("8) Testing schduling new tasks while exiting (explicit exit by calling copas.exit)")
88+
testran = 0
89+
copas.addthread(function()
90+
print("","8 running...")
91+
copas.addthread(function()
92+
while true do
93+
copas.pause(0.1)
94+
testran = testran + 1
95+
print("count...")
96+
if testran == 3 then -- testran == 3
97+
print("initiating exit...")
98+
copas.exit()
99+
break
100+
end
101+
end
102+
end)
103+
copas.addthread(function()
104+
copas.waitforexit()
105+
print("exit signal received...")
106+
testran = testran + 1 -- testran == 4
107+
copas.addthread(function()
108+
print("running new task from exit handler...")
109+
copas.pause(1)
110+
testran = testran + 1 -- testran == 5
111+
print("new task from exit handler done!")
112+
end)
113+
end)
114+
end)
115+
copas.loop()
116+
assert(testran == 5, "Test 8 was not executed!")
117+
print("8) success")

0 commit comments

Comments
 (0)