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

lunarmodules / copas / 3797080599

pending completion
3797080599

push

github

Thijs Schreijer
feat(timeouts) allow math.huge to wait forever

4 of 4 new or added lines in 2 files covered. (100.0%)

1266 of 1488 relevant lines covered (85.08%)

14936.03 hits per line

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

93.26
/src/copas/lock.lua
1
local copas = require("copas")
2✔
2
local gettime = require("socket").gettime
2✔
3

4
local DEFAULT_TIMEOUT = 10
2✔
5

6
local lock = {}
2✔
7
lock.__index = lock
2✔
8

9

10
-- registry, locks indexed by the coroutines using them.
11
local registry = setmetatable({}, { __mode="kv" })
2✔
12

13

14

15
--- Creates a new lock.
16
-- @param seconds (optional) default timeout in seconds when acquiring the lock (defaults to 10),
17
-- set to `math.huge` to have no timeout.
18
-- @param not_reentrant (optional) if truthy the lock will not allow a coroutine to grab the same lock multiple times
19
-- @return the lock object
20
function lock.new(seconds, not_reentrant)
2✔
21
  local timeout = tonumber(seconds or DEFAULT_TIMEOUT) or -1
6✔
22
  if timeout < 0 then
6✔
23
    error("expected timeout (1st argument) to be a number greater than or equal to 0, got: " .. tostring(seconds), 2)
×
24
  end
25
  return setmetatable({
6✔
26
            timeout = timeout,
6✔
27
            not_reentrant = not_reentrant,
6✔
28
            queue = {},
6✔
29
            q_tip = 0,  -- index of the first in line waiting
30
            q_tail = 0, -- index where the next one will be inserted
31
            owner = nil, -- coroutine holding lock currently
32
            call_count = nil, -- recursion call count
33
            errors = setmetatable({}, { __mode = "k" }), -- error indexed by coroutine
6✔
34
          }, lock)
6✔
35
end
36

37

38

39
do
40
  local destroyed_func = function()
41
    return nil, "destroyed"
2✔
42
  end
43

44
  local destroyed_lock_mt = {
2✔
45
    __index = function()
46
      return destroyed_func
2✔
47
    end
48
  }
49

50
  --- destroy a lock.
51
  -- Releases all waiting threads with `nil+"destroyed"`
52
  function lock:destroy()
2✔
53
    --print("destroying ",self)
54
    for i = self.q_tip, self.q_tail do
262✔
55
      local co = self.queue[i]
256✔
56
      self.queue[i] = nil
256✔
57

58
      if co then
256✔
59
        self.errors[co] = "destroyed"
249✔
60
        --print("marked destroyed ", co)
61
        copas.wakeup(co)
249✔
62
      end
63
    end
64

65
    if self.owner then
6✔
66
      self.errors[self.owner] = "destroyed"
6✔
67
      --print("marked destroyed ", co)
68
    end
69
    self.queue = {}
6✔
70
    self.q_tip = 0
6✔
71
    self.q_tail = 0
6✔
72
    self.destroyed = true
6✔
73

74
    setmetatable(self, destroyed_lock_mt)
6✔
75
    return true
6✔
76
  end
77
end
78

79

80
local function timeout_handler(co)
81
  local self = registry[co]
252✔
82
  if not self then
252✔
83
    return
×
84
  end
85

86
  for i = self.q_tip, self.q_tail do
31,377✔
87
    if co == self.queue[i] then
31,377✔
88
      self.queue[i] = nil
252✔
89
      self.errors[co] = "timeout"
252✔
90
      --print("marked timeout ", co)
91
      copas.wakeup(co)
252✔
92
      return
252✔
93
    end
94
  end
95
  -- if we get here, we own it currently, or we finished it by now, or
96
  -- the lock was destroyed. Anyway, nothing to do here...
97
end
98

99

100
--- Acquires the lock.
101
-- If the lock is owned by another thread, this will yield control, until the
102
-- lock becomes available, or it times out.
103
-- If `timeout == 0` then it will immediately return (without yielding).
104
-- @param timeout (optional) timeout in seconds, defaults to the timeout passed to `new` (use `math.huge` to have no timeout).
105
-- @return wait-time on success, or nil+error+wait_time on failure. Errors can be "timeout", "destroyed", or "lock is not re-entrant"
106
function lock:get(timeout)
2✔
107
  local co = coroutine.running()
759✔
108
  local start_time
109

110
  -- is the lock already taken?
111
  if self.owner then
759✔
112
    -- are we re-entering?
113
    if co == self.owner and not self.not_reentrant then
502✔
114
      self.call_count = self.call_count + 1
×
115
      return 0
×
116
    end
117

118
    self.queue[self.q_tail] = co
502✔
119
    self.q_tail = self.q_tail + 1
502✔
120
    timeout = timeout or self.timeout
502✔
121
    if timeout == 0 then
502✔
122
      return nil, "timeout", 0
×
123
    end
124

125
    -- set up timeout
126
    registry[co] = self
502✔
127
    copas.timeout(timeout, timeout_handler)
502✔
128

129
    start_time = gettime()
502✔
130
    copas.pauseforever()
502✔
131

132
    local err = self.errors[co]
502✔
133
    self.errors[co] = nil
502✔
134
    registry[co] = nil
502✔
135

136
    --print("released ", co, err)
137
    if err ~= "timeout" then
502✔
138
      copas.timeout(0)
250✔
139
    end
140
    if err then
502✔
141
      return nil, err, gettime() - start_time
502✔
142
    end
143
  end
144

145
  -- it's ours to have
146
  self.owner = co
257✔
147
  self.call_count = 1
257✔
148
  return start_time and (gettime() - start_time) or 0
257✔
149
end
150

151

152
--- Releases the lock currently held.
153
-- Releasing a lock that is not owned by the current co-routine will return
154
-- an error.
155
-- returns true, or nil+err on an error
156
function lock:release()
2✔
157
  local co = coroutine.running()
253✔
158

159
  if co ~= self.owner then
253✔
160
    return nil, "cannot release a lock not owned"
1✔
161
  end
162

163
  self.call_count = self.call_count - 1
252✔
164
  if self.call_count > 0 then
252✔
165
    -- same coro is still holding it
166
    return true
×
167
  end
168

169
  -- need a loop, since individual coroutines might have been removed
170
  -- so there might be holes
171
  while self.q_tip < self.q_tail do
503✔
172
    local next_up = self.queue[self.q_tip]
252✔
173
    if next_up then
252✔
174
      self.owner = next_up
1✔
175
      self.queue[self.q_tip] = nil
1✔
176
      self.q_tip = self.q_tip + 1
1✔
177
      copas.wakeup(next_up)
1✔
178
      return true
1✔
179
    end
180
    self.q_tip = self.q_tip + 1
251✔
181
  end
182
  -- queue is empty, reset pointers
183
  self.owner = nil
251✔
184
  self.q_tip = 0
251✔
185
  self.q_tail = 0
251✔
186
  return true
251✔
187
end
188

189

190

191
return lock
2✔
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