Replies: 16 comments
-
Timers are currently missing, and should be added to the framework. With timers, I would imagine one of the following scenarios: Future:
val f2 = Future(blockingIOOperation())
await(race(f2, timeout(1000)) We assume that Or, we could package that into a Future(blockingIOOperation()).timeout(1000) So, in short, timeouts need to be handled specially, they are not futures. About the structured concurrency policy in general: It's debatable whether an enclosing future should always wait until all nested futures have completed, or whether it's enough to just make cancellation requests for all nested futures that are still executing at that point and return immediately. Essentially, we are trading liveness for safety properties here and no policy is best in all cases. So I think we need one policy to be the default with the other policy being an opt-in. Which is which I am not sure yet. Following structured concurrency to the letter means that rogue computations can deadlock the system. Returning with just cancellation requests means that resources might still be blocked when a future returns. Probably, waiting for completion is best as the default. What's your opinion? |
Beta Was this translation helpful? Give feedback.
-
For timer, what kind of timer is going to be added?A scheduledExecutorService liked one for every core,or a HashedWheelTimer based one? And in jdk 20, there is a StructuredTaskScope, and support cancellation propagation which will help for not leaking. Cpu time is kind of resource too. StructuredTaskScope.ShutdownOnSuccess/ShutdownOnFailure。 |
Beta Was this translation helpful? Give feedback.
-
@He-Pin I think we are open to suggestions. I know nothing about timers, just that they exist. |
Beta Was this translation helpful? Give feedback.
-
I'm not sure I buy this. Timeouts are one of the primary use cases for cancelation in practice (they're absolutely the most frequent use of cancelation in production applications since every socket has at least one associated timer, and often more than one), and they have a tendency to show up in a lot of composed variants. Pure races in the sense that you may be envisioning with Fwiw, I quite like @adamw's example as a way of testing the limits of the framework as it stands, though it's worth noting that Cancelation in these types of systems (as end-users often conceptualize it) is really two things: sequence preemption and async interruption. The former is implemented by Cats Effect in the form of cancel checks on
You're quite correct that there's no right answer here. You're trying to decide between resource safety and deadlock safety, and it's not possible in general to have both. Having used various systems which bias in each direction for about a decade, I can tell you that I'm 100% in the camp of biasing in favor of resource safety. Deadlocks are usually pretty easy to track down with the tools provided by modern effect systems and tend to happen under normal operating conditions (not always, but usually). Conversely, the consequences of resource unsafety are leaks, crashes, and failure to self-heal under pressure. These issues manifest in production under the worst possible situations: when the system is under significant pressure due to some other externality. In other words, biasing away from resource safety causes the system to exhibit the worst-case behavior at the worst possible moment, whereas biasing away from deadlock safety causes the system to exhibit the worst-case behavior at (generally) more opportune times. With that said, strictly following structured concurrency is generally too restrictive in practice, and it makes it impossible to experiment and innovate around resource scoping and lifecycles (Fs2 is an excellent example of this: the entire framework is structured, but its implementation is in terms of unstructured primitives; Cats Effect's |
Beta Was this translation helpful? Give feedback.
-
@alexandru @viktorklang @jdegoes ping~ would be great to have your thoughts too |
Beta Was this translation helpful? Give feedback.
-
@djspiewak Thanks for your comments. This repo is still a strawman which means, made to be knocked down 😉. So it's really valuable to get constructive criticism at this early state. I did not fully understand the point about async interruption:
Did you have in mind that when cancelling a future, we also should do a I think I also agree with you that structured concurrency should be the default. In fact, if we implement that strategy, the opt-out is already available, since we can |
Beta Was this translation helpful? Give feedback.
-
See db25ad4 in #13 for changes that implement the structured concurrency rules for cancellation. (As always so far, totally ignoring optimizations). |
Beta Was this translation helpful? Give feedback.
-
There are a couple angles to this. First, for a Second, for any Third, for futures which are constructed via This third mechanism btw tends to be very subtle and complex, because you have to juggle three possible legs to a race condition:
All three of these things can interleave in any order on separate threads (and the second one tends to happen on third-party threads, so you really have no control over it). Oh as an aside, the listener notification in |
Beta Was this translation helpful? Give feedback.
-
Thanks for this crash course in low-level scheduling. Very much appreciated! So if I understood correctly.
We currently have interruptible and delay (via For promises, I was thinking of just setting a flag in the future value of a promise (or in the promise itself) that cancellation was requested and let the thread(s) that fulfil the promise poll that. I don't see what else one could do; certainly raising an exception in
I am not sure. Note that it's only the notification logic itself which is executed on the thread of the completing future. The continuation of the notified future is in any case scheduled on a different thread. The notification logic should usually run in a very short time span. |
Beta Was this translation helpful? Give feedback.
-
Happy to nerd out on it any time! There's a lot more here to explore and it's a lot of fun to wade through.
Yep! And to clarify, these tasks would not be cancelable. Or rather, cancelation would (asynchronously) wait until the task finished (to preserve structural concurrency).
Almost.
Nope.
I think so too.
There's a bunch of things to unpack in here. Starting from the last bit… You actually want most of the system to consist of The parallelism which is enabled by this sort of concurrency is I/O parallelism, where the motivating use-case is scatter/gather workflows (e.g. handling many thousands of connections, all coming from a single server socket, each of which in turn results in many more thousands of upstream connections, all of which should be parallelized). Each of those I/O events will either be blocking (in which case, it eats a thread and can't be done in parallel without starving the system of resources), or non-blocking (in which case, it is defined in terms of Regarding the async cancelation mechanism, it's definitely possible to do better, but probably not with the val executor: ScheduledExecutorService = ??? // make one of these per process
def sleep(d: FiniteDuration): IO[Unit] =
IO.async[Unit] { cb =>
IO {
val fut = executor.schedule(() => cb(Right(())), d.length, d.unit)
Some(IO(fut.cancel()))
}
} The fact that we can call
Is that the case? I didn't see that in the code but I also didn't read everything front to back. It looks to me like any suspended |
Beta Was this translation helpful? Give feedback.
-
Timers are one example, but you could also have an example without timers: Future:
val f1 = Future(blockingIOOperation1())
val f2 = Future(blockingIOOperation2())
f1.alt(f2).value e.g. racing cache retrieval with a DB query. When the faster one succeeds, you want the other one interrupted. And I'd say that it should be interrupted in a way that actually stops and releases the network socket. As a side question: when cancelling a future, should the
The way I understood structured concurrency, is that we should always wait for the nested futures/threads to complete. But of course I'm authoritative here :). Though without that property, we don't have backpressure. And that's a property that is often standard (e.g. the whole reactive "movement"). I definitely agree with Daniel, that deadlocks are easier to reproduce/debug than running out of resources.
I'm not sure I understand why that's the case. But maybe I misunderstand what's the target platform. From what I've imagined, at least as far as the JVM is concerned, a direct-style concurrency API would target a Loom-based runtime. There, it's "normal" to block (virtual) threads, and it's "normal" to do cancellation via interruption. In fact, that's the only possible way. I don't think the goal of a project like This of course skips over the issues with the Java interruption model, fairness of virtual thread scheduling and I think I also read that Daniel mentioned a hidden unbounded thread pool when using |
Beta Was this translation helpful? Give feedback.
-
In cats-effect (and ZIO) when you cancel an
is not enough. |
Beta Was this translation helpful? Give feedback.
-
So I find your take on promised vs runnable futures interesting. I'm learning that promised futures cannot be neglected. But there are also scenarios where runnable futures are dominant. One is compute bound things. Runnable futures are the natural building block for dataflow parallelism. I would imagine that higher level algorithms such as parallel collections could be built on them. Also, anything we do with an externally fulfilled future takes place in a runnable future. So promised futures should only be at the extreme points of a system where it interacts with the external world. But yes, cancelling them is also important. I'll look deeper into the solution you propose.
Good points. We need to look into interrupts in more detail.
Right now (i.e. with #13), Maybe there should also be an |
Beta Was this translation helpful? Give feedback.
-
Of note, in Kotlin Coroutines, |
Beta Was this translation helpful? Give feedback.
-
Since you pulled me into this thread, I'll share my thoughts: I think that designing a primitive for concurrent, async + blocking computation, which supports structured concurrency, runs efficiently, is properly backpressured, supports timeouts and races with sane semantics, and so forth, is a very complex and error-prone undertaking, ultimately benefiting from detailed knowledge of and experience in low-level systems programming, as well as concurrent programming, async programming, and I/O scheduling (to name a few!). Looking at the design of the Since in the Scala community we are fortunate enough to have collective experience building this machinery, and iterating on it over a period of years, in response to user feedback, it would be a shame to not take advantage of all this domain expertise. I highly recommend that development of These wheels have been invented many times before, and this reinvention, if it must happen within EPFL (why?), should learn from the other inventions to enjoy the best possible outcome. |
Beta Was this translation helpful? Give feedback.
-
This is a very early prototype, meant as a feasibility study and to explore the design space of possible APIs. We very much invite feedback and suggestions for all areas, in particular concerning scheduling and cancellation. The Scala community has a lot of expertise to offer, on which we want to draw. I believe there is also the important space of low-level concurrency mechanisms where it would make sense to share code between different effect systems. I intentionally talked early about this project at Scalar in order to draw the community's attention to it, and profit from their input. |
Beta Was this translation helpful? Give feedback.
-
I've started exploring the codebase & the design, and I have some doubts on the usefulness of the cancellation model.
Let's suppose I'd like to timeout a blocking I/O operation (like reading from a socket). Using the current code, that would translate to sth like:
However, that won't work, as the cancellation only impacts reading the value of a
Source
, not the actual computation: even though thealt
will complete after 1s, the blocking operation will continue, still waiting on the socket or such. (And for a good reason: it cannot impact the computation, asFuture
isn't bound to any thread/fiber, virtual or not.)Additionally, if we are to follow the structured concurrency approach, we cannot leak any running threads/fibers outside the scope of
Future:
: so once the overall future completes, any computations that started must somehow complete. The only way to guarantee this here is to wait until both branches finish (defeating the whole purpose ofalt
: as there's no way to interrupt the sleep, we'll always wait at least 1s).That's described e.g. on Wikipedia:
but also in the introductory articles.
On the other hand, if we let futures leak, there's no structured concurrency, and we are resource-unsafe.
Beta Was this translation helpful? Give feedback.
All reactions