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.