• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Tieske / corowatch / 3756454408

pending completion
3756454408

push

github

GitHub
fix(readme) batch for test

82 of 119 relevant lines covered (68.91%)

0.65 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

49.58
/src/corowatch.lua
1
---------------------------------------------------------------------------------------
2
-- Module to watch coroutine executiontime. Coroutines running too long without
3
-- yielding can be killed to prevent them from locking the Lua state.
4
-- The module uses `LuaSocket` to get the time (`socket.gettime` function). If you
5
-- do not want that, override the `coroutine.gettime` method with your own
6
-- implementation.
7
--
8
-- @copyright Copyright (c) 2013-2022 Thijs Schreijer
9
-- @author Thijs Schreijer
10
-- @license MIT, see `LICENSE.md`.
11
-- @name corowatch
12
-- @class module
13

14
local M = {}    -- module table
1✔
15
M._VERSION = "1.0"
1✔
16
M._COPYRIGHT = "Copyright (c) 2013-2022 Thijs Schreijer"
1✔
17
M._DESCRIPTION = "Lua module to watch coroutine usage and kill a coroutine if it fails to yield in a timely manner."
1✔
18

19

20
local corocreate = coroutine.create
1✔
21
local cororesume = coroutine.resume
1✔
22
local cororunning = coroutine.running
1✔
23
local corostatus = coroutine.status
1✔
24
local corowrap = coroutine.wrap
1✔
25
local coroyield = coroutine.yield
1✔
26
local _sethook = debug.sethook
1✔
27
local traceback = debug.traceback
1✔
28
local watch, resume, create
29
local hookcount = 10000
1✔
30

31

32
local _unpack = table.unpack or unpack
1✔
33
local function pack (...) return { n = select('#', ...), ...} end
1✔
34
local function unpack(t, i, j) return _unpack(t, i or 1, j or t.n or #t) end
1✔
35

36

37
-- create watch register; table indexed by coro with watch properties
38
local register = setmetatable({},{__mode = "k"})  -- set weak keys
1✔
39
-- register element = {
40
--   twarnlimit = when to warn, seconds
41
--   tkilllimit = when to kill, seconds
42
--   cb = callback, function
43
--   warned = boolean; was cwwarn called already?
44
--   killtime = point in time after which to kill the coroutine
45
--   warntime = point in time after which to warn
46
--   errmsg = errormessage if timedout, so if set, the coro should be dead
47
--   hook = function; the debughook in use
48
--   debug = debug.traceback() at the time of protecting
49
-- }
50

51

52
local mainthread = {}  -- for 5.1 where there is no main thread access, running() returns nil
1✔
53
local function sethook(coro, ...)  -- compatibility for Lua 5.1
54
  if coro == mainthread then
1✔
55
    return _sethook(...)
×
56
  end
57
  return _sethook(coro, ...)
1✔
58
end
59

60
-- Gets the entry for the coro from the coroutine register.
61
-- @return the coroutine entry from the register
62
local getwatch = function(coro)
63
  return register[coro or cororunning() or mainthread]
2✔
64
end
65

66

67
-- Debughook function, to check for timeouts
68
local checkhook = function()
69
  local e = getwatch()
×
70
  if not e then return end  -- not being watched
×
71
  local t = M.gettime()
×
72
  if e.errmsg then
×
73
    -- the coro is tagged with an error. This means that somewhere the error
74
    -- was thrown, but the coro didn't die (probably a pcall in between). The coro should have died
75
    -- but didn't so it is in an 'undetermined' state. So kill again.
76
    error(e.errmsg,2)
×
77
  elseif not e.warned and e.warntime and e.warntime < t then
×
78
    -- warn now
79
    if e.cb("warn") then
×
80
      -- returned positive, so reset timeouts
81
      if e.tkilllimit then e.killtime = t + e.tkilllimit end
×
82
      if e.twarnlimit then e.warntime = t + e.twarnlimit end
×
83
    else
84
      e.warned = true
×
85
    end
86
  elseif e.killtime and e.killtime < t then
×
87
    if e.cb then e.cb("kill") end
×
88
    -- run hook now every instruction, to kill again and again if it persists (pcall/cboundary)
89
    sethook(cororunning(), e.hook, "", 1)
×
90
    -- kill now
91
    e.errmsg = "Coroutine exceeded its allowed running time of "..tostring(e.tkilllimit).." seconds, without yielding"
×
92
    if e.debug then
×
93
      e.errmsg = e.errmsg ..
×
94
       "\n============== traceback at coroutine creation time ====================\n" ..
×
95
       e.debug ..
×
96
       "\n======================== end of traceback =============================="
97
    end
98
    error(e.errmsg, 2)
×
99
  end
100
end
101

102
-- Creates an entry in the coroutine register. If it already exists, existing values
103
-- will be overwritten (in the existing entry) with the new values.
104
-- @param coro coroutine for which to create the entry
105
-- @return the entry created
106
local createwatch = function(coro, tkilllimit, twarnlimit, cb)
107
  coro = coro or cororunning() or mainthread
1✔
108
  local entry = register[coro] or {}
1✔
109
  entry.tkilllimit = tkilllimit
1✔
110
  entry.twarnlimit = twarnlimit
1✔
111
  entry.cb = cb
1✔
112
  entry.hook = checkhook
1✔
113
  entry.debug = traceback()
1✔
114
  sethook(coro, entry.hook, "", hookcount)
1✔
115
  register[coro] = entry
×
116
  return entry
×
117
end
118

119
---------------------------------------------------------------------------------------
120
-- returns current time in seconds. If not overridden, it will require `luasocket` and use
121
-- `socket.gettime` to get the current time.
122
M.gettime = function()
123
  M.gettime = require("socket").gettime
×
124
  return M.gettime()
×
125
end
126

127
---------------------------------------------------------------------------------------
128
-- Protects a coroutine from running too long without yielding.
129
-- The callback has 1 parameter (string value being either "warn" or "kill"), but runs
130
-- on the coroutine that is subject of the warning. If the "warn" callback returns a
131
-- truthy value (neither `false`, nor `nil`) then the timeouts for kill and warn limits
132
-- will be reset (buying more time for the coroutine to finish its business).
133
--
134
-- NOTE: the callback runs inside a debughook.
135
-- @tparam coroutine|nil coro coroutine to be protected, defaults to the currently running routine
136
-- @tparam number|nil tkilllimit time in seconds it is allowed to run without yielding
137
-- @tparam number|nil twarnlimit time in seconds it is allowed before `cb` is called
138
-- (must be smaller than `tkilllimit`)
139
-- @tparam function|nil cb callback executed when the kill or warn limit is reached.
140
-- @return coro
141
M.watch = function(coro, tkilllimit, twarnlimit, cb)
142
  if getwatch(coro) then error("Cannot create a watch, there already is one") end
2✔
143
  assert(tkilllimit or twarnlimit, "Either kill limit or warn limit must be provided")
1✔
144
  if twarnlimit then assert(cb, "A callback function must be provided when adding a warnlimit") end
1✔
145
  if tkilllimit and twarnlimit then assert(tkilllimit>twarnlimit, "The warnlimit must be smaller than the killlimit") end
1✔
146
  createwatch(coro, tkilllimit, twarnlimit, cb)
1✔
147
  return coro
×
148
end
149
watch = M.watch
1✔
150

151
---------------------------------------------------------------------------------------
152
-- This is the same as the regular `coroutine.create`, except that when the running
153
-- coroutine is watched, then children spawned will also be watched with the same
154
-- settings.
155
-- @param f see `coroutine.create`
156
M.create = function(f)
157
  local s = getwatch(cororunning())
1✔
158
  if not s then return corocreate(f) end  -- I'm not being watched
1✔
159
  -- create and add watch
160
  return watch(corocreate(f), s.tkilllimit, s.twarnlimit, s.cb)
×
161
end
162
create = M.create
1✔
163

164
---------------------------------------------------------------------------------------
165
-- This is the same as the regular `coroutine.wrap`, except that when the running
166
-- coroutine is watched, then children spawned will also be watched with the same
167
-- settings. To set sepecific settings for watching use `coroutine.wrapf`.
168
-- @param f see `coroutine.wrap`
169
-- @see wrapf
170
M.wrap = function(f)
171
  if not getwatch(cororunning()) then return corowrap(f) end  -- not watched
×
172
  local coro = create(f)
×
173
  return function(...) return resume(coro, ...) end
×
174
end
175

176
---------------------------------------------------------------------------------------
177
-- This is the same as the regular `coroutine.wrap`, except that the coroutine created
178
-- is watched according to the parameters provided, and not according to the watch
179
-- parameters of the currently running coroutine.
180
-- @tparam function f function to wrap
181
-- @tparam number|nil tkilllimit see `watch`
182
-- @tparam number|nil twarnlimit see `watch`
183
-- @tparam function|nil cb see `watch`
184
-- @see create
185
-- @see wrap
186
M.wrapf = function(f, tkilllimit, twarnlimit, cb)
187
  local coro = watch(corocreate(f), tkilllimit, twarnlimit, cb)
×
188
  return function(...) return resume(coro, ...) end
×
189
end
190

191
---------------------------------------------------------------------------------------
192
-- This is the same as the regular `coroutine.resume`.
193
-- @param coro see `coroutine.resume`
194
-- @param ... see `coroutine.resume`
195
M.resume = function(coro, ...)
196
  assert(type(coro) == "thread", "Expected thread, got "..type(coro))
×
197
  local e = getwatch(coro)
×
198
  if e then
×
199
    if e.errmsg then
×
200
      -- the coro being resumed is tagged with an error. This means that somewhere the error
201
      -- was thrown, but the coro didn't die (probably a pcall in between). The coro should have died
202
      -- but didn't so it is in an 'undetermined' state. Return error, don't resume
203
      return false, e.errmsg
×
204
    end
205
    local t = M.gettime()
×
206
    if e.tkilllimit then e.killtime = t + e.tkilllimit end
×
207
    if e.twarnlimit then e.warntime = t + e.twarnlimit end
×
208
    e.warned = nil
×
209
  end
210
  local r = pack(cororesume(coro, ...))
×
211
  if e and e.errmsg then
×
212
    return false, e.errmsg
×
213
  else
214
    return unpack(r)
×
215
  end
216
end
217
resume = M.resume
1✔
218

219
---------------------------------------------------------------------------------------
220
-- This is the same as the regular `coroutine.yield`.
221
-- @param ... see `coroutine.yield`
222
M.yield = function(...)
223
  local e = getwatch()
×
224
  if e then
×
225
    if e.errmsg then
×
226
      -- the coro is yielding, while it is tagged with an error. This means that somewhere the error
227
      -- was thrown, but the coro didn't die (probably a pcall in between). The coro should have died
228
      -- but didn't so it is in an 'undetermined' state. So kill again.
229
      error(e.errmsg,2)
×
230
    end
231
    e.killtime = nil
×
232
    e.warntime = nil
×
233
    e.warned = nil
×
234
  end
235
  return coroyield(...)
×
236
end
237

238
---------------------------------------------------------------------------------------
239
-- This is the same as the regular `coroutine.status`.
240
-- @param coro see `coroutine.status`
241
M.status = function(coro)
242
  if (getwatch(coro) or {}).errmsg then
×
243
    return "dead"
×
244
  else
245
    return corostatus(coro)
×
246
  end
247
end
248

249
---------------------------------------------------------------------------------------
250
-- This is the same as the regular `debug.sethook`, except that when trying to set a
251
-- hook on a coroutine that is being watched, if will throw an error.
252
-- @param coro see `debug.sethook`
253
-- @param ... see `debug.sethook`
254
M.sethook = function(coro, ...)
255
  if getwatch(coro) then
×
256
    error("Cannot set a debughook because corowatch is watching this coroutine", 2)
×
257
  end
258
  -- not watched, so do the regular thing
259
  return sethook(coro, ...)
×
260
end
261

262
---------------------------------------------------------------------------------------
263
-- Export the corowatch functions to an external table, or the global environment.
264
-- The functions exported are `create`, `yield`, `resume`, `status`, `wrap`, and `wrapf`. The standard
265
-- `coroutine.running` will be added if there is no `running` value in the table yet. So
266
-- basically it exports a complete `coroutine` table + `wrapf`.
267
-- If the provided table contains subtables `coroutine` and/or `debug` then it is assumed to
268
-- be a function/global environment and `sethook` will be exported as well (exports will then
269
-- go into the two subtables)
270
-- @tparam[opt] table t table to which to export the coroutine functions.
271
-- @return the table provided, or a new table if non was provided, with the exported functions
272
-- @usage
273
-- -- monkey patch global environment, both coroutine and debug tables
274
-- require("corowatch").export(_G)
275
M.export = function(t)
276
  t = t or {}
2✔
277
  local c, d
278
  assert(type(t) == "table", "Expected table, got "..type(t))
2✔
279
  if (type(t.debug) == "table") or type(t.coroutine == "table") then
2✔
280
    -- we got a global table, so monkeypatch debug and coroutine table
281
    d = t.debug
2✔
282
    c = t.coroutine
2✔
283
  else
284
    -- we got something else, just export coroutine here
285
    d = nil
×
286
    c = t
×
287
  end
288
  if type(d) == "table" then
2✔
289
    d.sethook = M.sethook
2✔
290
  end
291
  if type(c) == "table" then
2✔
292
    c.yield = M.yield
2✔
293
    c.create = M.create
2✔
294
    c.resume = M.resume
2✔
295
    c.running = c.running or cororunning
2✔
296
    c.status = M.status
2✔
297
    c.wrap = M.wrap
2✔
298
    c.wrapf = M.wrapf
2✔
299
  end
300
  return t
2✔
301
end
302

303
-- export some internals for testing if requested
304
if _TEST then  -- luacheck: ignore
1✔
305
  M._register = register
1✔
306
  M._getwatch = getwatch
1✔
307
end
308

309
return M
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc