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

Calls and replies

(define (call to msg) (await (cons to msg)))

that is the whole request half. await reifies the call site’s continuation and hands it to whoever is running the prompt, as ('await to body k). the dispatch loop is running the prompt, so what it gets back from a turn is either the turn’s answer or a suspended call.

nothing blocks. the loop sends ('req me id body), records (id to . k) and goes back to receive. when ('reply id v) lands it resumes k with v.

(define counter
  (spawn (lambda (me)
           (task-loop me
                      (lambda h (n msg)
                        (case (verb (req-body msg))
                          ((bump) (begin (reply msg (+ n 1)) (+ n 1)))
                          ((read) (begin (reply msg n) n))
                          ((stop) (begin (reply msg 'bye) (cons 'stop n)))
                          (else (begin (refuse msg (bad-request (req-body msg))) n))))
                      0))))

(say (call counter '(bump)))
(say (call counter '(bump)))
(say (call counter '(read)))
(say (attempt (lambda () (call counter '(frobnicate)))))
(say (call counter '(stop)))
1
2
2
('throw 'bad-request 'frobnicate)
'bye

the server side is three accessors and two senders:

(define (req-from r) (cadr r))
(define (req-id r) (caddr r))
(define (req-body r) (cadddr r))
(define (reply req v) (send (req-from req) `(reply ,(req-id req) ,v)))
(define (refuse req why) (reply req (cons 'throw why)))

a reply is an ordinary message, so it need not come from the callee, or during the turn that received the request, or from a task at all:

((slow) (begin (send '(host clock)
                     `(after 20 ,(req-from msg) (reply ,(req-id msg) later)))
               n))
narju> (call server '(slow))
'later

the clock replied. the caller cannot tell.

A reply that is a failure

('throw . why) as a reply raises at the call site, which is what makes a refused request fail in the caller like a local error. the cost: you cannot return the pair ('throw . v) as data across an RPC. wrap it if you mean it.

One turn at a time

while a call is outstanding the loop still receives, but anything that is not its reply goes to a backlog and waits. a second turn started beside a suspended one would work from the state the suspended one still means to update, and whichever finished last would silently overwrite the other.

the backlog is read before the mailbox, so a message that has already waited once is not overtaken by one that just arrived.

to serve while a call is outstanding, put a different task’s state at stake:

(define f (future slow '(work)))
(say 'not-waiting-yet)
(say (call f '(read)))
'not-waiting-yet
42

future spawns a task that makes the call and then becomes a reference holding the answer. a future never ends on its own, so a program that must exit should not leave one parked.

When a call cannot be answered

the callee dying is not a hang. the loop monitors any address it calls, one monitor per callee, and turns the death into a raise at the call site:

(define dies
  (spawn (lambda (me) (task-loop me (lambda h (s msg) (throw 'nope)) '()))))
(say (attempt (lambda () (call dies '(hi)))))

(define gone (spawn (lambda (me) 'ok)))
(say (attempt (lambda () (call gone '(hi)))))
('throw 'callee-down #<task 1> 'throw . 'nope)
('throw 'callee-down #<task 2> 'ok . 'ok)
naj: task 1 failed: ('throw . 'nope)

the second finished normally rather than raising, and the caller is told which. the exit pair is carried whole, so callee-down says what a monitor would.

a callee that was already gone before the watch was armed answers ('throw . 'noproc) in that position, there being no exit left to report. the watch is armed before the request is sent, since the other order leaves a window where the monitor reports noproc about a task that died with something to say.

Timeouts

(call-within 50 slow '(anything))
('throw . 'timeout)

this arms a clock timer carrying the same id the real reply would, so whichever arrives second is dropped by the path a duplicate reply already takes.

giving up is not cancelling. the callee is still working and its eventual reply is discarded. nothing here can stop a task that is not listening.

Calling yourself

(call self '(anything))
naj: task 0 failed: ('throw 'self-call 'anything)

a deadlock, refused rather than parked, since parking would look like a peer taking its time. attempt around it does not help: the throw happens in the loop after the turn suspended, not inside the turn, so there is no protected region left to catch it.