#maelstrom #distributed #gossip #service #read-write #glomers

nebkor-maelstrom

An easy-to-use and synchronous client for creating Maelstrom distributed clients

3 releases (1 stable)

1.0.0 Jun 10, 2024
0.0.2 Jun 5, 2024
0.0.1 Jun 5, 2024

#229 in Concurrency

26 downloads per month

Custom license

20KB
356 lines

A synchronous and simple Maelstrom crate

nebkor-maelstreom is a lean and simple synchronous library for writing Maelstrom-compatible distributed actors. It has three dependencies:

  • serde
  • serde_json
  • serde_repr

For a simple example, see the echo example:

use nebkor_maelstrom::{Body, Message, Node, Runner};

struct Echo;

impl Node for Echo {
    fn handle(&mut self, runner: &Runner, msg: Message) {
        let typ = &msg.body.typ;
        if typ.as_str() == "echo" {
            let body = Body::from_type("echo_ok").with_payload(msg.body.payload.clone());
            runner.reply(&msg, body);
        }
    }
}

fn main() {
    let node = Echo;

    let runner = Runner::new(node);

    runner.run(None);
}

For a slightly more complicated example, check out the broadcast example, which passes the single-node challenge, but utterly fails even the friendliest (eg, no partitions or lag) multi-node challenge, so hopefully is not giving too much away.

Features

  • no async
  • minimal boilerplate
  • working RPC calls (allowing the main thread to call out to other nodes and receive a reply while already handling a message)
  • proxies for the Maelstrom KV services that use the RPC mechanism to provide read, write, and cas operations, and return Result<Option<serde_json::Value>, ErrorCode>s to the caller

How to use

Create a struct and implement nebkor_maelstrom::Node for it, which involves a single method, handle(&mut self, &Runner, Message). This method is passed a Runner which contains methods like send, reply, and rpc.

In your main function, instantiate that struct and pass that into Runner::new() to get a Runner. The run() method takes an optional callback that will be run when the init Message is received; see the broadcast example, where it spawns a thread from the callback to send periodic messages to the node.

Design considerations

I wanted the client code to be as simple as possible, with the least amount of boilerplate. Using &mut self as the receiver for the handle() method lets you easily mutate state in your node if you need to, without the ceremony of Rc<Mutex<>> and the like. Eschewing async results in an order of magnitude fewer dependencies, and the entire workspace (crate and clients) can be compiled in a couple seconds.

It also assumes that some things are infallible. For example, there's liberal unwrap()ing when calling send() or recv() on MPSC channels, because those kinds of errors are not part of the Maelstrom protocol; this crate is not a general-purpose network client crate for the real world. Likewise stdin and stdout are always assumed to be available and reliable; those two channels are the physical layer for connecting a node to the Maelstrom router, and failures there are out of scope for Gossip Glomers.

A final consideration is understandability of the crate itself; you should not have a hard time diving into its source from your IDE or browser.

Acknowledgments

I straight-up stole the design of the IO/network system from Maelbreaker, which allowed me to get a working RPC call. Thanks! And thanks to Nicole for nudging me to publish this.

Dependencies

~0.7–1.6MB
~35K SLoC