Good evening my esteemed colleagues.
I am implementing a communication library which requires concurrency to work correctly.
The structure of my application could be represented as the following diagram:
setup -> taskAcyclic: configuration state machine
-> taskCyclic
setupallocates the memory / resources / arranges various const structures- after setup we begin
taskAcyclic taskAcyclic begets taskCyclic.- The lifetime of
taskAcyclicexceedstaskCyclic. (structured concurrency)
taskCyclic is rather simple:
fn taskCyclic(
io: std.Io,
md: *gcat.MainDevice,
pdi_write_mutex: *std.Io.Mutex,
max_recv_timeouts: u32,
maybe_zh: ?*ZenohHandler,
config: Config,
stop_event: std.Io.Event,
) error{NonRecoverable}!void {
var recv_timeouts: u32 = 0;
while (!stop_event.isSet()) {
// must protect the process data
// from zenoh during the recv()
{
pdi_write_mutex.lockUncancelable(io);
defer pdi_write_mutex.unlock(io);
if (md.recv(io)) |cyclic_result| {
md.check(cyclic_result) catch |err| switch (err) {
error.NotAllSubdevicesInOP,
error.TopologyChanged,
error.Wkc,
=> |err2| {
std.log.err("failure out of .op: {s}", .{@errorName(err2)});
// TODO: emit notification?
},
};
recv_timeouts = 0;
} else |err| switch (err) {
error.RecvTimeout => {
std.log.warn("recv timeout!", .{});
recv_timeouts +|= 1;
if (recv_timeouts >= max_recv_timeouts) {
std.log.err("recv timeouts exhausted, rescanning bus", .{});
// TODO: emit notification?
}
},
error.LinkError,
=> return error.NonRecoverable,
}
// to reduce jitter, the lock is maintained though send
md.send(io) catch |err| switch (err) {
error.LinkError, error.Unexpected => return error.NonRecoverable,
};
}
if (maybe_zh) |*zh| {
zh.publishInputsOutputs(&md, config.value) catch |err| {
std.log.err("failed to publish inputs / outputs on zenoh: {s}", .{@errorName(err)});
return error.NonRecoverable; // TODO: correct action here?
};
}
md.sleep(io) catch |err| switch (err) {
error.Unexpected, error.UnsupportedClock, error.Canceled => |err2| {
std.log.err("sleep failure: {}", .{err2});
return error.NonRecoverable;
},
};
}
}
The key feature of taskCyclic is that it has a fixed cycle time.
On an ideal computer:
md.recv(io)consumes approx 100 usmd.send(io)consumes approx 100 us- other application code in the loop consumes approx 100 us
md.sleep(io)sleeps for the remainder of the time in the cycle- repeat
The problems with taskCyclic:
- there exists a certain set of “non-recoverable” errors that can only be recovered by unwinding up to taskAcyclic for reinitialization / retry logic.
- there exists a certain set of “recoverable” errors that can be recovered via prompt action by
taskAcyclic, but neverthelesstaskCyclicmust remain operating at fixed cycle time.
Now for taskAcyclic, I am unsure of what the shape of this task looks like:
perhaps some pseudocode:
fn taskAcyclc(io: std.Io, md: *MainDevice) !void {
spawnCyclicTask()
stay_online: while (true) {
md.maintainOperation(io)
waitForBadThingInTaskCyclic(io) catch |err| switch (err) {
error.NonRecoverable => {
stop_event.set();
cyclic_task.join()
break;
},
error.Recoverable => {
doRecoveryAction();
continue :stay_online;
},
}
}
return error.NonRecoverable;
}
Questions:
- In
taskCyclic: how should I be modelling “non-recoverable” versus “recoverable” errors? Proposal: “Non-recoverable” errors are areturn error.NonRecoverablewhile I should somehow provide somenotificationtotaskAcyclicfor the “recoverable” errors? - Should I structure the code differently between these two tasks?
- How should I model notification from
taskAcyclictotaskCyclic?