1 ;;; 8sync --- Asynchronous programming for Guile
2 ;;; Copyright (C) 2016 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 ;; XUDD inspired actor system
21 (define-module (8sync systems actors)
22 #:use-module (oop goops)
23 #:use-module (srfi srfi-9)
24 #:use-module (srfi srfi-9 gnu)
25 #:use-module (ice-9 format)
26 #:use-module (ice-9 match)
27 #:use-module (ice-9 pretty-print)
28 #:use-module (8sync agenda)
29 #:use-module (8sync repl)
30 #:export (;; utilities... ought to go in their own module
32 big-random-number-string
33 simple-message-id-generator
41 ;;; Commenting out the <address> type for now;
42 ;;; it may be back when we have better serializers
45 address-actor-id address-hive-id
57 ;; There are more methods for the hive, but there's
58 ;; no reason for the outside world to look at them maybe?
60 hive-create-actor hive-create-actor*
64 message-to message-action message-from
65 message-id message-body message-in-reply-to
69 send-message send-message-wait
70 reply-message reply-message-wait
73 hive-bootstrap-message))
77 (make-parameter (random-state-from-platform)))
79 ;; Probably bigger than necessary
80 (define random-number-size (expt 10 50))
82 (define (big-random-number)
83 (random random-number-size (%random-state)))
85 ;; Would be great to get this base64 encoded instead.
86 (define (big-random-number-string)
87 ;; @@: This is slow. Using format here is wasteful.
88 (format #f "~x" (big-random-number)))
90 ;; @@: This is slow. A mere ~275k / second on my (old) machine.
91 ;; The main cost seems to be in number->string.
92 (define (simple-message-id-generator)
93 ;; Prepending this cookie makes message ids unique per hive
94 (let ((prefix (format #f "~x:" (big-random-number)))
97 (set! counter (1+ counter))
98 (string-append prefix (number->string counter)))))
100 (define (require-slot slot-name)
101 "Generate something for #:init-thunk to complain about unfilled slot"
103 (throw 'required-slot
104 (format #f "Slot ~s not filled" slot-name)
109 ;;; Main actor implementation
110 ;;; =========================
112 (define-class <actor> ()
114 (id #:init-thunk (require-slot "id")
117 ;; The hive we're connected to.
118 ;; We need this to be able to send messages.
119 (hive #:init-thunk (require-slot "hive")
120 #:init-keyword #:hive
121 #:accessor actor-hive)
122 ;; How we receive and process new messages
123 (message-handler #:init-thunk (require-slot "message-handler")
124 #:allocation #:each-subclass))
126 (define-method (actor-message-handler (actor <actor>))
127 (slot-ref actor 'message-handler))
129 ;;; So these are the nicer representations of addresses.
130 ;;; However, they don't serialize so easily with scheme read/write, so we're
131 ;;; using the simpler cons cell version below for now.
133 ;; (define-record-type <address>
134 ;; (make-address actor-id hive-id) ; @@: Do we want the trailing -id?
136 ;; (actor-id address-actor-id)
137 ;; (hive-id address-hive-id))
139 ;; (set-record-type-printer!
141 ;; (lambda (record port)
142 ;; (format port "<address: ~s@~s>"
143 ;; (address-actor-id record) (address-hive-id record))))
146 (define (make-address actor-id hive-id)
147 (cons actor-id hive-id))
149 (define (address-actor-id address)
152 (define (address-hive-id address)
155 (define (address->string address)
156 (string-append (address-actor-id address) "@"
157 (address-hive-id address)))
159 (define-method (actor-id-actor (actor <actor>))
160 "Get the actor id component of the actor-id"
161 (address-actor-id (actor-id actor)))
163 (define-method (actor-id-hive (actor <actor>))
164 "Get the hive id component of the actor-id"
165 (address-hive-id (actor-id actor)))
167 (define-method (actor-id-string (actor <actor>))
168 "Render the full actor id as a human-readable string"
169 (address->string (actor-id actor)))
176 (define (simple-dispatcher action-map)
177 (lambda (actor message)
178 (let* ((action (message-action message))
179 (method (assoc-ref action-map action)))
181 (throw 'action-not-found
182 "No appropriate action handler found for actor"
186 #:available-actions (map car action-map)))
187 (method actor message))))
189 (define-syntax %expand-action-item
191 ((_ ((action-name action-args ...) body ...))
192 (cons (quote action-name)
193 (lambda (action-args ...)
195 ((_ (action-name handler))
196 (cons (quote action-name) handler))))
198 (define-syntax make-action-dispatch
200 "Expand a list of action names and actions into an alist
202 You can use this like the following:
203 (make-action-dispatch
205 (lambda (actor message)
206 (display \"I love cookies!\n\")))
208 (lambda (actor message)
209 (display \"Life of the party!\"))))
211 Alternately, if you'd like to skip the lambda, you could use the slightly
212 more compact following syntax:
213 (make-action-dispatch
214 ((cookies actor message)
215 (display \"I love cookies!\n\"))
216 ((party actor message)
217 (display \"Life of the party!\")))"
218 ((make-action-dispatch action-item ...)
220 (list (%expand-action-item action-item) ...)))))
222 (define-syntax-rule (define-simple-actor class (actions ...))
223 (define-class class (<actor>)
225 #:init-value (make-action-dispatch actions ...)
226 #:allocation #:each-subclass)))
231 ;;; Every actor has a hive. The hive is a kind of "meta-actor"
232 ;;; which routes all the rest of the actors in a system.
234 (define-generic hive-handle-failed-forward)
236 (define-class <hive> (<actor>)
237 ;; This gets set to itself immediately after being created
238 (hive #:init-value #f)
239 (actor-registry #:init-thunk make-hash-table
240 #:getter hive-actor-registry)
241 (msg-id-generator #:init-thunk simple-message-id-generator
242 #:getter hive-msg-id-generator)
243 ;; Ambassadors are used (or will be) for inter-hive communication.
244 ;; These are special actors that know how to route messages to other hives.
245 (ambassadors #:init-thunk make-weak-key-hash-table
246 #:getter hive-ambassadors)
247 ;; Waiting coroutines
248 ;; This is a map from cons cell of message-id
249 ;; to a cons cell of (actor-id . coroutine)
250 ;; @@: Should we have a <waiting-coroutine> record type?
251 (waiting-coroutines #:init-thunk make-hash-table
252 #:getter hive-waiting-coroutines)
254 ;; When actors send messages to each other they abort to this prompt
255 ;; to send the message, then carry on their way
256 (prompt #:init-thunk make-prompt-tag
257 #:getter hive-prompt)
260 (make-action-dispatch
261 ;; This is in the case of an ambassador failing to forward a message...
262 ;; it reports it back to the hive
263 (*failed-forward* hive-handle-failed-forward))))
265 (define-method (hive-handle-failed-forward (hive <hive>) message)
266 "Handle an ambassador failing to forward a message"
269 (define* (make-hive #:key hive-id)
270 (let ((hive (make <hive>
273 (big-random-number-string))))))
274 ;; Set the hive's actor reference to itself
275 (set! (actor-hive hive) hive)
278 (define-method (hive-id (hive <hive>))
279 (actor-id-hive hive))
281 (define-method (hive-gen-actor-id (hive <hive>) cookie)
282 (make-address (if cookie
283 (string-append cookie "-" (big-random-number-string))
284 (big-random-number-string))
287 (define-method (hive-gen-message-id (hive <hive>))
288 "Generate a message id using HIVE's message id generator"
289 ((hive-msg-id-generator hive)))
291 (define-method (hive-resolve-local-actor (hive <hive>) actor-address)
292 (hash-ref (hive-actor-registry hive) actor-address))
294 (define-method (hive-resolve-ambassador (hive <hive>) ambassador-address)
295 (hash-ref (hive-ambassadors hive) ambassador-address))
297 (define-method (make-forward-request (hive <hive>) (ambassador <actor>) message)
298 (make-message (hive-gen-message-id hive) (actor-id ambassador)
299 ;; If we make the hive not an actor, we could either switch this
300 ;; to #f or to the original actor...?
301 ;; Maybe some more thinking should be done on what should
302 ;; happen in case of failure to forward? Handling ambassador failures
303 ;; seems like the primary motivation for the hive remaining an actor.
306 `((original . ,message))))
308 (define-method (hive-process-message (hive <hive>) message)
309 "Handle one message, or forward it via an ambassador"
310 (define (process-local-message)
311 (let ((actor (hive-resolve-local-actor hive (message-to message))))
313 (throw 'actor-not-found
314 (format #f "Message ~a from ~a directed to nonexistant actor ~a"
316 (address->string (message-from message))
317 (address->string (message-to message)))
319 (call-with-prompt (hive-prompt hive)
321 (define message-handler (actor-message-handler actor))
322 ;; @@: Should a more general error handling happen here?
323 (message-handler actor message))
325 (lambda (kont actor message)
326 (let ((hive (actor-hive actor)))
327 ;; Register the coroutine
328 (hash-set! (hive-waiting-coroutines hive)
330 (cons (actor-id actor) kont))
331 ;; Send off the message
332 (8sync (hive-process-message hive message)))))))
334 (define (resume-waiting-coroutine)
335 (match (hash-remove! (hive-waiting-coroutines hive)
336 (message-in-reply-to message))
339 (#f (throw 'no-waiting-coroutine
340 "message in-reply-to tries to resume nonexistent coroutine"
343 (define (process-remote-message)
344 ;; Find the ambassador
345 (let* ((remote-hive-id (hive-id (message-to message)))
346 (ambassador (hive-resolve-ambassador remote-hive-id))
347 (message-handler (actor-message-handler ambassador))
348 (forward-request (make-forward-request hive ambassador message)))
349 (message-handler ambassador forward-request)))
351 (let ((to (message-to message)))
352 ;; This seems to be an easy mistake to make, so check that addressing
355 (throw 'missing-addressee
356 "`to' field is missing on message"
358 (if (hive-actor-local? hive to)
359 (if (message-in-reply-to message)
360 (resume-waiting-coroutine)
361 (process-local-message))
362 (process-remote-message))))
364 (define-method (hive-actor-local? (hive <hive>) address)
365 (hash-ref (hive-actor-registry hive) address))
367 (define-method (hive-register-actor! (hive <hive>) (actor <actor>))
368 (hash-set! (hive-actor-registry hive) (actor-id actor) actor))
370 (define-method (%hive-create-actor (hive <hive>) actor-class
372 "Actual method called by hive-create-actor.
374 Since this is a define-method it can't accept fancy define* arguments,
375 so this gets called from the nicer hive-create-actor interface. See
376 that method for documentation."
377 (let* ((actor-id (hive-gen-actor-id hive id-cookie))
378 (actor (apply make actor-class
379 ;; @@: If we switch to a hive-proxy, do it here
383 (hive-register-actor! hive actor)
384 ;; return the actor id
387 (define* (hive-create-actor hive actor-class
391 (%hive-create-actor hive actor-class
394 (define-syntax hive-create-actor*
396 "Create an instance of actor-class attached to this hive.
397 Return the new actor's id.
399 Used internally, and used for bootstrapping a fresh hive.
401 Note that actors should generally not call this method directly.
402 Instead, actors should call create-actor."
403 ((_ args ... (init-args ...))
404 (hive-create-actor args ...
405 #:init (list init-args ...)))))
408 ;; TODO: Give actors this instead of the actual hive reference
409 (define-class <hive-proxy> ()
410 (send-message #:getter proxy-send-message
411 #:init-keyword #:send-message)
412 (create-actor #:getter proxy-create-actor
413 #:init-keyword #:create-actor))
415 ;; Live the hive proxy, but has access to the hive itself...
416 (define-class <debug-hive-proxy> (<hive-proxy>)
417 (hive #:init-keyword #:hive))
425 (define-record-type <message>
426 (make-message-intern id to from action
427 body in-reply-to wants-reply ; do we need hive-proxy?
428 ;; Are these still needed?
429 replied deferred-reply)
434 (action message-action)
436 (in-reply-to message-in-reply-to)
437 (wants-reply message-wants-reply)
439 ;; See XUDD source for these. Not use yet, maybe eventually will be?
440 ;; XUDD uses them for autoreply.
441 ;; Requiring mutation on message objects is clearly not great,
442 ;; but it may be worth it...? Investigate!
443 (replied message-replied set-message-replied!)
444 (deferred-reply message-deferred-reply set-message-deferred-reply!))
447 (define* (make-message id to from action body
448 #:key in-reply-to wants-reply
449 replied deferred-reply)
450 (make-message-intern id to from action body
451 in-reply-to wants-reply replied
454 ;; Note: the body of messages is currently an alist, but it's created
455 ;; from a keyword based property list (see the following two functions).
456 ;; But, that's an extra conversion step, and maybe totally unnecessary:
457 ;; we already have message-ref, and this could just pull a keyword
458 ;; from a property list.
459 ;; The main ways this might be useful are error checking,
460 ;; serialization across the wire (but even that might require some
461 ;; change), and using existing tooling (though adding new tooling
462 ;; would be negligible in implementation effort.)
464 (define* (message-ref message key #:optional dflt)
465 "Extract KEY from body of MESSAGE.
467 Optionally set default with [DFLT]"
468 (let ((result (assoc key (message-body message))))
469 (if result (cdr result)
473 (define (kwarg-list-to-alist args)
474 (let loop ((remaining args)
477 (((? keyword? key) val rest ...)
479 (cons (cons (keyword->symbol key) val)
482 (_ (throw 'invalid-kwarg-list
483 "Invalid keyword argument list"
487 (define (send-message from-actor to-id action . message-body-args)
488 "Send a message from an actor to another actor"
489 (let* ((hive (actor-hive from-actor))
490 (message (make-message (hive-gen-message-id hive) to-id
491 (actor-id from-actor) action
492 (kwarg-list-to-alist message-body-args))))
493 (8sync (hive-process-message hive message))))
495 (define (send-message-wait from-actor to-id action . message-body-args)
496 "Send a message from an actor to another, but wait until we get a response"
497 (let* ((hive (actor-hive from-actor))
498 (agenda-prompt (hive-prompt (actor-hive from-actor)))
499 (message (make-message (hive-gen-message-id hive) to-id
500 (actor-id from-actor) action
501 (kwarg-list-to-alist message-body-args)
503 (abort-to-prompt agenda-prompt from-actor message)))
505 ;; TODO: Intelligently ~propagate(ish) errors on -wait functions.
506 ;; We might have `send-message-wait-brazen' to allow callers to
507 ;; not have an exception thrown and instead just have a message with
508 ;; the appropriate '*error* message returned.
510 (define (reply-message from-actor original-message
513 (set-message-replied! original-message #t)
514 (let* ((hive (actor-hive from-actor))
515 (new-message (make-message (hive-gen-message-id hive)
516 (message-from original-message)
517 (actor-id from-actor) '*reply*
518 (kwarg-list-to-alist message-body-args)
519 #:in-reply-to (message-id original-message))))
520 (8sync (hive-process-message hive new-message))))
522 (define (reply-message-wait from-actor original-message
524 "Reply to a messsage, but wait until we get a response"
525 (set-message-replied! original-message #t)
526 (let* ((hive (actor-hive from-actor))
527 (agenda-prompt (hive-prompt (actor-hive from-actor)))
528 (new-message (make-message (hive-gen-message-id hive)
529 (message-from original-message)
530 (actor-id from-actor) '*reply*
531 (kwarg-list-to-alist message-body-args)
533 #:in-reply-to (message-id original-message))))
534 (abort-to-prompt agenda-prompt from-actor new-message)))
538 ;;; 8sync bootstrap utilities
539 ;;; =========================
541 (define* (ez-run-hive hive initial-tasks #:key repl-server)
542 "Start up an agenda and run HIVE in it with INITIAL-TASKS.
544 Should we start up a cooperative REPL for live hacking? REPL-SERVER
545 wants to know! You can pass it #t or #f, or if you want to specify a port,
547 (let* ((queue (list->q initial-tasks))
548 (agenda (make-agenda #:pre-unwind-handler print-error-and-continue
551 ;; If repl-server is an integer, we'll use that as the port
552 ((integer? repl-server)
553 (spawn-and-queue-repl-server! agenda repl-server))
555 (spawn-and-queue-repl-server! agenda)))
556 (start-agenda agenda)))
558 (define (hive-bootstrap-message hive to-id action . message-body-args)
560 (apply send-message hive to-id action message-body-args)))
564 ;;; Convenience procedures
565 ;;; ======================
567 (define (serialize-message message)
568 "Serialize a message for read/write"
571 (address->string (message-to message))
572 (address->string (message-from message))
573 (message-action message)
574 (message-body message)
575 (message-in-reply-to message)
576 (message-replied message)
577 (message-deferred-reply message)))
579 (define (write-message message port)
580 "Write out a message to a port for easy reading later.
582 Note that if a sub-value can't be easily written to something
583 Guile's `read' procedure knows how to read, this doesn't do anything
584 to improve that. You'll need a better serializer for that.."
585 (write (serialize-message message) port))
587 (define (serialize-message-pretty message)
588 "Serialize a message in a way that's easy for humans to read."
590 (id ,(message-id message))
591 (to ,(message-to message))
592 (from ,(message-from message))
593 (action ,(message-action message))
594 (body ,(message-body message))
595 (in-reply-to ,(message-in-reply-to message))
596 (replied ,(message-replied message))
597 (deferred-reply ,(message-deferred-reply message))))
599 (define (pprint-message message)
600 "Pretty print a message."
601 (pretty-print (serialize-message-pretty message)))