Challenge #1: Echo

Our first challenge is more of a “getting started” guide" to get the hang of working with Maelstrom in Go. In Maelstrom, we create a node which is a binary that receives JSON messages from STDIN and sends JSON messages to STDOUT. You can find a full protocol specification on the Maelstrom project.

We’ve created a Maelstrom Go library which provides maelstrom.Node that handles all this boilerplate for you. It lets you register handler functions for each message type—similar to how http.Handler works in the standard library.

Specification

In this challenge, your node will receive an "echo" message from Maelstrom that looks like this:

{
  "src": "c1",
  "dest": "n1",
  "body": {
    "type": "echo",
    "msg_id": 1,
    "echo": "Please echo 35"
  }
}

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 but that is handled automatically by the Go library.

Your job is to send a message with the same body back to the client but with a message type of "echo_ok". It should also associate itself with the original message by setting the "in_reply_to" field to the original message ID. This reply field is handled automatically if you use the Node.Reply() method.

It should look something like:

{
  "src": "n1",
  "dest": "c1",
  "body": {
    "type": "echo_ok",
    "msg_id": 1,
    "in_reply_to": 1,
    "echo": "Please echo 35"
  }
}

Implementing a node

For this first challenge, we’ll walk you through how to implement the echo program. First, create a directory for your binary called maelstrom-echo and initialize a Go module for it:

$ mkdir maelstrom-echo
$ cd maelstrom-echo
$ go mod init maelstrom-echo
$ go mod tidy

Then create your main.go file in this directory. This should start with the main package declaration and a few imports:

package main

import (
    "encoding/json"
    "log"

    maelstrom "github.com/jepsen-io/maelstrom/demo/go"
)

Inside our main() function, we’ll start by instantiating our node type:

n := maelstrom.NewNode()

From here, we can register a handler callback function for our “echo” message. This function accepts a maelstrom.Message which contains the source and destination nodes for the message as well as the body content.

n.Handle("echo", func(msg maelstrom.Message) error {
    // Unmarshal the message body as an loosely-typed map.
    var body map[string]any
    if err := json.Unmarshal(msg.Body, &body); err != nil {
        return err
    }

    // Update the message type to return back.
    body["type"] = "echo_ok"

    // Echo the original message back with the updated message type.
    return n.Reply(msg, body)
})

In this handler, we’re unmarshaling to a generic map since we simply want to echo back the same message we received. The Reply() method will automatically set the source and destination fields in the return message and it will associate the message as a reply to the original one received.

Finally, we’ll delegate execution to the Node by calling its Run() method. This method continuously reads messages from STDIN and fires off a goroutine for each one to the associated handler. If no handler exists for a message type, Run() will return an error.

if err := n.Run(); err != nil {
    log.Fatal(err)
}

You can find a full implementation of this maelstrom-echo program in the Maelstrom codebase.

To compile our program, fetch the Maelstrom library and install:

go get github.com/jepsen-io/maelstrom/demo/go
go install .

This will build the maelstrom-echo binary and place it in your $GOBIN path which is typically ~/go/bin.

Installing Maelstrom

Maelstrom is built in Clojure so you’ll need to install OpenJDK. It also provides some plotting and graphing utilities which rely on Graphviz & gnuplot. If you’re using Homebrew, you can install these with this command:

brew install openjdk graphviz gnuplot

You can find more details on the Prerequisites section on the Maelstrom docs.

Next, you’ll need to download Maelstrom itself. These challenges have been tested against the Maelstrom 0.2.3. Download the tarball & unpack it. You can run the maelstrom binary from inside this directory.

Running our node in Maelstrom

We can now start up Maelstrom and pass it the full path to our binary:

./maelstrom test -w echo --bin ~/go/bin/maelstrom-echo --node-count 1 --time-limit 10

This command instructs maelstrom to run the "echo" workload against our binary. It runs a single node and it will send "echo" commands for 10 seconds.

Maelstrom will only inject network failures and it will not intentionally crash your node process so you don’t need to worry about persistence. You can use in-memory data structures for these challenges.

If everything ran correctly, you should see a bunch of log messages and stats and then finally a pleasent message from Maelstrom:

Everything looks good! ヽ(‘ー`)ノ

Success! If everything is working, move on to the Unique ID Generation challenge to build and test a distributed unique ID generator on your own.

If you’re not seeing this success message, head over to our Fly.io Community forum for some help.

Debugging maelstrom

If your test fail, you can run the Maelstrom web server to view your results in more depth:

./maelstrom serve

You can then open a browser to http://localhost:8080 to see results. Consult the Maelstrom result documentation for further details.

  1. Read More About Echo
  2. Read More About Unique ID Generation
  3. Read More About Broadcast
  4. Read More About Grow-Only Counter
  5. Read More About Kafka-Style Log
  6. Read More About Totally-Available Transactions