Introduce Task monad - #1802
Conversation
b9621d1 to
e24af06
Compare
ce8e407 to
b7ba3a9
Compare
|
I'm getting closer to a first well-connected base with things like resource safety, interruptability and time somewhat properly present. I think we should have a discussion on what to do with the interruptability of graphs, though:
Is this something to discuss and look at now? I suppose it could be done later, by putting interruptability into a type class when creating a |
|
@jypma if you thinking copying some of the pekko-connectors-kafka code into your task package helps, go ahead. Let's keep any additions specific to the task package for now and we can later discuss if it is worth taking some of reusable bits out and making them available as public APIs or to be used by other Pekko classes. |
0172b96 to
f6dc3c5
Compare
|
I've decided to just introduce a |
9010cc4 to
a7b04d0
Compare
This commit introduces Task, a data structure that represents a recipe, or program, for producing a value of type T (or failing with an exception). It is similar in semantics to RunnableGraph[T], but intended as first-class building block. It has the following properties: - A task can have resources associated to it, which are guaranteed to be released if the task is cancelled or fails - Tasks can be forked so multiple ones can run concurrently - Such forked tasks can be cancelled A Task can be created from a RunnableGraph which has a KillSwitch, by connecting a Source and a Sink through a KillSwitch, or by direct lambda functions.
|
@jypma Seems there are some conflicts. I would like to learn more once this is ready |
He-Pin
left a comment
There was a problem hiding this comment.
This is an ambitious and substantial addition to Pekko -- introducing a Task monad with resource safety, cancellation, and concurrency primitives. A few observations:
-
Architectural significance: This introduces a significant new abstraction layer. The TODO list in the PR body acknowledges important open items: Scala DSL completeness, moving to a dedicated
taskmodule, andTask.runAsMain. These should be resolved before merge. -
Module placement: Currently in the
streammodule, but the TODO notes that streams should eventually depend ontask, not the other way around. This architectural decision should be settled before merging. -
Testing gap: The PR acknowledges needing more test cases for edge cases and concurrency race conditions. This is critical for a concurrency primitive.
-
Staleness: Open since March 2025 with activity stopping in May 2025. The concept is interesting but the PR is clearly incomplete (several unchecked TODO items). @jypma -- is this something you plan to complete? Otherwise consider closing and revisiting when there's bandwidth to finish the full implementation.
|
I don't have any plans to complete this, unfortunately. I've concluded that Pekko doesn't currently have the resource safeties needed for a proper Task monad, particularly with respect to graph cancellation and shutdown. Hence, we'd have to look at the internals of Pekko first, e.g. tightening the semantics of callbacks within a graph when a shutdown happens, to get precise control there. For those reading along: what I've been working on a bit on instead, is to take a framework that does give these guarantees, but only in Scala (ZIO), and create a "nice" fluent Java API on top of that. Pekko streams could slot in there as well. My initial approach for that is here: JIO, although development there has also stalled a bit (but only due to shifting interests). Send me a message if anyone is curious about that direction. For this PR on Pekko, feel free to close it. Thanks for all the thoughtful discussion and comments! |
Relates to #1801
This commit introduces
Task, a data structure that represents a recipe, or program, for producing a value of type T (or failing with an exception). It is similar in semantics toRunnableGraph[T], but intended as first-class building block.It has the following properties:
A Task can be created from a
RunnableGraphwhich has aKillSwitch, by connecting aSourceand aSinkthrough aKillSwitch, or by direct lambda functions.Open discussion points and TODOs (in order of highest architectural impact):
KillSwitchdoesn't communicate back a trigger when the graph is actually done cancelling. However, since we also have theFuture[T]that the graph's sink materializes to, that's enough to synchronize on.forkDaemon,forkResource). No child fibers (yet).zip,andThen,before,raceAll)asResource, ...).delay()and general scheduling by hooking in the scheduling features ofMaterializerSink.forEachTaskandFlow.mapTaskResource.ofAutoCloseableTask.never()Task.runAsMain(or perhapsApplicationbase classes / traits), which runs a task asmain(), capturing Ctrl+C to interrupt the task (and, hence, clean up resources).taskmodule. Don't depend onstream. Streams should depend ontask, so we can use tasks directly in stream combinators.Needing an eye from reviewers:
To be handled in later pull requests:
TestClock(implementation ofClockfor unit tests, where tests can advance the clock, so time-driven code can run deterministically and faster than realtime)Task-friendly concurrency primitives like a queue, mutex, and a mutable ref with stream subscription support.Effect[E,A]which introduces modelled errors (in addition to exceptions). This can be easily built on top of aTaskDef[Either[E,A]].Fixes #1801 .