Posted by icyfox 3 days ago
I fell in love with durable execution a few years ago, after working at a company with a heavy pubsub architecture. In comparison to job orchestrators that force a certain task design or special syntax, many of these durable libs lets you write business logic in regular code: `asyncio.gather` to parallelize tasks, `for` loops, conditionals, and let the runtime figure out how to run them efficiently across a cluster of nodes. But most of the current solutions require a heavy broker, runtime hacks to try to ensure deterministic workflow logic, and feel like you have to learn a whole new ecosystem just to get started.
A few unique design decisions in waymark:
— Write jobs in normal code (right now Python but JS coming soon). Instead of running control flow as-is, which requires booting up an interpreter to replay past state, we’ll just compile your code directly. We walk your AST and map your logic to our stackless VM bytecode. We support variables, conditionals, await, sleep, helper functions, loops... basically what you’d expect in workflow logic.
— Since we’re compiling a language subset, we can proactively flag errors in your workflows locally instead of when they run in production.
— It only requires Postgres as the task broker. We’ve spent a lot of time tuning our design to play nicely with its strengths and limitations; our current benchmarks show around 1500 workflows/second but we still have some more optimization work to do here.
— Actions plug-and-play with FastAPI’s dependency injection syntax. So if you already have an ecosystem based on typehinting/DI, it should fit like a glove.
It allows for parallelization of workflows across one machine or a whole cluster. The bulk of the work per node is done by our central Rust core - which distributes workflows to properly saturate all cores. The idea is to be as batteries included as possible with the broker, webapp, and runtime.
What to use it for? It works great as the runner loop controlling agents, but it also works well for basically any task that you need to kick off in the background. It’s largely replaced our way of writing api code. Most frontend actions at this point just push to a simple REST endpoint that kick off a background job.
I spent an embarrassing amount of time making our https://waymark.sh site interactive - it’s probably the best place to learn more. We’re starting with Python support and looking for community input on what language to ship next. Would love to hear your thoughts!
As for CIL - we should add it at the "What should we build next?" section on the website :)
With the way our current Python workers are implemented you'd have we'd have to place the sync code onto threads manually.
We could make it happen though, if there is a demand for sync code; we can allow actions to be non-async, and from that run them in sync mode - either with an sdk-level offload to threads off-of async reactor, or on a separate worker pools that run fork-per-action. From the perspective of our VM, we don't really have a difference there...
As for async - background jobs are better modeled as async because of their natural parallelism across nodes imo. Logic in actions themselves are free to be as synchronous as you want. Why the preference?
Also, it is okay to run something in the foreground. Not everything needs to run in the background. Sometimes it's better to wait and have assurance of completion.
There is no natural parallelism of async code in Python. The coroutines run in a single thread.
This is a platform for running workflows distributed on multiple machines, sometimes long-running and with lots of IO. Every single contender in the space advertises the same code, from temporal to hatchet to dagster. Because it makes sense to write it this way.
Also, the claim made that something is "better modeled as async" is wholly unqualified.
You'd use asyncio.to_thread[0].
I also don't quite get your complaint... asyncio provides a high-level concurrency primitive with coroutines, and that's a good enough primitive for workflow engines. Even with asyncio you could fork/spawn processes easily with aiomultiprocess[1], if you'd ever want or need to.
What's your use case for having more low-level concurrency primitives (threads/processes) available, having to deal with their synchronization, and running workflows sync?
[0]: https://docs.python.org/3/library/asyncio-task.html#asyncio....
No such thing is typically necessary. If modeled correctly, the need for synchronization is minimal.
With sync code, tasks run on different threads/interpreters/processes. Message passing via queues is used to communicate which tasks need to run. Many asyncio users seem to wholly unaware of how well it works. Go-lang doesn't even have or need async.
I actually like asyncio; I am just not a religious zealot about it as many here seem to be, also without awareness of the struggles they face with loop-hogging coroutines and multicore parallelism.
When we compile a workflow into bytecode we get an executable; we store it and assign it an executable id. In-flight workflows are technically VM instances, and they always pin to a concrete executable id. When the VM goes out of residency (i.e. evicted from a node that executes it and unloaded from memory) we will eventually gc the executable bytecode from memory if it's not used by any VM for a while - but it's kept in postgres so VM can be revived.
There are ways to make workflows upgradeable - but don't have this yet. Ideally, we'd want to build it once we have enough feedback on how people want to do this - cause it's really about choosing the trade-offs more than anything else.