Supervision
supervise is a task-loop with a particular handler, and that handler with
its helpers is seventy lines of the prelude. there is no supervisor behaviour
in the scheduler.
(define (supervise me specs)
(task-loop me (lambda h (st msg) (supervisor-turn st msg)) (start-all specs)))
a spec is (id limit . make), dotted, where make is the procedure a task
runs. a list of one spec is written:
(list (cons 'w (cons 2 worker)))
children start when the supervisor does, so a tree is running as soon as its root is.
(define (worker me)
(task-loop me
(lambda h (n msg)
(let ((ask (req-body msg)))
(case (verb ask)
((ping) (begin (reply msg 'pong) n))
((die) (begin (reply msg 'ok) (throw 'sad)))
((stop) (begin (reply msg 'ok) (cons 'stop 'ok)))
(else (begin (refuse msg (bad-request ask)) n)))))
0))
(define sup (spawn (lambda (me) (supervise me (list (cons 'w (cons 2 worker)))))))
(say (call sup '(which-children)))
(define w (call sup '(child w)))
(say (call w '(ping)))
(say (attempt (lambda () (call sup '(child nope)))))
(say (call w '(stop)))
(('w . #<task 2>))
'pong
('throw 'no-such-child . 'nope)
'ok
a supervisor answers ('which-children) and ('child id). it answers by id
because a restart is a new task and so a new address: the id is what makes a
restarted child the same child.
Transient, and only transient
a child that raised is started again. a child that returned did what it was for and is not. that is OTP’s transient strategy, and the only one of the three that needs no extra field, since the exit pair already says which happened.
(define sup (spawn (lambda (me) (supervise me (list (cons 'w (cons 1 worker)))))))
(define w (call sup '(child w)))
(call w '(die))
(define w2 (call sup '(child w)))
(say (list 'was w 'now w2))
(say (call w2 '(ping)))
('was #<task 2> 'now #<task 3>)
'pong
a restart is a fresh (make me), so initial state comes back from the spec.
there is no snapshot anywhere.
spend the budget and the supervisor gives up and stops, which anyone calling the child learns about the usual way:
(call w2 '(die))
(say (attempt (lambda () (call sup '(child w)))))
('throw 'callee-down #<task 1> 'ok 'give-up 'w 'throw . 'sad)
the supervisor exited ('ok . ('give-up 'w ('throw . 'sad))), naming the child
and what it died of. a supervisor with no children left also stops.
naj reports any task that ended by raising, so a script exercising restarts
exits 1 even when the supervisor handled everything correctly.
Limits
the limit field is a count of restarts left, or a rate (n . ms):
(cons 'w (cons 2 worker)) ; two restarts, ever
(cons 'w (cons (cons 3 1000) worker)) ; three per second
the count is the default because it needs no clock, and a tree built out of
counts runs in a world that registers no ('host clock) peer at all. see
Host capabilities.
Trees
a supervisor is a task, so a supervisor’s child can be a supervisor:
(define (branch me) (supervise me (list (cons 'a (cons 1 worker)))))
(define root (spawn (lambda (me) (supervise me (list (cons 'b (cons 1 branch)))))))
(define b (call root '(child b)))
(define a (call b '(child a)))
(say (call a '(ping)))
(call a '(stop))
'pong
nothing special happens for the nested case. branch is a procedure of one
address, which is all start-child wanted.
Spawn and watch are one step
(define (start-child id limit stamps make)
(list id (spawn-monitor make) limit stamps make))
written as two steps, a turn ending between them would let the child die before
the watch landed, and the supervisor would be told noproc instead of what
actually happened.