Previous blog post - Distributed System challenges in Rust
Echo
This is the first challenge that we have to do.
The Maelstrom server will send an echo
message of the following format:
{
"src": "c1",
"dest": "n1",
"body": {
"type": "echo",
"msg_id": 1,
"echo": "Please echo 35"
}
}
and we need to reply with the following message:
{
"src": "n1",
"dest": "c1",
"body": {
"type": "echo_ok",
"msg_id": 1,
"in_reply_to": 1,
"echo": "Please echo 35"
}
}
In the given format, there are a couple of things that we need to keep in mind:
- Nodes & clients are sequentially numbered (e.g. n1, n2, etc).
- Nodes are prefixed with "n" and external clients are prefixed with "c".
- Message IDs are unique per source node
The third point here is something that is handled by the library (along with a helpful into_reply
function that converts a message into the format that a "reply" should be in.)
Solution
To start off, we define the structure of the incoming and outgoing messages that we'll be handling.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
#[serde(rename_all = "snake_case")]
enum EchoPayload {
Echo { echo: String },
EchoOk { echo: String },
}
Serde
automatically handles discriminated/polymorphic deserialization. By defining the tag
field, we tell serde
to pick the EchoPayload
enum variant which matches the name provided in the type
field in the message that it is operating on. For type = "echo"
, it would pick the EchoPayload::Echo
variant, and for type = "echo_ok"
, it would pick the EchoPayload::EchoOk
variant.
The next thing to do, is to define an EchoNode
which implements the Node
trait provided by the library. This node would be given all incoming messages, and be expected to respond appropriately. It will maintain any kind of state that is relevant to it.
struct EchoNode {
echo_message: String,
}
The first problem requires a very simple node. It just needs to store the echo_message
it received, and then spit out the same message in a reply.
Our step
function looks something like this:
fn step(
&mut self,
input: Event<EchoPayload, ()>,
output: &mut std::io::StdoutLock,
) -> anyhow::Result<()> {
match input {
Event::Message(message) => {
match message.body.payload {
EchoPayload::Echo { echo } => {
self.echo_message = echo;
let message_id = message.body.id;
let message = Message {
src: message.src,
dest: message.dest,
body: Body {
id: message.body.id,
in_reply_to: message.body.in_reply_to,
payload: EchoPayload::EchoOk {
echo: self.echo_message.clone(),
},
},
};
message.into_reply(Some(&mut message_id.unwrap())).send(output)?;
}
EchoPayload::EchoOk { echo } => {}
}
},
Event::Injected(injected) => {},
Event::EOF => {}
}
Ok(())
}
We begin by extracting the payload out of the incoming message, creating a response message which includes the same string that we got in the echo payload, and then writing that to stdout.
I ignore most of the error states for this problem -
- We don't need to perform any actions if some other node sends us an
echo_ok
. - I don't yet know the relevance of the
injected
variant - We don't need to do anything special on
EOF
With this done, we run our maelstrom test to verify that everything works.
This one was a pretty simple problem to solve, as it was intended to be the "introduction" to the Maelstrom runtime and the architecture of the library. You can find the code for my solution on my GitHub here.