1 ;;; 8sync --- Asynchronous programming for Guile
2 ;;; Copyright © 2016, 2017 Christopher Allan Webber <cwebber@dustycloud.org>
4 ;;; This file is part of 8sync.
6 ;;; 8sync is free software: you can redistribute it and/or modify it
7 ;;; under the terms of the GNU Lesser General Public License as
8 ;;; published by the Free Software Foundation, either version 3 of the
9 ;;; License, or (at your option) any later version.
11 ;;; 8sync is distributed in the hope that it will be useful,
12 ;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
13 ;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 ;;; GNU Lesser General Public License for more details.
16 ;;; You should have received a copy of the GNU Lesser General Public
17 ;;; License along with 8sync. If not, see <http://www.gnu.org/licenses/>.
19 (define-module (8sync actors)
20 #:use-module (oop goops)
21 #:use-module (srfi srfi-9)
22 #:use-module (ice-9 control)
23 #:use-module (ice-9 format)
24 #:use-module (ice-9 match)
25 #:use-module (ice-9 atomic)
26 #:use-module (ice-9 pretty-print)
27 #:use-module (ice-9 receive)
29 #:use-module (fibers channels)
30 #:use-module (fibers conditions)
31 #:use-module (fibers operations)
32 #:use-module (8sync agenda)
33 #:use-module (8sync inbox)
34 #:use-module (8sync rmeta-slot)
35 #:export (;; utilities... ought to go in their own module
37 big-random-number-string
43 ;;; Commenting out the <address> type for now;
44 ;;; it may be back when we have better serializers
47 address-actor-id address-hive-id
54 actor-init! actor-cleanup!
64 ;; ;; There are more methods for the hive, but there's
65 ;; ;; no reason for the outside world to look at them maybe?
67 bootstrap-actor bootstrap-actor*
69 create-actor create-actor*
74 message-to message-action message-from
75 message-id message-body message-in-reply-to
83 (set! *random-state* (random-state-from-platform))
85 ;; Same size as a uuid4 I think...
86 (define random-number-size (expt 2 128))
88 (define (big-random-number)
89 (random random-number-size))
91 ;; Would be great to get this base64 encoded instead.
92 (define (big-random-number-string)
93 ;; @@: This is slow. Using format here is wasteful.
94 (format #f "~x" (big-random-number)))
96 ;; @@: This is slow-ish. A mere ~275k / second on my (old) machine.
97 ;; The main cost seems to be in number->string.
98 (define (simple-message-id-generator)
99 ;; Prepending this cookie makes message ids unique per hive
100 (let ((prefix (format #f "~x:" (big-random-number)))
103 (set! counter (1+ counter))
104 (string-append prefix (number->string counter)))))
111 (define-record-type <message>
112 (make-message-intern id to from action
113 body in-reply-to wants-reply)
115 ;; @@: message-ids are removed. They could be re-enabled
116 ;; if we had thread-safe promises...
117 (id message-id) ; id of this message
118 (to message-to) ; actor id this is going to
119 (from message-from) ; actor id of sender
120 (action message-action) ; action (a symbol) to be handled
121 (body message-body) ; argument list "body" of message
122 (in-reply-to message-in-reply-to) ; message id this is in reply to, if any
123 (wants-reply message-wants-reply)) ; whether caller is waiting for reply
126 (define* (make-message id to from action body
127 #:key in-reply-to wants-reply)
128 (make-message-intern id to from action body
129 in-reply-to wants-reply))
131 (define (kwarg-list-to-alist args)
132 (let loop ((remaining args)
135 (((? keyword? key) val rest ...)
137 (cons (cons (keyword->symbol key) val)
140 (_ (throw 'invalid-kwarg-list
141 "Invalid keyword argument list"
145 ;;; See: https://web.archive.org/web/20081223021934/http://mumble.net/~jar/articles/oo-moon-weinreb.html
146 ;;; (also worth seeing: http://mumble.net/~jar/articles/oo.html )
148 ;; This is the internal, generalized message sending method.
149 ;; Users shouldn't use it! Use the <-foo forms instead.
151 (define-inlinable (%<- wants-reply from-actor to action args message-id in-reply-to)
152 ;; Okay, we need to deal with message ids.
153 ;; Could we get rid of them? :\
154 ;; It seems if we can use eq? and have messages be immutable then
155 ;; it should be possible to identify follow-up replies.
156 ;; If we need to track replies across hive boundaries we could
157 ;; register unique ids across the ambassador barrier.
159 (#(_ _ (? channel? channel) dead?)
160 (let ((message (make-message message-id to
161 (and from-actor (actor-id from-actor))
163 #:wants-reply wants-reply
164 #:in-reply-to in-reply-to)))
167 (put-operation channel message)
168 (wait-operation dead?)))))
169 ;; TODO: put remote addresses here.
170 (#(actor-id hive-id #f #f)
171 ;; Here we'd make a call to our hive...
173 ;; A message sent to nobody goes nowhere.
174 ;; TODO: Should we display a warning here, probably?
177 (define (<- to action . args)
178 (define from-actor (*current-actor*))
179 (%<- #f from-actor to action args
181 ((actor-msg-id-generator from-actor)))
182 (big-random-number-string))
185 ;; TODO: this should abort to the prompt, then check for errors
188 (define (<-wait to action . args)
189 (define prompt (*actor-prompt*))
191 (error "Tried to <-wait without being in an actor's context..."))
193 (let ((reply (abort-to-prompt prompt to action args)))
194 (cond ((eq? action '*error*)
195 (throw 'hive-unresumable-coroutine
196 "Won't resume coroutine; got an *error* as a reply"
198 (else (apply values (message-body reply))))))
201 ;;; Main actor implementation
202 ;;; =========================
204 (define (actor-inheritable-message-handler actor message)
205 (define action (message-action message))
207 (class-rmeta-ref (class-of actor) 'actions action
208 #:equals? eq? #:cache-set! hashq-set!
209 #:cache-ref hashq-ref))
211 (throw 'action-not-found
212 "No appropriate action handler found for actor"
216 (apply method actor message (message-body message)))
218 (define-syntax-rule (build-actions (symbol method) ...)
219 "Construct an alist of (symbol . method), where the method is wrapped
220 with wrap-apply to facilitate live hacking and allow the method definition
221 to come after class definition."
223 (list (cons (quote symbol)
224 (wrap-apply method)) ...)))
226 (define-class <actor> ()
227 ;; An address object... a vector of #(actor-id hive-id inbox-channel dead?)
228 ;; - inbox-channel is the receiving channel (as opposed to actor-inbox-deq)
229 ;; - dead? is a fibers condition variable which is set once this actor
231 (id #:init-keyword #:address
233 ;; The connection to the hive we're connected to.
234 (hive-channel #:init-keyword #:hive-channel
235 #:accessor actor-hive-channel)
237 ;; Our queue to send/receive messages on
238 (inbox-deq #:init-thunk make-channel
239 #:accessor actor-inbox-deq)
241 (msg-id-generator #:init-thunk simple-message-id-generator
242 #:getter actor-msg-id-generator)
244 ;; How we receive and process new messages
245 (message-handler #:init-value actor-inheritable-message-handler
246 ;; @@: There's no reason not to use #:class instead of
247 ;; #:each-subclass anywhere in this file, except for
248 ;; Guile bug #25211 (#:class is broken in Guile 2.2)
249 #:allocation #:each-subclass
250 #:getter actor-message-handler)
253 ;; - #t as in, send the init message, but don't wait (default)
254 ;; - 'wait, as in wait on the init message
255 ;; - #f as in don't bother to init
256 (should-init #:init-value #t
257 #:getter actor-should-init
258 #:allocation #:each-subclass)
260 ;; This is the default, "simple" way to inherit and process messages.
261 (actions #:init-thunk (build-actions)
262 #:allocation #:each-subclass))
264 ;;; Actors may specify an "init" action that occurs before the actor
265 ;;; actually begins to run.
266 ;;; During actor-init!, an actor may send a message to itself or others
267 ;;; via <- but *may not* use <-wait.
268 (define-method (actor-init! (actor <actor>))
271 (define-method (actor-cleanup! (actor <actor>))
274 ;;; Addresses are vectors where the first part is the actor-id and
275 ;;; the second part is the hive-id. This works well enough... they
276 ;;; look decent being pretty-printed.
278 (define (make-address actor-id hive-id channel dead?)
279 (vector actor-id hive-id channel dead?))
281 (define (address-actor-id address)
282 (vector-ref address 0))
284 (define (address-hive-id address)
285 (vector-ref address 1))
287 (define (address-channel address)
288 (vector-ref address 2))
290 (define (address-dead? address)
291 (vector-ref address 3))
293 (define (address->string address)
294 (string-append (address-actor-id address) "@"
295 (address-hive-id address)))
297 (define (address-equal? address1 address2)
298 "Check whether or not the two addresses are equal.
300 This compares the actor-id and hive-id but ignores the channel and
303 (#(actor-id-1 hive-id-1 _ _)
305 (#(actor-id-2 hive-id-2)
306 (and (equal? actor-id-1 actor-id-2)
307 (and (equal? hive-id-1 hive-id-2))))
311 (define (actor-id-actor actor)
312 "Get the actor id component of the actor-id"
313 (address-actor-id (actor-id actor)))
315 (define (actor-id-hive actor)
316 "Get the hive id component of the actor-id"
317 (address-hive-id (actor-id actor)))
319 (define (actor-id-string actor)
320 "Render the full actor id as a human-readable string"
321 (address->string (actor-id actor)))
323 (define (actor-inbox-enq actor)
324 (address-channel (actor-id actor)))
326 (define *current-actor*
329 (define *actor-prompt*
332 (define (actor-main-loop actor)
333 "Main loop of the actor. Loops around, pulling messages off its queue
335 ;; @@: Maybe establish some sort of garbage collection routine for these...
338 (define message-handler
339 (actor-message-handler actor))
341 (address-dead? (actor-id actor)))
342 (define prompt (make-prompt-tag (actor-id-actor actor)))
344 (define (handle-message message)
349 (message-handler actor message))
351 ;; Return reply if necessary
352 (when (message-wants-reply message)
353 (when (message-wants-reply message)
354 (%<- #f actor (message-from message) '*reply*
355 vals ((actor-msg-id-generator actor))
356 (message-id message)))))))
358 (let ((err (current-error-port)))
361 (let ((stack (make-stack #t 4)))
362 (format err "Uncaught exception when handling message ~a:\n"
364 (display-backtrace stack err)
365 (print-exception err (stack-ref stack 0)
368 ;; If the other actor is waiting on a reply, let's let them
369 ;; know there was an error...
370 (when (message-wants-reply message)
371 (%<- #f actor (message-from message) '*error*
372 (list key) ((actor-msg-id-generator actor))
373 (message-id message)))))))))
375 (define (resume-handler message)
376 (define in-reply-to (message-in-reply-to message))
378 ((hash-ref waiting in-reply-to) =>
380 (hash-remove! waiting in-reply-to)
383 (format (current-error-port)
384 "Tried to resume nonexistant message: ~a\n"
385 (message-id message)))))
387 (define halt-or-handle-message
388 ;; It would be nice if we could give priorities to certain operations.
389 ;; halt should always win over getting a message...
391 (wrap-operation (wait-operation dead?)
392 (const #f)) ; halt and return
393 (wrap-operation (get-operation (actor-inbox-deq actor))
395 (call-with-prompt prompt
397 (if (message-in-reply-to message)
398 ;; resume a continuation which was waiting on a reply
399 (resume-handler message)
400 ;; start handling a new message
401 (handle-message message)))
402 ;; Here's where we abort to if we're doing <-wait
403 ;; @@: maybe use match-lambda if we're going to end up
404 ;; handling multiple ~commands
405 (lambda (kont to action message-args)
407 ((actor-msg-id-generator actor)))
408 (hash-set! waiting message-id kont)
409 (%<- #t actor to action message-args message-id #f)))
412 ;; Mutate the parameter; this should be fine since each fiber
413 ;; runs in its own dynamic state with with-dynamic-state.
414 ;; See with-dynamic-state discussion in
415 ;; https://wingolog.org/archives/2017/06/27/growing-fibers
416 (*current-actor* actor)
417 ;; We temporarily set the *actor-prompt* to #f to make sure that
418 ;; actor-init! doesn't try to do a <-wait message (and not accidentally use
419 ;; the parent fiber's *actor-prompt* either.)
422 (*actor-prompt* prompt)
425 (and (perform-operation halt-or-handle-message)
432 (define-syntax-rule (define-actor class inherits
435 (define-class class inherits
436 (actions #:init-thunk (build-actions action ...)
437 #:allocation #:each-subclass)
443 ;;; Every actor has a hive, which keeps track of other actors, manages
444 ;;; cleanup, and performs inter-hive communication.
446 (define-class <hive> ()
447 (id #:init-keyword #:id
449 (actor-registry #:init-thunk make-hash-table
450 #:getter hive-actor-registry)
451 ;; TODO: Rename "ambassadors" to "relays"
452 ;; Ambassadors are used (or will be) for inter-hive communication.
453 ;; These are special actors that know how to route messages to other
455 (ambassadors #:init-thunk make-weak-key-hash-table
456 #:getter hive-ambassadors)
457 (channel #:init-thunk make-channel
458 #:getter hive-channel)
459 (halt? #:init-thunk make-condition
460 #:getter hive-halt?))
462 (define* (make-hive #:key hive-id)
463 (make <hive> #:id (or hive-id
464 (big-random-number-string))))
466 (define (gen-actor-id cookie)
468 (string-append cookie ":" (big-random-number-string))
469 (big-random-number-string)))
471 (define (hive-main-loop hive)
472 "The main loop of the hive. This listens for messages on the hive-channel
473 for certain actions to perform.
475 `messages' here is not the same as a <message> object; these are a list of
476 values, the first value being a symbol"
477 (define channel (hive-channel hive))
478 (define halt? (hive-halt? hive))
479 (define registry (hive-actor-registry hive))
481 ;; not the same as a <message> ;P
482 (define handle-message
484 (('register-actor actor-id address actor)
485 (hash-set! registry actor-id (vector address actor)))
486 ;; Remove the actor from hive
487 (('remove-actor actor-id)
488 (hash-remove! (hive-actor-registry hive) actor-id))
489 (('register-ambassador hive-id ambassador-actor-id)
491 (('unregister-ambassador hive-id ambassador-actor-id)
493 (('forward-message from-actor-id message)
496 (define halt-or-handle
498 (wrap-operation (get-operation channel)
502 (wrap-operation (wait-operation halt?)
506 (and (perform-operation halt-or-handle)
509 (define *current-hive* (make-parameter #f))
511 (define* (spawn-hive proc #:key (hive (make-hive)))
512 "Spawn a hive in a fiber running PROC, passing it the fresh hive"
513 (spawn-fiber (lambda () (hive-main-loop hive)))
516 (define (run-hive proc . args)
517 "Spawn a hive and run it in run-fibers. Takes a PROC as would be passed
518 to spawn-hive... all remaining arguments passed to run-fibers."
524 (define (%create-actor hive-channel hive-id
525 actor-class init-args id-cookie send-init?)
526 (let* ((actor-id (gen-actor-id id-cookie))
527 (dead? (make-condition))
528 (inbox-enq (make-channel))
529 (address (make-address actor-id hive-id
531 (actor (apply make actor-class
532 #:hive-channel hive-channel
535 (should-init (actor-should-init actor)))
537 ;; start the main loop
538 (spawn-fiber (lambda ()
539 ;; start the inbox loop
542 (delivery-agent inbox-enq (actor-inbox-deq actor)
544 ;; this one is decidedly non-parallel, because we want
545 ;; the delivery agent to be in the same thread as its actor
548 (actor-main-loop actor))
551 (put-message hive-channel (list 'register-actor actor-id address actor))
553 ;; return the address
556 (define* (bootstrap-actor hive actor-class #:rest init-args)
557 "Create an actor on HIVE using ACTOR-CLASS passing in INIT-ARGS args"
558 (%create-actor (hive-channel hive) (hive-id hive) actor-class
559 init-args (symbol->string (class-name actor-class))
562 (define* (bootstrap-actor* hive actor-class id-cookie #:rest init-args)
563 "Create an actor, but also allow customizing a 'cookie' added to the id
565 (%create-actor (hive-channel hive) (hive-id hive) actor-class
569 (define* (create-actor from-actor actor-class #:rest init-args)
570 "Create an instance of actor-class. Return the new actor's id.
572 This is the method actors should call directly (unless they want
573 to supply an id-cookie, in which case they should use
575 (%create-actor (actor-hive-channel from-actor) (actor-id-hive from-actor)
576 actor-class init-args #f #t))
579 (define* (create-actor* from-actor actor-class id-cookie #:rest init-args)
580 "Create an instance of actor-class. Return the new actor's id.
582 Like create-actor, but permits supplying an id-cookie."
583 (%create-actor (actor-hive-channel from-actor) (actor-id-hive from-actor)
584 actor-class init-args id-cookie #t))
586 (define* (self-destruct actor #:key (cleanup #t))
587 "Remove an actor from the hive.
589 Unless #:cleanup is set to #f, this will first have the actor handle
590 its '*cleanup* action handler."
591 (signal-condition! (address-dead? (actor-id actor)))
592 (put-message (actor-hive-channel actor) (list 'remove-actor (actor-id-actor actor)))
593 ;; Set *actor-prompt* to nothing to prevent actor-cleanup! from sending
594 ;; a message with <-wait
596 (actor-cleanup! actor))
598 ;; From a patch I sent to Fibers...
599 (define (condition-signalled? cvar)
600 "Return @code{#t} if @var{cvar} has already been signalled.
602 In general you will want to use @code{wait} or @code{wait-operation} to
603 wait on a condition. However, sometimes it is useful to see whether or
604 not a condition has already been signalled without blocking."
605 (atomic-box-ref ((@@ (fibers conditions) condition-signalled?) cvar)))
607 (define (actor-alive? actor)
608 (condition-signalled? (address-dead? (actor-id actor))))