Parallel
Parallel execution is where Aqua fully shines.
Contract
Parallel arms have no access to each other's data. Sync points must be explicit (see Join behavior).
If any arm is executed successfully, the flow execution continues.
All the data defined in parallel arms is available in the subsequent code.
Implementation limitation
Parallel execution has some implementation limitations:
Parallel means independent execution on different peers
No parallelism when executing a script on a single peer
No concurrency in services: every service instance does only one job simultaneously.
Keep services small in terms of computation and memory (WebAssembly limitation)
These limitations might be overcome in future Aqua updates. But for now, plan your application design having this in mind.
Parallel operations
par
par
syntax is derived from π-calculus notation of parallelism: A | B
par
works in an infix manner between the previously stated function and the next one.
co
co
, short for coroutine
, prefixes an operation to send it to the background. From π-calculus perspective, it's the same as A | null
, where null
-process is the one that does nothing and completes immediately.
Join behavior
Join means that data was created by different parallel execution flows and then used on a single peer to perform computations. It works the same way for any parallel blocks, be it par
, co
or something else (for par
).
In Aqua, you can refer to previously defined variables. In case of sequential computations, they are available, if execution not failed:
Let's make this script parallel: execute foo
and bar
on different peers in parallel, then use both to compute baz
.
What will happen when execution comes to baz
?
Actually, the script will be executed twice: the first time it will be sent from peer1
, and the second time – from peer2
. Or another way round: peer2
then peer1
, we don't know who is faster.
When execution will get to baz
for the first time, Aqua VM will realize that it lacks some data that is expected to be computed above in the parallel branch. And halt.
After the second branch executes, VM will be woken up again, reach the same piece of code and realize that now it has enough data to proceed.
This way you can express race (see Collection types and Conditional return for other uses of this pattern):
Explicit join expression
Consider the case when you want to wait for a certain amount of results computed in parallel, and then return.
How to return no less than n+1
responsible peers?
Keep in mind that indices start from 0
.
If the expected length of a stream equals n
, and you wait for element stream[n]
, your code will hang forever, as it exceeds the length!
One way is to use a useless stream:
Actually useless
stream is useless, we create it just to push the nth element into it. However, it forces waiting for responded[n
] to be available. When responded
is returned, it will be at least of length n+1
or longer.
To eliminate the need for such workarounds, Aqua has the join
expression that does nothing except consuming its arguments, hence waiting for them:
You can use any number of arguments to join
, separating them with a comma. join
is executed on a particular node – so join
respects the on
scopes it's in.
Timeout and race patterns
To limit the execution time of some part of an Aqua script, you can use a pattern that's often called "race". Execute a function in parallel with Peer.timeout
, and take results from the first one to complete.
This way, you're racing your function against timeout
. If timeout
is the first one to complete, consider your function "timed out".
Peer.timeout
is defined in aqua-lib
.
For this pattern to work, it is important to keep an eye on where exactly the timeout is scheduled and executed. One caveat is that you cannot timeout the unreachable peer by calling a timeout on that peer.
Here's an example of how to put a timeout on peer traversal:
And here's how to approach error handling when using Peer.timeout
Last updated
Was this helpful?