Skip to content

Distribution

Simon Fowler edited this page Jun 25, 2017 · 2 revisions

Distribution Overview

Links has for some time incorporated process-based, actor-ish concurrency, and more recently, has incorporated session-typed channels. Both of these have worked on the client, and on the server.

However, while the abstractions themselves imply location transparency (for example, if we have a process ID, we should be able to send to it irrespective of where the process is located), there has until now existed a "barrier" between different concurrency runtimes. The only way of doing client-server communication has been RPC calls, which are very handy for some things, but less so for others.

Bringing Distribution to Links

The distribution patches aim to break this barrier. It is now possible to use channel- and actor-based processes across this gap.

Minimal example: Processes

Here is a program which spawns a loop on the server, which response with a "Hello" value to any request it receives.

module Client {

  fun mainPage(serverPid) {
    var _ = spawnClient {
      fun loop(n) {
        var ourPid = self();
        if (n > 0) {
          serverPid ! Hi(ourPid);
          receive { case x -> print("Received hello!"); loop(n - 1) }
        } else {
          print("Done!");
        }
      }
      loop(5)
    };
    page
      <html>
        <h1>Hi!</h1>
      </html>
  }

}

module Server {

  fun go() {
    spawn {
      fun loop() {
        receive {
          case Hi(pid) -> pid ! Hello; loop()
        }
      }
      loop()
    }
  }

}

fun main() {
  var serverPid = Server.go();
  addRoute("/", fun (_, _) { Client.mainPage(serverPid) } );
  serveWebsockets();
  servePages()
}

main()

The Client module has a mainPage which creates a page, given a server process ID. spawnClient ensures a process is spawned on the client. Here, we spawn a process which loops 5 times: the process retrieves its own PID using self(), sends a message to the server, and blocks waiting for a response. The server hears the response and the PID, and sends back a Hello message. The client receives the response, decrements the counter, and loops.

The main extra bit of work to be done here is to call serveWebsockets() before servePages(), in order to initialise the websocket logic which underpins this.

Minimal Example: Sessions

module Sessions {
  typename Ping = [| Ping |];
  typename Pong = [| Pong |];
  typename PingPong =
    ?(Ping).!(Pong). [&| Again: PingPong, Stop: End |&];
}

open Sessions

module Client {

  fun mainPage(ap) {
    var _ = spawnClient {
      var c = request(ap);

      fun loop(n, c) {
        var ourPid = self();
        var c = send (Ping, c);
        var (_, c) = receive(c);
        print("Received pong!");
        if (n > 0) {
          var c = select Again c;
          loop(n - 1, c)
        } else {
          var _ = select Stop c;
          print("Done!");
        }
      }
      loop(5, c)
    };
    page
      <html>
        <h1>Hi!</h1>
      </html>
  }

}

module Server {

  fun clientLoop(c) {
    var (_, c) = receive(c);
    print("Received ping!");
    var c = send(Pong, c);
    offer(c) {
      case Again(c) -> clientLoop(c);
      case Stop(c) -> ()
    }
  }

  fun go(ap) {
    spawn {
      fun loop() {
        var c = accept(ap);
        clientLoop(c);
        loop()
      }
      loop()
    }
  }

}

fun main() {
  var ap = (new() : AP(PingPong));
  var _ = Server.go(ap);
  addRoute("/", fun (_, _) { Client.mainPage(ap) } );
  serveWebsockets();
  servePages()
}

main()

We can adapt the previous example to use session types. We firstly describe (in the Sessions module) a session type (from the view of the server) which receives a ping, receives a pong, and then receives a decision whether to loop and go again, or to stop.

In main(), we create an "access point" called ap using the new command. Note that while we choose to give this a type annotation, this isn't actually necessary---it helps with documentation and debugging, however. An access point is a "matchmaking service". In this case, the server gets the "server end" by passing ap as an argument to accept in the go function, whereas the client (running in the browser) gets the "client end" by passing ap as an argument to request.

More involved examples

You can find a distributed chat server in examples/distribution/chatserver. Run it from that directory to get styles to work.

Implementation

Distribution is implemented through the use of websockets. Each client is assigned a unique client ID, and if websockets are enabled, will then make a request to a websocket connection URL (default is "/ws/").

To implement all of this, we have abstracted and generalised PIDs, channels, and names. You can see these in processNames.ml.

Much of the machinery is in proc.ml.

Still to do

  • We don't do anything fancy when a client goes away mid-session. This is the subject of ongoing research, but we're looking to repurpose Affine Sessions (Mostrous & Vasconcelos, 2014).

  • Multiple server processes, as opposed to a single distinguished webserver

  • Access points for sessions can only reside on the server at the moment.

  • Multiparty sessions