So, you want to write a networked actor! Well, luckily that’s pretty easy, especially with all you know so far.
(use-modules (oop goops) (8sync) (ice-9 rdelim) ; line delineated i/o (ice-9 match)) ; pattern matching (define-actor <telcmd> (<actor>) ((*init* telcmd-init) (*cleanup* telcmd-cleanup) (new-client telcmd-new-client) (handle-line telcmd-handle-line)) (socket #:accessor telcmd-socket #:init-value #f))
Nothing surprising about the actor definition, though we do see that it has a slot for a socket. Unsurprisingly, that will be set up in the *init* handler.
(define (set-port-nonblocking! port) (let ((flags (fcntl port F_GETFL))) (fcntl port F_SETFL (logior O_NONBLOCK flags)))) (define (setup-socket) ;; our socket (define s (socket PF_INET SOCK_STREAM 0)) ;; reuse port even if busy (setsockopt s SOL_SOCKET SO_REUSEADDR 1) ;; connect to port 8889 on localhost (bind s AF_INET INADDR_LOOPBACK 8889) ;; make it nonblocking and start listening (set-port-nonblocking! s) (listen s 5) s) (define (telcmd-init telcmd message) (set! (telcmd-socket telcmd) (setup-socket)) (display "Connect like: telnet localhost 8889\n") (while (actor-alive? telcmd) (let ((client-connection (accept (telcmd-socket telcmd)))) (<- (actor-id telcmd) 'new-client client-connection)))) (define (telcmd-cleanup telcmd message) (display "Closing socket!\n") (when (telcmd-socket telcmd) (close (telcmd-socket telcmd))))
That setup-socket code looks pretty hard to read! But that’s pretty standard code for setting up a socket. One special thing is done though… the call to set-port-nonblocking! sets flags on the socket port so that, you guessed it, will be a nonblocking port.
This is put to immediate use in the telcmd-init method. This code looks suspiciously like it should block… after all, it just keeps looping forever. But since 8sync is using Guile’s suspendable ports code feature, so every time this loop hits the accept call, if that call would have blocked, instead this whole procedure suspends to the scheduler… automatically!… allowing other code to run.
So, as soon as we do accept a connection, we send a message to ourselves with the new-client action. But wait! Aren’t actors only supposed to handle one message at a time? If the telcmd-init loop just keeps on looping and looping, when will the new-client message ever be handled? 8sync actors only receive one message at a time, but by default if an actor’s message handler suspends to the agenda for some reason (such as to send a message or on handling I/O), that actor may continue to accept other messages, but always in the same thread.5
We also see that we’ve established a *cleanup* handler. This is run any time either the actor dies, either through self destructing, because the hive completes its work, or because a signal was sent to interrupt or terminate our program. In our case, we politely close the socket when <telcmd> dies.
(define (telcmd-new-client telcmd message client-connection) (define client (car client-connection)) (set-port-nonblocking! client) (let loop () (let ((line (read-line client))) (cond ((eof-object? line) (close client)) (else (<- (actor-id telcmd) 'handle-line client (string-trim-right line #\return)) (when (actor-alive? telcmd) (loop))))))) (define (telcmd-handle-line telcmd message client line) (match (string-split line #\space) (("") #f) ; ignore empty lines (("time" _ ...) (display (strftime "The time is: %c\n" (localtime (current-time))) client)) (("echo" rest ...) (format client "~a\n" (string-join rest " "))) ;; default (_ (display "Sorry, I don't know that command.\n" client))))
Okay, we have a client, so we handle it! And once again… we see this goes off on a loop of its own! (Also once again, we have to do the set-port-nonblocking! song and dance.) This loop also automatically suspends when it would otherwise block… as long as read-line has information to process, it’ll keep going, but if it would have blocked waiting for input, then it would suspend the agenda.6
The actual method called whenever we have a "line" of input is pretty straightforward… in fact it looks an awful lot like the IRC bot handle-line procedure we used earlier. No surprises there!7
Now let’s run it:
(let* ((hive (make-hive)) (telcmd (bootstrap-actor hive <telcmd>))) (run-hive hive '()))
Open up another terminal… you can connect via telnet:
$ telnet localhost 8889 Trying 127.0.0.1... Connected to localhost. Escape character is '^]'. time The time is: Thu Jan 5 03:20:17 2017 echo this is an echo this is an echo shmmmmmmorp Sorry, I don't know that command.
Horray, it works! Type Ctrl+] Ctrl+d to exit telnet.
Not so bad! There’s more that could be optimized, but we’ll consider that to be advanced topics of discussion.
So that’s a pretty solid intro to how 8sync works! Now that you’ve gone through this introduction, we hope you’ll have fun writing and hooking together your own actors. Since actors are so modular, it’s easy to have a program that has multiple subystems working together. You could build a worker queue system that displayed a web interface and spat out notifications about when tasks finish to IRC, and making all those actors talk to each other should be a piece of cake. The sky’s the limit!
This is customizable: an actor can be set up to queue messages so that absolutely no messages are handled until the actor completely finishes handling one message. Our loop couldn’t look quite like this though!
If there’s a lot of data coming in and you don’t want your I/O loop to become too "greedy", take a look at setvbuf.
Well, there may be one surprise to a careful observer. Why are we sending a message to ourselves? Couldn’t we have just dropped the argument of "message" to telcmd-handle-line and just called it like any other procedure? Indeed, we could do that, but sending a message to ourself has an added advantage: if we accidentally "break" the telcmd-handle-line procedure in some way (say we add a fun new command we’re playing with it), raising an exception won’t break and disconnect the client’s main loop, it’ll just break the message handler for that one line, and our telcmd will happily chug along accepting another command from the user while we try to figure out what happened to the last one.