Yes, exactly. The scope would, in effect, get HC because an exception ends the scope.
I think there is a compromise between in-band and out-of-band. In my examples with trio, one would opt-in to getting an early exception at points in the code where it is safe to stop work (accept loops, waiting on a recv).
In C, you had the suggestion of recv_from_socket_or_channel
where you may want to interrupt a suspend on a socket when you get an in-band message from a channel. You explicitly opted-in to getting notified early, and if you get a message from the channel you know that your socket is still in a good state.
Whether that is signaled with an exception or the return of an explicit state is more of a language flavor question than whether the runtime should offer this kind of functionality - where you can opt-in to the early return of a suspended function so you can perform graceful cleanup.
I think the runtime should offer those early return mechanisms rather than libraries plumbing the signal thoughout their code.
In your example from the blog:
coroutine void nested_worker(message_t msg) {
// process the message here
}
coroutine void worker(socket_t s, channel_t ch) {
bundle_t b = bundle();
while(1) {
message_t msg;
int rc = recv_from_socket_or_channel(s, ch, &msg);
if(rc == ECANCELED) goto hard_cancellation;
if(rc == FROM_CHANNEL) goto graceful_shutdown;
if(rc == FROM_SOCKET) {
bundle_go(b, nested_worker(msg));
}
rc = send(s, "Hello, world!");
if(rc == ECANCELED) goto hard_cancellation;
}
graceful_shutdown:
rc = bundle_cancel(b, 20); // cancel the nested workers with 20 second grace period
if(rc == ECANCELED) return;
return;
hard_cancellation:
rc = bundle_cancel(b, 0); // cancel the nested worker immediately
if(rc == ECANCELED) return;
return;
}
int main(void) {
socket_t s = create_connected_socket();
channel_t ch = channel();
bundle_t b = bundle();
bundle_go(b, worker(s, ch));
sleep(60);
send(ch, "STOP"); // ask for graceful shutdown
bundle_cancel(b, 10); // give it at most 10 seconds to finish
return 0;
}
What I think should happen in worker
instead is that you always do graceful shutdown if you receive ECANCELED
from recv_from_socket_or_channel
. If this is a hard cancel the nested_worker
will receive ECANCELED
at any checkpoint in that code anyway so bundle_cancel(b, 0)
is unnecessary.
The question is how do you opt recv_from_socket_or_channel
in to early cancellation and make sure send
is not opted in, because you don’t want to stop sending during early cancellation.
coroutine void nested_worker(message_t msg) {
// process the message here
}
coroutine void worker(socket_t s, channel_t ch) {
bundle_t b = bundle();
while(1) {
message_t msg;
int rc = recv_from_socket_early_cancel(s, &msg);
if(rc == ECANCELED) goto shutdown;
if(rc == FROM_SOCKET) {
bundle_go(b, nested_worker(msg));
}
// if you wanted to stop here too use send_early_cancel(s, "Hello, world!") instead
rc = send(s, "Hello, world!");
if(rc == ECANCELED) goto shutdown;
}
shutdown:
// cancel the nested workers with 20 second grace period
// if this is a hard cancel, workers will hard cancel themselves anyway
rc = bundle_cancel(b, 20);
if(rc == ECANCELED) return;
return;
}
int main(void) {
socket_t s = create_connected_socket();
channel_t ch = channel();
bundle_t b = bundle();
bundle_go(b, worker(s, ch));
sleep(60);
// this will make all early cancel checkpoints return ECANCELED
// after 10 seconds all checkpoints will return ECANCELED
bundle_cancel(b, 10); // give it at most 10 seconds to finish
return 0;
}
I’m not sure if you really want to make a separate _early_cancel
function for every coroutine. Maybe a way to signal the runtime or bundle that you’re going into an early cancel state? I’m not familiar with libdill
so you’ll have to forgive me.
coroutine void worker(socket_t s, channel_t ch) {
bundle_t b = bundle();
while(1) {
message_t msg;
ENTER_EARLY_CANCEL()
int rc = recv(s, &msg);
EXIT_EARLY_CANCEL()
if(rc == ECANCELED) goto shutdown;
if(rc == FROM_SOCKET) {
bundle_go(b, nested_worker(msg));
}
rc = send(s, "Hello, world!");
if(rc == ECANCELED) goto shutdown;
}
shutdown:
// cancel the nested workers with 20 second grace period
// if this is a hard cancel, workers will hard cancel themselves anyway
rc = bundle_cancel(b, 20);
if(rc == ECANCELED) return;
return;
}
Now the real challenge is can you be in a consistent state if a parent calls ENTER_EARLY_CANCEL
and some child or grandchild coroutine now gets ECANCELED
where they normally expect only hard cancel? If they aren’t graceful aware, they’re going to clean up whatever they can and return back to you. Maybe it isn’t safe to assume that things are consistent anymore and you should just exit (it was in the middle of protocol negotiations on the socket), but then again I’d argue you shouldn’t call ENTER_EARLY_CANCEL
unless you know the code you’re calling handles it properly.
I would probably suggest that you should always try to be in a consistent state even in the hard cancel case. Don’t immediately goto shutdown
, try to finish the handshake. In the HC case every checkpoint will immediately return ECANCLED
so the only thing you lose by continuting is potentially expensive CPU operations (e.g. calculating a shared secret). When you should not be cancelled early, you setup guards ENTER_NO_EARLY_CANCEL
/EXIT_NO_EARLY_CANCEL
around your critical sections.