I like context.WithCancel from Go, but it can essentially be implemented directly in PHP land since
all the necessary tools are available.
Note, this is precisely the problem, implement cancellation propagation to child fibers in userland
PHP requires writing a bunch of boilerplate try-catch blocks to propagate CancellationExceptions to
child FutureHandle::cancel()s (spawning multiple fibers to execute subtasks concurrently during an
async method call is pretty common, and the current implicit cancellation mode requires writing a
bunch of try-catch blocks to propagate cancellation, instead of just passing a cancellation object,
or a flag to inherit the cancellation of the current fiber when spawning a new one).
Catching CancellationException is only necessary if there is some defer code.
If there isn't, then there's no need to catch it. Try-catch blocks are not mandatory.
We can create a Cancellation object, pass it via use or as a parameter to all child fibers, and
check it in await(). This is the most explicit approach. In this case, try-catch would only be
needed if we want to clean up some resources. Otherwise, we can omit it.
According to the RFC, if a fiber does not catch CancellationException, it will be handled by the
Scheduler. Therefore, catching this exception is not strictly necessary.
If this solution also seems too verbose, there is another one that can be implemented without
modifying this RFC. For example, implementing a cancellation operation for a Context. All coroutines
associated with this context would be canceled. From an implementation perspective, this is
essentially iterating over all coroutines and checking which context they belong to.
Note the explicit use case I listed is that of an unlock() in a finally block that *requires
spawning a new fiber* in order to execute the actual unlock() RPC call: this is explicitly in
contrast with the RFC, which specifies that
So, if I understand correctly, the code in question looks like this:
try { lock(); ... } finally { unlock(); }
function unlock() {
async\run();
}
If I got it right, then the following happens:
The code inside try {} allocates resources.
The code inside finally {} also allocates resources.
So, what do we get? We're trying to terminate the execution of a fiber, and instead, it creates
a new one. It seems like there's a logical error here.
Instead of creating a new fiber, it would be better to use microtasks.
I would really prefer it to be always enabled, no fallback at all, because as I said, it will make
absolutely no difference to legacy, non-async projects that do not use fibers, but it will avoid a
split ecosystem scenario.
I'm not arguing at all that avoiding the call to this function is a good solution. I’m on
your side. The only question is how to achieve this technically.
Could you describe an example of "ecosystem split" in the context of this function? What
exactly is the danger?
I see no reason why it should break the contract, if implemented by isolating the global state of
each fiber, it can be treated as a mere implementation detail of the (eventually new) SAPI.
So, I can take NGINX and FCGI, and without changing the FCGI interface itself, but modifying its
internal implementation, get a working application. Yes, but... that means all global variables,
including static ones, need to be tied to the context. It's not that it can't be done, but
what about memory consumption.
I'm afraid that if the code wasn't designed for a LongRunning APP, it's unlikely to
handle this task correctly.
--
Ed.