fix(eventloop): isolate handler failures
This commit is contained in:
parent
4f7967513f
commit
4f2c64ef1d
@ -1,5 +1,5 @@
|
|||||||
# Host-side watchdog for the normal `just test` CraftOS-PC process.
|
# Host-side watchdog for the normal `just test` CraftOS-PC process.
|
||||||
TRAPOS_TEST_TIMEOUT_SECONDS=3
|
TRAPOS_TEST_TIMEOUT_SECONDS=4
|
||||||
|
|
||||||
# Dedicated `just test-timeout` fixture timings.
|
# Dedicated `just test-timeout` fixture timings.
|
||||||
TRAPOS_TEST_TIMEOUT_WATCHDOG_SECONDS=1
|
TRAPOS_TEST_TIMEOUT_WATCHDOG_SECONDS=1
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "TrapOS",
|
"name": "TrapOS",
|
||||||
"version": "0.12.3",
|
"version": "0.12.4",
|
||||||
"branch": "master",
|
"branch": "master",
|
||||||
"packages": [
|
"packages": [
|
||||||
"trapos"
|
"trapos"
|
||||||
|
|||||||
@ -1,13 +1,13 @@
|
|||||||
{
|
{
|
||||||
"packages": {
|
"packages": {
|
||||||
"trapos-core": "0.9.3",
|
"trapos-core": "0.9.4",
|
||||||
"trapos-test": "0.2.3",
|
"trapos-test": "0.2.3",
|
||||||
"trapos-boot": "0.4.2",
|
"trapos-boot": "0.4.3",
|
||||||
"trapos-net": "0.3.2",
|
"trapos-net": "0.3.2",
|
||||||
"trapos-ui": "0.2.4",
|
"trapos-ui": "0.2.4",
|
||||||
"trapos-ai": "0.8.0",
|
"trapos-ai": "0.8.0",
|
||||||
"trapos-cloud": "0.4.3",
|
"trapos-cloud": "0.4.4",
|
||||||
"trapos-sandbox-legacy": "0.3.2",
|
"trapos-sandbox-legacy": "0.3.2",
|
||||||
"trapos": "0.12.3"
|
"trapos": "0.12.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "trapos-boot",
|
"name": "trapos-boot",
|
||||||
"version": "0.4.2",
|
"version": "0.4.3",
|
||||||
"description": "TrapOS boot: startup MOTD and autostart server launcher",
|
"description": "TrapOS boot: startup MOTD and autostart server launcher",
|
||||||
"dependencies": ["trapos-core"],
|
"dependencies": ["trapos-core"],
|
||||||
"files": ["programs/motd.lua", "startup/boot.lua"],
|
"files": ["programs/motd.lua", "startup/boot.lua"],
|
||||||
|
|||||||
@ -45,7 +45,17 @@ if periphemu then
|
|||||||
periphemu.create("top", "modem")
|
periphemu.create("top", "modem")
|
||||||
end
|
end
|
||||||
|
|
||||||
_G.bootEventLoop = createEventLoop()
|
local function reportBootEventLoopError(eventName, err)
|
||||||
|
local message = "boot eventloop handler error (" .. tostring(eventName) .. "): " .. tostring(err)
|
||||||
|
local printErrorFn = _G.printError
|
||||||
|
if type(printErrorFn) == "function" then
|
||||||
|
printErrorFn(message)
|
||||||
|
else
|
||||||
|
print(message)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
_G.bootEventLoop = createEventLoop({ onError = reportBootEventLoopError })
|
||||||
|
|
||||||
local shellExited = false
|
local shellExited = false
|
||||||
local action = "shutdown"
|
local action = "shutdown"
|
||||||
|
|||||||
@ -228,6 +228,7 @@ local function createCloud()
|
|||||||
-- Inbound frames of an advertised type are re-emitted as trapos_cloud_request
|
-- Inbound frames of an advertised type are re-emitted as trapos_cloud_request
|
||||||
-- events; unadvertised (non-ping) types get an immediate unknown_type reply.
|
-- events; unadvertised (non-ping) types get an immediate unknown_type reply.
|
||||||
local servedTypes = {};
|
local servedTypes = {};
|
||||||
|
local scheduleReconnect = nil;
|
||||||
|
|
||||||
local function sendFrame(frame)
|
local function sendFrame(frame)
|
||||||
if not activeWs then
|
if not activeWs then
|
||||||
@ -253,10 +254,16 @@ local function createCloud()
|
|||||||
if not reconnecting then
|
if not reconnecting then
|
||||||
log('info', 'connecting', { url = url });
|
log('info', 'connecting', { url = url });
|
||||||
end
|
end
|
||||||
httpLike.websocketAsync(url);
|
local ok, err = pcall(function() httpLike.websocketAsync(url); end);
|
||||||
|
if not ok then
|
||||||
|
if not reconnecting then
|
||||||
|
log('warn', 'websocket open failed', { url = url, error = tostring(err) });
|
||||||
|
end
|
||||||
|
scheduleReconnect();
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function scheduleReconnect()
|
function scheduleReconnect()
|
||||||
activeWs = nil;
|
activeWs = nil;
|
||||||
if state == 'unauthorized' or state == 'displaced' then
|
if state == 'unauthorized' or state == 'displaced' then
|
||||||
return;
|
return;
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "trapos-cloud",
|
"name": "trapos-cloud",
|
||||||
"version": "0.4.3",
|
"version": "0.4.4",
|
||||||
"description": "TrapOS cloud gateway client: WS daemon (servers/cloud) + cloud command + MCP exec/write server",
|
"description": "TrapOS cloud gateway client: WS daemon (servers/cloud) + cloud command + MCP exec/write server",
|
||||||
"dependencies": ["trapos-core"],
|
"dependencies": ["trapos-core"],
|
||||||
"files": [
|
"files": [
|
||||||
|
|||||||
@ -209,6 +209,27 @@ testlib.test('startSession connects to the /gateway path', function()
|
|||||||
testlib.assertEquals(ctx.url, 'ws://host:4444/gateway');
|
testlib.assertEquals(ctx.url, 'ws://host:4444/gateway');
|
||||||
end);
|
end);
|
||||||
|
|
||||||
|
testlib.test('startSession retries when websocketAsync throws', function()
|
||||||
|
local http = { connects = {} };
|
||||||
|
function http.websocketAsync(url)
|
||||||
|
http.connects[#http.connects + 1] = url;
|
||||||
|
error('open boom', 0);
|
||||||
|
end
|
||||||
|
function http.websocket()
|
||||||
|
return nil;
|
||||||
|
end
|
||||||
|
|
||||||
|
local ctx = startSession({ http = http });
|
||||||
|
|
||||||
|
testlib.assertEquals(ctx.session.isReady(), false);
|
||||||
|
testlib.assertEquals(#http.connects, 1);
|
||||||
|
testlib.assertEquals(#ctx.el.timers, 1);
|
||||||
|
|
||||||
|
ctx.el.fireTimer(#ctx.el.timers);
|
||||||
|
testlib.assertEquals(#http.connects, 2);
|
||||||
|
testlib.assertEquals(#ctx.el.timers, 2);
|
||||||
|
end);
|
||||||
|
|
||||||
testlib.test('startSession sends hello on open and goes ready on ack', function()
|
testlib.test('startSession sends hello on open and goes ready on ack', function()
|
||||||
local ctx = startSession({ secret = 'hunter2' });
|
local ctx = startSession({ secret = 'hunter2' });
|
||||||
local ws = fakes.fakeWs();
|
local ws = fakes.fakeWs();
|
||||||
|
|||||||
@ -16,19 +16,26 @@
|
|||||||
-- end)
|
-- end)
|
||||||
--
|
--
|
||||||
-- events.runLoop();
|
-- events.runLoop();
|
||||||
local next_eventloop_id = 1;
|
local EVENTLOOP_COUNTER_KEY = '__trapos_next_eventloop_id';
|
||||||
|
|
||||||
|
local function nextEventLoopId()
|
||||||
|
local id = tonumber(_G[EVENTLOOP_COUNTER_KEY]) or 1;
|
||||||
|
_G[EVENTLOOP_COUNTER_KEY] = id + 1;
|
||||||
|
return id;
|
||||||
|
end
|
||||||
|
|
||||||
local function noop()
|
local function noop()
|
||||||
end
|
end
|
||||||
|
|
||||||
local STOP = '@libeventloop/STOP_HANDLER_SUBSCRIPTION';
|
local STOP = '@libeventloop/STOP_HANDLER_SUBSCRIPTION';
|
||||||
|
|
||||||
local function createEventLoop()
|
local function createEventLoop(opts)
|
||||||
|
opts = opts or {};
|
||||||
local api = {}
|
local api = {}
|
||||||
|
|
||||||
local runningLoop = false;
|
local runningLoop = false;
|
||||||
local eventloop_id = next_eventloop_id;
|
local eventloop_id = nextEventLoopId();
|
||||||
next_eventloop_id = next_eventloop_id + 1;
|
local stopRequested = false;
|
||||||
|
|
||||||
local shouldCheckHandlers = true;
|
local shouldCheckHandlers = true;
|
||||||
|
|
||||||
@ -58,6 +65,25 @@ local function createEventLoop()
|
|||||||
local nextOnStartHandlerId = 1;
|
local nextOnStartHandlerId = 1;
|
||||||
local onStartHandlers = {};
|
local onStartHandlers = {};
|
||||||
|
|
||||||
|
local function reportError(eventName, err)
|
||||||
|
if type(opts.onError) == 'function' then
|
||||||
|
pcall(opts.onError, eventName, err);
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function dispatchHandler(eventName, handler, ...)
|
||||||
|
if type(opts.onError) ~= 'function' then
|
||||||
|
return true, handler(...);
|
||||||
|
end
|
||||||
|
|
||||||
|
local ok, result = pcall(handler, ...);
|
||||||
|
if not ok then
|
||||||
|
reportError(eventName, result);
|
||||||
|
return false, nil;
|
||||||
|
end
|
||||||
|
return true, result;
|
||||||
|
end
|
||||||
|
|
||||||
local function resetOnStartHandlers()
|
local function resetOnStartHandlers()
|
||||||
nextOnStartHandlerId = 1;
|
nextOnStartHandlerId = 1;
|
||||||
onStartHandlers = {};
|
onStartHandlers = {};
|
||||||
@ -139,6 +165,7 @@ local function createEventLoop()
|
|||||||
|
|
||||||
local function resetAll()
|
local function resetAll()
|
||||||
runningLoop = false;
|
runningLoop = false;
|
||||||
|
stopRequested = false;
|
||||||
handlersCounter = 0;
|
handlersCounter = 0;
|
||||||
allHandlers = {};
|
allHandlers = {};
|
||||||
runningHandlers = nil;
|
runningHandlers = nil;
|
||||||
@ -183,7 +210,11 @@ local function createEventLoop()
|
|||||||
-- stopLoop
|
-- stopLoop
|
||||||
function api.stopLoop()
|
function api.stopLoop()
|
||||||
if api.isRunningLoop() then
|
if api.isRunningLoop() then
|
||||||
os.queueEvent(END_OF_LOOP)
|
if runningHandlers or runningTimeoutHandler then
|
||||||
|
stopRequested = true;
|
||||||
|
else
|
||||||
|
os.queueEvent(END_OF_LOOP)
|
||||||
|
end
|
||||||
else
|
else
|
||||||
error("libeventloop error: loop is already stopped")
|
error("libeventloop error: loop is already stopped")
|
||||||
end
|
end
|
||||||
@ -274,7 +305,7 @@ local function createEventLoop()
|
|||||||
for k, h in pairs(timeoutHandlers) do
|
for k, h in pairs(timeoutHandlers) do
|
||||||
if k == timerId then
|
if k == timerId then
|
||||||
removeTimeoutHandler(k)
|
removeTimeoutHandler(k)
|
||||||
h();
|
dispatchHandler('timer', h);
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
runningTimeoutHandler = false
|
runningTimeoutHandler = false
|
||||||
@ -291,8 +322,8 @@ local function createEventLoop()
|
|||||||
if handlers then
|
if handlers then
|
||||||
runningHandlers = eventName
|
runningHandlers = eventName
|
||||||
for _, handler in pairs(handlers) do
|
for _, handler in pairs(handlers) do
|
||||||
local result_handler = handler(table.unpack(packed))
|
local ok, result_handler = dispatchHandler(eventName, handler, table.unpack(packed))
|
||||||
if result_handler == api.STOP then
|
if ok and result_handler == api.STOP then
|
||||||
api.unregister(eventName, handler)
|
api.unregister(eventName, handler)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -300,11 +331,12 @@ local function createEventLoop()
|
|||||||
flushUnregisterQueue()
|
flushUnregisterQueue()
|
||||||
end
|
end
|
||||||
|
|
||||||
if eventName == END_OF_LOOP or (shouldCheckHandlers and eventName == 'terminate') then
|
end
|
||||||
execOnStopHandlers()
|
|
||||||
resetAll()
|
if stopRequested or eventName == END_OF_LOOP or (shouldCheckHandlers and eventName == 'terminate') then
|
||||||
break
|
execOnStopHandlers()
|
||||||
end
|
resetAll()
|
||||||
|
break
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "trapos-core",
|
"name": "trapos-core",
|
||||||
"version": "0.9.3",
|
"version": "0.9.4",
|
||||||
"description": "TrapOS base: package manager, event loop, upgrade and event tools",
|
"description": "TrapOS base: package manager, event loop, upgrade and event tools",
|
||||||
"dependencies": [],
|
"dependencies": [],
|
||||||
"files": [
|
"files": [
|
||||||
|
|||||||
@ -138,6 +138,127 @@ testlib.test('onStart and onStop run around loop', function()
|
|||||||
testlib.assertEquals(order, 'start>timeout>stop');
|
testlib.assertEquals(order, 'start>timeout>stop');
|
||||||
end);
|
end);
|
||||||
|
|
||||||
|
testlib.test('same-loop stop does not leak to a sibling loop with the same id', function()
|
||||||
|
local createEventLoopA = assert(loadfile('/apis/eventloop.lua'))();
|
||||||
|
local createEventLoopB = assert(loadfile('/apis/eventloop.lua'))();
|
||||||
|
local bootEvents = createEventLoopA();
|
||||||
|
local verifierEvents = createEventLoopB();
|
||||||
|
local bootProbeHandled = false;
|
||||||
|
|
||||||
|
bootEvents.register('eventloop_test_same_loop_probe', function()
|
||||||
|
bootProbeHandled = true;
|
||||||
|
bootEvents.stopLoop();
|
||||||
|
end);
|
||||||
|
|
||||||
|
verifierEvents.register('eventloop_test_same_loop_stop', function()
|
||||||
|
verifierEvents.stopLoop();
|
||||||
|
os.queueEvent('eventloop_test_same_loop_probe');
|
||||||
|
end);
|
||||||
|
|
||||||
|
os.queueEvent('eventloop_test_same_loop_stop');
|
||||||
|
parallel.waitForAll(function() bootEvents.runLoop(true); end, function() verifierEvents.runLoop(); end);
|
||||||
|
|
||||||
|
testlib.assertTrue(bootProbeHandled);
|
||||||
|
end);
|
||||||
|
|
||||||
|
testlib.test('external stop events use ids unique across module instances', function()
|
||||||
|
local createEventLoopA = assert(loadfile('/apis/eventloop.lua'))();
|
||||||
|
local createEventLoopB = assert(loadfile('/apis/eventloop.lua'))();
|
||||||
|
local bootEvents = createEventLoopA();
|
||||||
|
local verifierEvents = createEventLoopB();
|
||||||
|
local bootProbeHandled = false;
|
||||||
|
|
||||||
|
bootEvents.register('eventloop_test_external_stop', function()
|
||||||
|
verifierEvents.stopLoop();
|
||||||
|
os.queueEvent('eventloop_test_external_probe');
|
||||||
|
end);
|
||||||
|
|
||||||
|
bootEvents.register('eventloop_test_external_probe', function()
|
||||||
|
bootProbeHandled = true;
|
||||||
|
bootEvents.stopLoop();
|
||||||
|
end);
|
||||||
|
|
||||||
|
os.queueEvent('eventloop_test_external_stop');
|
||||||
|
parallel.waitForAll(function() bootEvents.runLoop(true); end, function() verifierEvents.runLoop(true); end);
|
||||||
|
|
||||||
|
testlib.assertTrue(bootProbeHandled);
|
||||||
|
end);
|
||||||
|
|
||||||
|
testlib.test('protected dispatch reports errors and keeps the loop alive', function()
|
||||||
|
local errors = {};
|
||||||
|
local events = createEventLoop({
|
||||||
|
onError = function(eventName, err)
|
||||||
|
errors[#errors + 1] = { eventName = eventName, err = err };
|
||||||
|
end,
|
||||||
|
});
|
||||||
|
local siblingCalled = false;
|
||||||
|
local laterCalled = false;
|
||||||
|
|
||||||
|
events.register('eventloop_test_protected', function()
|
||||||
|
error('handler boom', 0);
|
||||||
|
end);
|
||||||
|
|
||||||
|
events.register('eventloop_test_protected', function()
|
||||||
|
siblingCalled = true;
|
||||||
|
os.queueEvent('eventloop_test_protected_later');
|
||||||
|
end);
|
||||||
|
|
||||||
|
events.register('eventloop_test_protected_later', function()
|
||||||
|
laterCalled = true;
|
||||||
|
events.stopLoop();
|
||||||
|
end);
|
||||||
|
|
||||||
|
os.queueEvent('eventloop_test_protected');
|
||||||
|
events.runLoop();
|
||||||
|
|
||||||
|
testlib.assertEquals(#errors, 1);
|
||||||
|
testlib.assertEquals(errors[1].eventName, 'eventloop_test_protected');
|
||||||
|
testlib.assertTrue(string.find(tostring(errors[1].err), 'handler boom', 1, true) ~= nil);
|
||||||
|
testlib.assertTrue(siblingCalled);
|
||||||
|
testlib.assertTrue(laterCalled);
|
||||||
|
end);
|
||||||
|
|
||||||
|
testlib.test('protected timeout dispatch reports errors and keeps the loop alive', function()
|
||||||
|
local errors = {};
|
||||||
|
local events = createEventLoop({
|
||||||
|
onError = function(eventName, err)
|
||||||
|
errors[#errors + 1] = { eventName = eventName, err = err };
|
||||||
|
end,
|
||||||
|
});
|
||||||
|
local laterCalled = false;
|
||||||
|
|
||||||
|
events.setTimeout(function()
|
||||||
|
error('timeout boom', 0);
|
||||||
|
end, 0);
|
||||||
|
|
||||||
|
events.setTimeout(function()
|
||||||
|
laterCalled = true;
|
||||||
|
events.stopLoop();
|
||||||
|
end, 0);
|
||||||
|
|
||||||
|
events.runLoop();
|
||||||
|
|
||||||
|
testlib.assertEquals(#errors, 1);
|
||||||
|
testlib.assertEquals(errors[1].eventName, 'timer');
|
||||||
|
testlib.assertTrue(string.find(tostring(errors[1].err), 'timeout boom', 1, true) ~= nil);
|
||||||
|
testlib.assertTrue(laterCalled);
|
||||||
|
end);
|
||||||
|
|
||||||
|
testlib.test('timeout handler can stop a loop with persistent handlers', function()
|
||||||
|
local events = createEventLoop();
|
||||||
|
|
||||||
|
events.register('eventloop_test_never_fired', function()
|
||||||
|
end);
|
||||||
|
|
||||||
|
events.setTimeout(function()
|
||||||
|
events.stopLoop();
|
||||||
|
end, 0);
|
||||||
|
|
||||||
|
events.runLoop();
|
||||||
|
|
||||||
|
testlib.assertTrue(not events.isRunningLoop());
|
||||||
|
end);
|
||||||
|
|
||||||
testlib.test('empty loop returns and runs onStop', function()
|
testlib.test('empty loop returns and runs onStop', function()
|
||||||
local events = createEventLoop();
|
local events = createEventLoop();
|
||||||
local stopped = false;
|
local stopped = false;
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "trapos",
|
"name": "trapos",
|
||||||
"version": "0.12.3",
|
"version": "0.12.4",
|
||||||
"description": "TrapOS full install meta-package",
|
"description": "TrapOS full install meta-package",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"trapos-boot",
|
"trapos-boot",
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user