Distributed Computation
Source: tutorial/10_distributed_computation (directory) See in Playground
This closes the tutorial track. Every previous example ran in a single process; distribute is the one primitive that spreads a computation across several engines, the mechanism the showcase examples lean on for scaling further. A local client sends a stream of numbers to a second, separately running Mélodium engine; that engine doubles each number and sends it back.
Unlike every other tutorial step, this example is a single standalone .mel file, distributed_computation.mel, run directly with melodium run. There is no Compo.toml.
Requires two terminals, each running its own Mélodium engine, sharing the same distribution group (a MELODIUM_GROUP_ID environment variable) and a matching pair of authentication keys.
Running
Both terminals must export the same MELODIUM_GROUP_ID. The two keys are swapped between them: one side’s send key is the other side’s recv key. The UUIDs below are example values for local pairing only, any matching pair works as long as both terminals agree.
Terminal 1, the listening engine:
cd tutorial/10_distributed_computation
export MELODIUM_GROUP_ID=10101010-1010-1010-1010-101010101010
melodium dist --localhost --port 6789 \
--recv-key 11111111-1111-1111-1111-111111111111 \
--send-key 22222222-2222-2222-2222-222222222222Terminal 2, this script, with the keys swapped:
cd tutorial/10_distributed_computation
export MELODIUM_GROUP_ID=10101010-1010-1010-1010-101010101010
melodium run distributed_computation.mel --port 6789 \
--send_key 11111111-1111-1111-1111-111111111111 \
--recv_key 22222222-2222-2222-2222-222222222222info: distrib: connected to remote engine
info: doubled: 6
info: doubled: 6
info: doubled: 6
info: doubled: 6
info: doubled: 6--localhost uses an embedded certificate meant for local testing. The listener must be up first: main only sends data once it confirms the connection is ready, but that connection attempt can only succeed once the listener itself is accepting it.
Optional: add --api-report and an API token (MELODIUM_API_TOKEN) to see this run’s full trace on Cadence.CI .
How it works
A single DistributionEngine model identifies the remote treatment to run and its version, not a network resource:
model Doubler() : DistributionEngine {
treatment = "distributed_computation::double"
version = "0.1.0"
}This is different from models like SqlPool or HttpServer: the network target itself is supplied separately, through an Access value passed to start. double, the treatment executed on the remote engine, is defined in the same file:
treatment double()
input n: Stream<i64>
output n: Stream<i64>
{
doubled: add<i64>()
Self.n -> doubled.a
Self.n -> doubled.b
doubled.sum -> Self.n
}The overall data flow crosses the network in both directions:
Connecting to the remote engine
work/access::|new_access builds an Access value (IP, port, and the two authentication keys) entirely from parameters, no cloud service involved, just a second Mélodium process reachable over the network. start then opens the connection:
treatment main(
const port: u16 = 6789,
const send_key: string,
const recv_key: string,
const amount: u128 = 5,
const value: i64 = 3
)
model distributor: Doubler()
{
startup()
accessBlock: emit<Access>(value=|new_access([|from_ipv4(|localhost_ipv4())], port, send_key, recv_key))
startup.trigger -> accessBlock.trigger
distribStart: start[distributor=distributor](params=|map([]))
accessBlock.emit -> distribStart.access
distribErr: logError(label="distrib")
distribFailed: logErrorMessage(label="distrib", message="could not connect to the remote engine")
distribStart.error -> distribErr.message
distribStart.failed -> distribFailed.trigger
logReady: logInfoMessage(label="distrib", message="connected to remote engine")
distribStart.ready -> logReady.trigger
run[distributor=distributor](amount=amount, value=value)
distribStart.ready -> run.trigger
}|new_access’s parameter order is (ip, port, remote_key, self_key): remote_key is the identity presented outward, the local send_key, and self_key is what is checked against what comes back, the local recv_key. Only once distribStart.ready fires does run actually build and send any data, so nothing races the connection setup. With the defaults (--amount 5 --value 3), the client sends five copies of 3 and logs doubled: 6 five times.
Sending and receiving across the network
dispatchDouble is the general shape for “run this like a local treatment, but remotely”: distribute allocates a distribution_id for one exchange, then sendStream and recvStream, tagged with matching names, carry the actual data in both directions:
treatment run[distributor: DistributionEngine](const amount: u128, const value: i64)
input trigger: Block<void>
{
length: emit<u128>(value=amount)
numbers: generate<i64>(data=value)
Self.trigger -> length.trigger,emit -> numbers.length,stream -> dispatch.n
dispatch: dispatchDouble[distributor=distributor]()
logResult: logInfos(label="doubled")
asText: toString<i64>()
dispatch.n -> asText.value,into -> logResult.messages
}
treatment dispatchDouble[distributor: DistributionEngine]()
input n: Stream<i64>
output n: Stream<i64>
{
startTrigger: trigger<i64>()
dist: distribute[distributor=distributor]()
Self.n -> startTrigger.stream,start -> dist.trigger
send: sendStream<i64>[distributor=distributor](name="n")
recv: recvStream<i64>[distributor=distributor](name="n")
dist.distribution_id -> send.distribution_id
dist.distribution_id -> recv.distribution_id
Self.n -> send.data
recv.data -> Self.n
}This is the three-step handshake for one remote call: allocate an ID, send the input, receive the output, all tagged by port name ("n" here) so multiple streams can cross the same connection unambiguously. From dispatchDouble’s own inputs and outputs, calling the remote double treatment looks exactly like connecting to a local one.
Dependencies
There is no Compo.toml for this example: as a standalone script, distributed_computation.mel declares its dependencies directly in its own header:
#!/usr/bin/env melodium
#! name = distributed_computation
#! version = 0.1.0
#! require = std:0.10.* net:0.10.* work:0.10.* distrib:0.10.*std: core flows, logging, data structuresnet: IP address helperswork: network access description for the remote enginedistrib: running treatments on a remote engine and wiring their streams