Skip to content

Commit

Permalink
Cancel no-longer-needed timers
Browse files Browse the repository at this point in the history
Several functions accept a "timeout" argument, which is implemented by
starting a timer, and then racing the desired output against the timer
event.

However, if the timer never wins, we weren't cancelling the timer, and
so it was still queued. This is especially problematic if dozens or
hundreds of rednet (or websocket) messages are received in quick
succession, as we could fill the entire event queue, and stall the
computer.

See #1995
  • Loading branch information
SquidDev committed Nov 10, 2024
1 parent 3293639 commit b742745
Show file tree
Hide file tree
Showing 5 changed files with 54 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ public final MethodResult receive(Optional<Double> timeout) throws LuaException
? environment.startTimer(Math.round(checkFinite(0, timeout.get()) / 0.05))
: -1;

return new ReceiveCallback(timeoutId).pull;
return new ReceiveCallback(environment, timeoutId).pull;
}

/**
Expand Down Expand Up @@ -110,18 +110,22 @@ private void checkOpen() throws LuaException {

private final class ReceiveCallback implements ILuaCallback {
final MethodResult pull = MethodResult.pullEvent(null, this);
private final IAPIEnvironment environment;
private final int timeoutId;

ReceiveCallback(int timeoutId) {
ReceiveCallback(IAPIEnvironment environment, int timeoutId) {
this.timeoutId = timeoutId;
this.environment = environment;
}

@Override
public MethodResult resume(Object[] event) {
if (event.length >= 3 && Objects.equals(event[0], MESSAGE_EVENT) && Objects.equals(event[1], address)) {
environment.cancelTimer(timeoutId);
return MethodResult.of(Arrays.copyOfRange(event, 2, event.length));
} else if (event.length >= 2 && Objects.equals(event[0], CLOSE_EVENT) && Objects.equals(event[1], address) && websocket.isClosed()) {
// If the socket is closed abort.
environment.cancelTimer(timeoutId);
return MethodResult.of();
} else if (event.length >= 2 && timeoutId != -1 && Objects.equals(event[0], TIMER_EVENT)
&& event[1] instanceof Number id && id.intValue() == timeoutId) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,8 @@ function locate(_nTimeout, _bDebug)
modem.close(CHANNEL_GPS)
end

os.cancelTimer(timeout)

-- Return the response
if pos1 and pos2 then
if _bDebug then
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,7 @@ function receive(protocol_filter, timeout)
-- Return the first matching rednet_message
local sender_id, message, protocol = p1, p2, p3
if protocol_filter == nil or protocol == protocol_filter then
if timer then os.cancelTimer(timer) end
return sender_id, message, protocol
end
elseif event == "timer" then
Expand Down Expand Up @@ -431,6 +432,7 @@ function lookup(protocol, hostname)
if hostname == nil then
table.insert(results, sender_id)
elseif message.sHostname == hostname then
os.cancelTimer(timer)
return sender_id
end
end
Expand All @@ -440,6 +442,9 @@ function lookup(protocol, hostname)
break
end
end

os.cancelTimer(timer)

if results then
return table.unpack(results)
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,36 @@ describe("The rednet library", function()
fake_computer.run_all { computer_1, computer_2 }
end)

describe("timeouts", function()
it("can time out messages", function()
local computer = computer_with_rednet(1, function(rednet)
local id = rednet.receive(1)
expect(id):eq(nil)
end, { open = true })

local computers = { computer }
fake_computer.run_all(computers, false)
fake_computer.advance_all(computers, 1)
fake_computer.run_all(computers)
end)

it("cancels the pending timer", function()
local computer = computer_with_rednet(1, function(rednet)
-- Send a message to ourselves with a timer
rednet.send(1, "hello")
local id = rednet.receive(1)
expect(id):eq(1)
end, { open = true })

fake_computer.run_all({ computer })

-- Our pending timer list only contains the rednet.run timer.
expect(computer.pending_timers):same {
[debugx.getupvalue(computer.env.rednet.run, "prune_received_timer")] = 10,
}
end)
end)

it("repeats messages between computers", function()
local computer_1, modem_1 = computer_with_rednet(1, function(rednet)
rednet.send(3, "Hello")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ local function make_computer(id, fn)
pending_timers[t], next_timer = clock + delay, next_timer + 1
return t
end,
cancelTimer = function(id) pending_timers[id] = nil end,
clock = function() return clock end,
sleep = function(time)
local timer = env.os.startTimer(time or 0)
Expand Down Expand Up @@ -98,7 +99,16 @@ local function make_computer(id, fn)

local position = vector.new(0, 0, 0)

return { env = env, peripherals = peripherals, queue_event = queue_event, step = step, co = co, advance = advance, position = position }
return {
env = env,
peripherals = peripherals,
queue_event = queue_event,
step = step,
co = co,
advance = advance,
position = position,
pending_timers = pending_timers,
}
end

local function parse_channel(c)
Expand Down

0 comments on commit b742745

Please sign in to comment.