Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Distribution

a node dials another and gets one address back. everything after that is send, call and monitor, unchanged.

(define echo
  (spawn (lambda (me)
           (task-loop me
                      (lambda h (st msg)
                        (case (verb (req-body msg))
                          ((ping) (begin (reply msg 'pong) st))
                          ((stop) (begin (reply msg 'ok) (cons 'stop 'ok)))
                          (else (begin (refuse msg (bad-request (req-body msg))) st))))
                      '()))))

(define at (call '(host gate) '(listen (tcp "127.0.0.1:0"))))

(define driver
  (spawn (lambda (me)
           (task-loop me
                      (lambda h (st msg)
                        (let ((there (call '(host gate) (list 'dial (list 'tcp at)))))
                          (let ((e (call there '(lookup echo))))
                            (begin
                              (say (list 'far e))
                              (say (call e '(ping)))
                              (call e '(stop))
                              (call there '(stop))
                              (cons 'stop 'ok)))))
                      '()))))

(send driver 'go)
(hand greeter (list (cons 'echo echo)))
('far #<task 1 on 16>)
'pong

a node dialling itself, which is why one file shows the whole round trip. two processes differ in nothing.

hand greeter as the last form of a file is how a script becomes a node’s task 0. greet is a handler, not a loop, because which task runs it is not its business. registrations that arrived before the file reached its last form are in the loop’s backlog by then, so they go to the table rather than to the file’s body.

Task 0 is a convention

an address is only ever learned by being told, so a node just dialled is unreachable unless one address on it is known in advance. dial answers task 0 there, and the greeter is what conventionally sits in it.

a rendezvous, not a guard. once a link exists the peer can post to any task on the node by number. trust is per link, not per task.

everything else crosses as itself. an address cannot:

  • one the sender called its own becomes one of ours through this link
  • one the sender reached through this link becomes one of ours directly

the two cases swap, and the sender’s connection number, a name in the sender’s numbering, is discarded. this is exact rather than heuristic because an address is a host type a program cannot forge. ('task 1) written by hand is a two-element list and stays one.

an address naming a third node has no image on the far side, and sending one is refused with ('throw . 'third-party-address) rather than half-built. forwarding it would mean proxying for a link the receiver does not hold.

Two ways for a call to fail

('throw 'callee-down #<task 1 on 16> 'throw . 'noproc)
('throw 'callee-down #<task 1 on 16> 'throw . 'noconnection)

the first is the far node saying that task is not there. the second is not hearing from the far node at all.

a remote death carries its exit value:

(monitor e)
(call e '(stop))
(say (receive))
('task-down #<task 1 on 16> 'ok . 'bye)

when the value cannot be encoded the death is still reported, with ('throw . 'third-party-address) in its place.

Watching is a node frame, not a service

a link carries two frames addressed to the node itself rather than to a task on it: ('watch id) and ('down id result).

deliberately not a table held by some task. a task’s table is lost when the task restarts and nothing happens to say so, whereas the node knows about its own links because it is what holds them. the id used is one a task id can never be, so a frame for the node cannot be addressed to a task.

a watch set on a task that is already gone is answered at once. the round trip is the window, so a late watch would otherwise never be answered.

The gate

('dial (tcp "host:port"))     answers task 0 on the new link
('listen (tcp "host:port"))   answers the address it bound, and casts
                              ('peer addr) to the caller per connection

the gate is the only thing that opens a link, so it is the only thing that names one. the counter it hands out needs neither sharing nor an atomic, which is why a listener’s accepts come back through the gate instead of becoming links where they arrive.

(say (attempt (lambda () (call '(host gate) '(dial (carrier "pigeon"))))))
('throw 'no-transport 'carrier "pigeon")

there is no policy flag. what a program may open is what the world it runs in registers, and an embedding with a fixed set of peers dials them itself and serves no gate at all.

On the wire

a frame is a task id, a length and a payload. the length is the one field a peer can make arbitrarily large without sending anything, so it is checked before it is believed.

the payload encoding is not the printer. it carries what a printer could not:

  • sharing. a graph naming one pair a thousand times decodes to one pair, and the decoded side shares what the sender shared.
  • host types. a string travels as its type name and its own bytes, decoded by whatever the far node registered under that name. a type the far node does not have is a deployment fact, reported as such rather than as a corrupt frame.
  • the shapes that do not print back. a symbol prints as 'a, which reads as a two-element list.

a closure has no encoding, the same judgement send makes at the primitive. see Tasks and messages.

a frame this end cannot read ends the link rather than the message: the two sides disagree about the protocol, which is not something one message went wrong at.