Rakka:   Actor Model for Racket
1 Quick Start
1.1 Proof of Life
1.2 Your First Gen  Server
2 Gen  Server In Depth
2.1 The Callbacks
2.2 Calls vs Casts
2.3 The handle-info Callback
3 Tutorial:   Key-Value Store
4 Naming and Discovery
5 When Servers Crash
5.1 Links:   Shared Fate
5.2 Monitors:   One-Way Watching
5.3 When to Use Which
6 Supervisors:   Automatic Recovery
6.1 Basic Supervision
6.2 Restart Strategies
6.3 Restart Types
6.4 Child Specs
7 Applications:   Structuring Real Programs
8 Under the Hood:   Raw Actors
8.1 Spawn and Messages
8.2 The Actor Loop Pattern
8.3 When to Use Raw Actors
9 Green Threads and Scheduling
9.1 Cooperative vs Preemptive
9.2 Threaded Mode
9.3 Performance Comparison
10 Gen  Statem:   State Machines
11 Distribution:   Actors Across Machines
11.1 Starting Nodes
11.2 Connecting and Messaging
11.3 Global Registry
11.4 Remote Spawning
12 Hot Code Reload
12.1 Basic Reload
12.2 State Migration
12.3 Tree-Wide Upgrades
13 Operational Features
13.1 Logging
13.2 Metrics
13.3 Process Introspection
13.4 Graceful Shutdown
14 Process Groups
15 Pub  Sub:   Topic-Based Messaging
16 Tables:   Shared State with Isolation
16.1 Your First Table
16.2 Why Tables Exist
16.3 Copy Semantics:   The Key Insight
16.4 Named Tables:   Global Access
16.5 Ownership and Lifecycle
16.6 Common Patterns
17 API Reference
17.1 Gen  Server
gen-server-start
gen-server-start-link
gen-server-call
gen-server-cast!
gen-server-stop
ok
stop-init
reply
noreply
stop
17.2 Supervisor
supervisor-start-link
child-spec*
strategy?
17.3 Process Primitives
spawn
spawn-link
self
send!
receive
receive/  timeout
receive-match
pid?
pid-alive?
17.4 Registry
register!
unregister!
whereis
registered
17.5 Links and Monitors
link!
unlink!
monitor!
demonitor!
set-trap-exit!
exit-signal
down-signal
17.6 Runtime Mode
runtime-mode
runtime-mode-green?
scheduler-start!
scheduler-stop!
scheduler-run-until-done!
scheduler-running?
17.7 Gen  Statem
gen-statem-start
gen-statem-start-link
gen-statem-call
gen-statem-cast!
17.8 Distribution
start-epmd!
stop-epmd!
start-node!
stop-node!
connect-node!
nodes
global-register!
global-whereis
global-send!
send-remote!
dist-link!
dist-monitor!
17.9 Hot Reload
version-info
hot-reload!
trigger-code-change!
upgrade-process!
upgrade-supervisor-tree!
17.10 Testing Utilities
wait-until
check-process-exits
with-fresh-registry
17.11 Process Groups
pg-join
pg-leave
pg-members
pg-which-groups
pg-monitor
pg-demonitor
pg-monitor-ref?
17.12 Pub  Sub
pubsub-start
pubsub-stop
pubsub-subscribe
pubsub-unsubscribe
pubsub-broadcast
pubsub-broadcast-from
pubsub-subscribers
17.13 Tables
table-new
table?
table-alive?
table-whereis
table-insert!
table-lookup
table-member?
table-remove!
table-insert-new!
table-update!
table-delete!
table-keys
table-count
table-to-list
table-clear!
table-info
18 License
9.1.0.1

Rakka: Actor Model for Racket🔗ℹ

Aldric Giacomoni

 (require rakka)

Erlang/OTP-style actors for Racket 9. Isolated processes, message passing, supervision trees, and hot code reload.

For you if: You want fault-tolerant concurrency without shared-state threading.

    1 Quick Start

      1.1 Proof of Life

      1.2 Your First GenServer

    2 GenServer In Depth

      2.1 The Callbacks

      2.2 Calls vs Casts

      2.3 The handle-info Callback

    3 Tutorial: Key-Value Store

    4 Naming and Discovery

    5 When Servers Crash

      5.1 Links: Shared Fate

      5.2 Monitors: One-Way Watching

      5.3 When to Use Which

    6 Supervisors: Automatic Recovery

      6.1 Basic Supervision

      6.2 Restart Strategies

      6.3 Restart Types

      6.4 Child Specs

    7 Applications: Structuring Real Programs

    8 Under the Hood: Raw Actors

      8.1 Spawn and Messages

      8.2 The Actor Loop Pattern

      8.3 When to Use Raw Actors

    9 Green Threads and Scheduling

      9.1 Cooperative vs Preemptive

      9.2 Threaded Mode

      9.3 Performance Comparison

    10 GenStatem: State Machines

    11 Distribution: Actors Across Machines

      11.1 Starting Nodes

      11.2 Connecting and Messaging

      11.3 Global Registry

      11.4 Remote Spawning

    12 Hot Code Reload

      12.1 Basic Reload

      12.2 State Migration

      12.3 Tree-Wide Upgrades

    13 Operational Features

      13.1 Logging

      13.2 Metrics

      13.3 Process Introspection

      13.4 Graceful Shutdown

    14 Process Groups

    15 PubSub: Topic-Based Messaging

    16 Tables: Shared State with Isolation

      16.1 Your First Table

      16.2 Why Tables Exist

      16.3 Copy Semantics: The Key Insight

      16.4 Named Tables: Global Access

      16.5 Ownership and Lifecycle

      16.6 Common Patterns

    17 API Reference

      17.1 GenServer

      17.2 Supervisor

      17.3 Process Primitives

      17.4 Registry

      17.5 Links and Monitors

      17.6 Runtime Mode

      17.7 GenStatem

      17.8 Distribution

      17.9 Hot Reload

      17.10 Testing Utilities

      17.11 Process Groups

      17.12 PubSub

      17.13 Tables

    18 License

1 Quick Start🔗ℹ

1.1 Proof of Life🔗ℹ

Run an actor in 3 lines:

#lang rakka
(require rakka)
 
(spawn (lambda () (displayln "I'm an actor!")))

You just ran an isolated process. Now let’s build something useful.

1.2 Your First GenServer🔗ℹ

One-shot actors aren’t practical. For stateful services, use GenServer:

#lang racket/base
(require rakka racket/match)
 
(struct counter ()
  #:methods gen:server
  [(define (init self args)
     (ok 0))  ; start at 0
 
   (define (handle-call self msg state from)
     (match msg
       ['get (reply state state)]
       ['inc (reply 'ok (add1 state))]))
 
   (define (handle-cast self msg state)
     (noreply state))
 
   (define (handle-info self msg state)
     (noreply state))
 
   (define (terminate self reason state)
     (void))])
 
;; Use it
(define c (gen-server-start (counter) #f))
(gen-server-call c 'inc)  ; => 'ok
(gen-server-call c 'inc)  ; => 'ok
(gen-server-call c 'get)  ; => 2
(gen-server-stop c)

What you get for free:
  • State management (no manual loop)

  • Request/reply handled for you

  • Timeouts, shutdown, error handling built in

2 GenServer In Depth🔗ℹ

GenServer is the workhorse of Rakka. Most actors you write will be GenServers.

2.1 The Callbacks🔗ℹ

You implement these, Rakka calls them:

Callback

  

When Called

  

You Return

init

  

Server starts

  

(ok state) or (stop-init reason)

handle-call

  

Sync request (caller waits)

  

(reply value new-state)

handle-cast

  

Async message (fire-and-forget)

  

(noreply new-state)

handle-info

  

Any other message

  

(noreply new-state)

terminate

  

Server stopping

  

(void)

2.2 Calls vs Casts🔗ℹ

Calls are synchronous—the caller blocks until you reply:

;; Client side
(gen-server-call my-server 'get-data)  ; blocks
 
;; Server side (handle-call)
(reply the-data new-state)  ; unblocks the caller

Casts are asynchronous—fire and forget:

;; Client side
(gen-server-cast! my-server 'reset)  ; returns immediately
 
;; Server side (handle-cast)
(noreply new-state)  ; no reply expected

Use calls when you need a response. Use casts when you don’t.

2.3 The handle-info Callback🔗ℹ

Messages that aren’t calls or casts go to handle-info. This includes:
  • Messages sent with plain send!

  • Exit signals from linked processes (if trapping exits)

  • Monitor down signals

  • Timer messages

3 Tutorial: Key-Value Store🔗ℹ

Let’s build a practical GenServer—an in-memory key-value store.

#lang racket/base
(require rakka racket/match)
 
(struct kv-store ()
  #:methods gen:server
  [(define (init self args)
     (ok (make-hash)))  ; state is a hash table
 
   (define (handle-call self msg state from)
     (match msg
       [(list 'get key)
        (reply (hash-ref state key #f) state)]
       [(list 'keys)
        (reply (hash-keys state) state)]))
 
   (define (handle-cast self msg state)
     (match msg
       [(list 'put key value)
        (hash-set! state key value)
        (noreply state)]
       [(list 'delete key)
        (hash-remove! state key)
        (noreply state)]
       ['clear
        (noreply (make-hash))]))
 
   (define (handle-info self msg state)
     (noreply state))
 
   (define (terminate self reason state)
     (void))])
 
;; Try it
(define store (gen-server-start (kv-store) #f))
 
(gen-server-cast! store '(put name "Alice"))
(gen-server-cast! store '(put age 30))
 
(gen-server-call store '(get name))   ; => "Alice"
(gen-server-call store '(keys))       ; => '(name age)
 
(gen-server-cast! store '(delete age))
(gen-server-call store '(keys))       ; => '(name)
 
(gen-server-stop store)

Notice the pattern:
  • Reads use handle-call (caller needs the data)

  • Writes use handle-cast (no response needed)

4 Naming and Discovery🔗ℹ

Instead of passing PIDs around, register names:

;; Start with a name
(gen-server-start (kv-store) #f #:name 'cache)
 
;; Now anyone can find it
(gen-server-call 'cache '(get user-123))

Or register after the fact:

(define pid (gen-server-start (kv-store) #f))
(register! 'cache pid)
 
;; Look up by name
(define cache-pid (whereis 'cache))

whereis returns #f if the name isn’t registered.

5 When Servers Crash🔗ℹ

Actors crash. Rakka gives you tools to respond.

5.1 Links: Shared Fate🔗ℹ

Linked processes die together. If one crashes, the other gets an exit signal:

;; Link when starting
(gen-server-start-link (my-server) args)
 
;; Or link explicitly
(link! some-pid)

By default, exit signals kill the receiver. To catch them instead:

(set-trap-exit! (self) #t)
 
;; Now exits arrive in handle-info
(define (handle-info self msg state)
  (match msg
    [(exit-signal from reason)
     (printf "~a died: ~a\n" from reason)
     (noreply state)]
    [_ (noreply state)]))

5.2 Monitors: One-Way Watching🔗ℹ

Monitors let you watch without shared fate. If the target dies, you get notified but don’t die:

(define ref (monitor! some-pid))
 
;; In handle-info:
(match msg
  [(down-signal ref pid reason)
   (printf "~a died: ~a\n" pid reason)
   (noreply state)])

Use demonitor! to stop watching.

5.3 When to Use Which🔗ℹ

Mechanism

  

Use When

Link

  

Processes should live and die together (e.g., worker and its coordinator)

Monitor

  

You need to know about death but survive it (e.g., connection pool tracking clients)

6 Supervisors: Automatic Recovery🔗ℹ

Instead of handling every possible error, let processes crash and restart clean. This is the "let it crash" philosophy.

6.1 Basic Supervision🔗ℹ

(define sup-pid
  (supervisor-start-link
   'one-for-one    ; restart strategy
   3               ; max 3 restarts
   5               ; in 5 seconds
   (list
    (child-spec* #:id 'cache
                 #:start (lambda () (gen-server-start-link (kv-store) #f))
                 #:restart 'permanent)
    (child-spec* #:id 'worker
                 #:start (lambda () (gen-server-start-link (worker) #f))
                 #:restart 'permanent))))

If a child crashes, the supervisor restarts it. If it crashes too often (3 times in 5 seconds), the supervisor gives up and terminates.

6.2 Restart Strategies🔗ℹ

Strategy

  

Behavior

'one-for-one

  

Restart only the crashed child

'one-for-all

  

Restart all children if one crashes

'rest-for-one

  

Restart crashed child and those started after it

'simple-one-for-one

  

Dynamic children, all same spec

6.3 Restart Types🔗ℹ

Type

  

Behavior

'permanent

  

Always restart

'temporary

  

Never restart

'transient

  

Restart only on abnormal exit

6.4 Child Specs🔗ℹ

(child-spec* #:id 'my-worker           ; unique identifier
             #:start start-fn          ; (-> pid?) returns linked pid
             #:restart 'permanent      ; restart policy
             #:shutdown 5000)          ; ms to wait for graceful stop

7 Applications: Structuring Real Programs🔗ℹ

An Application is your top-level entry point. It starts your supervision tree.

(struct my-app ()
  #:methods gen:application
  [(define (start self args)
     (define sup (supervisor-start-link
                   'one-for-one 3 5
                   (list
                     (child-spec* #:id 'cache
                                  #:start cache-start
                                  #:restart 'permanent)
                     (child-spec* #:id 'web
                                  #:start web-start
                                  #:restart 'permanent))))
     (app-ok sup))
 
   (define (stop self state)
     (void))])
 
;; Start the application
(application-start (my-app) '())

This gives you:
  • A supervised tree of processes

  • Automatic restart on failures

  • Clean shutdown when the app stops

8 Under the Hood: Raw Actors🔗ℹ

GenServer is built on lower-level primitives. You usually don’t need these, but understanding them helps.

8.1 Spawn and Messages🔗ℹ

;; Spawn a process
(define pid (spawn (lambda ()
                     (displayln "Started!")
                     (define msg (receive))  ; block for message
                     (printf "Got: ~a\n" msg))))
 
;; Send it a message
(send! pid 'hello)

8.2 The Actor Loop Pattern🔗ℹ

Raw actors typically loop, handling messages:

(define (counter-actor initial)
  (let loop ([count initial])
    (match (receive)
      ['inc (loop (add1 count))]
      [(list 'get from)
       (send! from count)
       (loop count)]
      ['stop (void)])))

This is exactly what GenServer abstracts. Compare:
  • Raw: You write the loop, handle replies manually

  • GenServer: You implement callbacks, infrastructure handles the rest

8.3 When to Use Raw Actors🔗ℹ

Almost never. Use raw actors only when:
  • You need custom message scheduling (e.g., priority queues)

  • You’re building infrastructure on top of Rakka

  • You’re learning how actors work internally

For application code, use GenServer.

9 Green Threads and Scheduling🔗ℹ

Rakka uses green threads by default—lightweight cooperative scheduling that runs thousands of actors on a single OS thread.

9.1 Cooperative vs Preemptive🔗ℹ

Green threads yield at receive points. But what if an actor computes without receiving? It starves other actors.

Solution: Use #lang rakka for fair scheduling.

#lang rakka  ; <-- enables preemptive yielding
(require rakka)
 
(define (fib n)
  (if (<= n 1) n
      (+ (fib (- n 1)) (fib (- n 2)))))
 
;; Even fib(30) won't starve other actors
(spawn (lambda () (printf "fib(30) = ~a\n" (fib 30))))
(spawn (lambda () (displayln "I run too!")))

#lang rakka counts function calls and yields after ~4000, like Erlang’s reduction counting.

9.2 Threaded Mode🔗ℹ

For CPU-bound parallelism, switch to OS threads:

(runtime-mode 'threaded)
 
(spawn (lambda ()
  ;; This runs on a real OS thread
  (heavy-computation)))

9.3 Performance Comparison🔗ℹ

Operation

  

Green

  

Threaded

Spawn 1000 actors

  

2ms

  

45ms

50k messages

  

32ms

  

5,300ms

Use green mode unless you need CPU parallelism.

10 GenStatem: State Machines🔗ℹ

When behavior depends on state, use GenStatem (generic state machine).

(struct traffic-light ()
  #:methods gen:statem
  [(define (statem-init self args)
     (statem-ok 'red 0))  ; state='red, data=cycle-count
 
   (define (callback-mode self) 'handle-event)
 
   (define (handle-event self event-type event state data)
     (match* (event-type (call-event-request event))
       [('call 'next)
        (define new-state
          (match state ['red 'green] ['green 'yellow] ['yellow 'red]))
        (define new-data
          (if (eq? new-state 'red) (add1 data) data))
        (next-state new-state new-data (statem-reply event new-state))]
       [('call 'cycles)
        (keep-state-and-data (statem-reply event data))]))
 
   (define (statem-terminate self reason state data)
     (void))])
 
(define light (gen-statem-start (traffic-light) #f))
(gen-statem-call light 'next)   ; => 'green
(gen-statem-call light 'next)   ; => 'yellow
(gen-statem-call light 'next)   ; => 'red
(gen-statem-call light 'cycles) ; => 1

Use GenStatem when:
  • Valid operations depend on current state

  • You have explicit state transitions

  • State machine is the natural model

For simple stateful services, GenServer is usually clearer.

11 Distribution: Actors Across Machines🔗ℹ

Rakka supports Erlang-style distribution with EPMD protocol compatibility.

11.1 Starting Nodes🔗ℹ

;; Start EPMD (once per machine)
(define epmd (start-epmd! 4369))
 
;; Start this node
(start-node! "mynode@localhost" 9001
             #:cookie "secret-cookie")

The cookie must match between nodes.

11.2 Connecting and Messaging🔗ℹ

;; On node2
(start-node! "node2@localhost" 9002
             #:cookie "secret-cookie")
 
(connect-node! "mynode@localhost")
 
;; Send to a globally registered process
(global-send! 'calculator '(add 1 2))

11.3 Global Registry🔗ℹ

;; Register globally (visible on all nodes)
(global-register! 'calculator my-pid)
 
;; Look up from any node
(define calc (global-whereis 'calculator))

11.4 Remote Spawning🔗ℹ

;; On remote node, register a spawn handler
(register-spawn-handler! 'my-worker
  (lambda (args) (spawn (lambda () (worker-loop args)))))
 
;; From any node, spawn remotely
(define remote-pid
  (request-spawn! "node2@localhost" 'my-worker '(args)))

12 Hot Code Reload🔗ℹ

Update running code without stopping your system.

12.1 Basic Reload🔗ℹ

(hot-reload! "my-server.rkt")

12.2 State Migration🔗ℹ

When code changes, the code-change callback transforms old state to new:

(struct my-server ()
  #:methods gen:server
  [;; ... other callbacks ...
 
   (define (code-change self version-info state)
     (match (version-info-new-version version-info)
       ["2.0"
        ;; Migrate state from v1 to v2
        (ok (upgrade-state state))]
       [_ (ok state)]))])
 
;; Trigger the upgrade
(trigger-code-change! pid (version-info "1.0" "2.0" #f))

12.3 Tree-Wide Upgrades🔗ℹ

Upgrade an entire supervision tree (children first, then supervisor):

(upgrade-supervisor-tree! sup-pid (version-info "1.0" "2.0" #f))

13 Operational Features🔗ℹ

13.1 Logging🔗ℹ

(current-rakka-log-level 'debug)
(set-process-log-level! some-pid 'error)
 
(rakka-log-info "Starting on port ~a" port)
(rakka-log-error "Connection failed: ~a" reason)

Lifecycle events (SPAWN, EXIT, CRASH, RESTART) are logged automatically.

13.2 Metrics🔗ℹ

;; Built-in metrics
(counter-value metrics-process-spawns)
(gauge-value metrics-active-processes)
 
;; Custom metrics
(define reqs (make-counter 'http-requests))
(counter-inc! reqs)
 
(define conns (make-gauge 'connections))
(gauge-inc! conns)
(gauge-dec! conns)

13.3 Process Introspection🔗ℹ

(sys-get-state some-pid)   ; get current state
(sys-suspend some-pid)     ; pause processing
(sys-resume some-pid)      ; resume
(sys-trace some-pid #t)    ; enable tracing

13.4 Graceful Shutdown🔗ℹ

(on-shutdown
 (lambda (ctx) (close-connections))
 #:priority 10
 #:name 'cleanup)
 
(graceful-shutdown #:timeout 30000)

14 Process Groups🔗ℹ

Group processes together for easy lookup and messaging. Inspired by Erlang/OTP’s pg module.

;; Add a process to a group
(pg-join 'workers (self))
 
;; Get all processes in a group
(define workers (pg-members 'workers))
(for ([w workers]) (send! w 'do-work))
 
;; Leave a group
(pg-leave 'workers (self))
 
;; See all groups
(pg-which-groups)  ; => '(workers ...)

Groups auto-create on first join. Processes are automatically removed when they die. A process can join the same group multiple times (refcounted)—it must leave the same number of times.

Note: This is local-only. Erlang’s pg module synchronizes across cluster nodes; ours does not yet.

15 PubSub: Topic-Based Messaging🔗ℹ

Publish-subscribe for decoupled communication, inspired by Phoenix.PubSub.

;; Start a named pubsub
(pubsub-start 'events)
 
;; Subscribe to a topic (in an actor)
(pubsub-subscribe 'events "user:123")
 
;; Broadcast to all subscribers
(pubsub-broadcast 'events "user:123" '(new-message "hello"))
 
;; Broadcast excluding yourself
(pubsub-broadcast-from 'events "user:123" '(typing-indicator))
 
;; Unsubscribe
(pubsub-unsubscribe 'events "user:123")
 
;; Stop when done
(pubsub-stop 'events)

Subscribers are automatically removed when they die (uses process groups under the hood).

Note: This is local-only. Phoenix.PubSub broadcasts across cluster nodes; ours does not yet.

16 Tables: Shared State with Isolation🔗ℹ

Mutable key-value storage that multiple actors can access safely. Values are copied in and out, so mutations never leak between actors.

For you if: You need shared state (caches, counters, lookup tables) but want the safety of message passing.

16.1 Your First Table🔗ℹ

#lang racket/base
(require rakka)
 
;; Create a table (must be inside an actor)
(spawn (lambda ()
  (define cache (table-new 'my-cache #:type 'set))
 
  (table-insert! cache 'user-123 '((name . "Alice") (score . 42)))
  (table-lookup cache 'user-123)  ; => '((name . "Alice") (score . 42))
 
  (table-update! cache 'user-123
    (lambda (user) (cons '(vip . #t) user))
    '())
 
  (displayln (table-lookup cache 'user-123))))

That’s it. You have isolated shared state.

16.2 Why Tables Exist🔗ℹ

Rakka actors don’t share memory—they communicate by message passing. But sometimes you genuinely need shared state:

  • Caches: Multiple actors checking the same cached data

  • Counters: Incrementing a shared metric

  • Lookup tables: A routing table that many actors consult

You could create a GenServer that wraps a hash table, but:
  • Every read requires a message round-trip

  • The GenServer becomes a bottleneck

  • Simple lookups feel heavy

Tables solve this with a different tradeoff: direct access, but values are copied on every insert and lookup. This preserves isolation—mutating a looked-up value doesn’t affect the table.

16.3 Copy Semantics: The Key Insight🔗ℹ

Tables copy values in and out. This is crucial:

(spawn (lambda ()
  (define t (table-new 'isolation-demo #:type 'set))
 
  ;; Insert a mutable hash
  (define h (make-hash '((count . 0))))
  (table-insert! t 'data h)
 
  ;; Mutate the original
  (hash-set! h 'count 999)
 
  ;; Table still has the old value!
  (hash-ref (table-lookup t 'data) 'count)  ; => 0
 
  ;; Lookup returns a copy too
  (define result (table-lookup t 'data))
  (hash-set! result 'count 777)
  (hash-ref (table-lookup t 'data) 'count)  ; => still 0
))

Rule: What goes in stays in. What comes out is a copy.

This uses Racket’s place-message-allowed? constraint—only serializable values can be stored. No procedures, no opaque structs.

16.4 Named Tables: Global Access🔗ℹ

Anonymous tables require passing the reference. Named tables can be looked up by symbol:

;; Create a named table
(spawn (lambda ()
  (table-new 'sessions #:type 'set #:named #t)
  (table-insert! 'sessions 'sess-abc '((user . "alice")))
  ;; ...
))
 
;; From another actor, find it by name
(spawn (lambda ()
  (define sessions (table-whereis 'sessions))
  (when sessions
    (table-lookup sessions 'sess-abc))))
 
;; Or use the name directly in operations
(spawn (lambda ()
  (table-insert! 'sessions 'sess-xyz '((user . "bob")))))

table-whereis returns #f if the name doesn’t exist.

16.5 Ownership and Lifecycle🔗ℹ

Every table has an ownerthe actor that created it. When the owner dies, the table dies too:

(define owner
  (spawn (lambda ()
    (table-new 'will-die #:type 'set #:named #t)
    (receive))))  ; wait forever
 
(table-whereis 'will-die)  ; => the table
 
(send! owner 'goodbye)     ; owner exits
(sleep 0.1)                ; let death propagate
 
(table-whereis 'will-die)  ; => #f (table is gone)

This automatic cleanup prevents orphaned tables. If you need a table to outlive its creator, have a long-lived actor (like a supervisor’s child) own it.

16.6 Common Patterns🔗ℹ

Counter with atomic increment:
(table-update! counters 'page-views add1 0)  ; returns new value

Cache with conditional insert:
(unless (table-member? cache key)
  (table-insert! cache key (expensive-computation)))
(table-lookup cache key)

Or atomically:
(when (table-insert-new! cache key computed-value)
  (log-info "Cache miss for ~a" key))

Iterate all entries:
(for ([entry (table-to-list my-table)])
  (printf "~a: ~a\n" (car entry) (cdr entry)))

17 API Reference🔗ℹ

17.1 GenServer🔗ℹ

procedure

(gen-server-start impl args [#:name name])  pid?

  impl : server?
  args : any/c
  name : (or/c symbol? #f) = #f
Starts a GenServer. Optionally registers under name.

procedure

(gen-server-start-link impl    
  args    
  [#:name name])  pid?
  impl : server?
  args : any/c
  name : (or/c symbol? #f) = #f
Like gen-server-start, but links to the caller.

procedure

(gen-server-call server    
  request    
  [#:timeout timeout])  any/c
  server : (or/c pid? symbol?)
  request : any/c
  timeout : exact-positive-integer? = 5000
Sends request and waits for reply. Raises error on timeout.

procedure

(gen-server-cast! server request)  void?

  server : (or/c pid? symbol?)
  request : any/c
Sends request asynchronously. No reply.

procedure

(gen-server-stop server [reason])  void?

  server : (or/c pid? symbol?)
  reason : any/c = 'normal
Stops the server.

struct

(struct ok (state)
    #:extra-constructor-name make-ok)
  state : any/c
Return from init to start with state.

struct

(struct stop-init (reason)
    #:extra-constructor-name make-stop-init)
  reason : any/c
Return from init to abort startup.

struct

(struct reply (value new-state)
    #:extra-constructor-name make-reply)
  value : any/c
  new-state : any/c
Return from handle-call to reply with value.

struct

(struct noreply (new-state)
    #:extra-constructor-name make-noreply)
  new-state : any/c
Return from handle-cast or handle-info.

struct

(struct stop (reason reply-value new-state)
    #:extra-constructor-name make-stop)
  reason : any/c
  reply-value : any/c
  new-state : any/c
Return from any callback to stop the server.

17.2 Supervisor🔗ℹ

procedure

(supervisor-start-link strategy    
  max-restarts    
  max-seconds    
  child-specs)  pid?
  strategy : strategy?
  max-restarts : exact-nonnegative-integer?
  max-seconds : exact-positive-integer?
  child-specs : (listof child-spec?)
Starts a supervisor linked to the caller.

procedure

(child-spec* #:id id    
  #:start start    
  [#:restart restart    
  #:shutdown shutdown])  child-spec?
  id : any/c
  start : (-> pid?)
  restart : (or/c 'permanent 'temporary 'transient) = 'permanent
  shutdown : (or/c 'brutal 'infinity exact-nonnegative-integer?)
   = 5000
Creates a child specification.

procedure

(strategy? v)  boolean?

  v : any/c
Returns #t if v is one of: 'one-for-one, 'one-for-all, 'rest-for-one, 'simple-one-for-one.

17.3 Process Primitives🔗ℹ

procedure

(spawn thunk)  pid?

  thunk : (-> any)
Spawns a new process running thunk.

procedure

(spawn-link thunk)  pid?

  thunk : (-> any)
Like spawn, but links to the caller.

procedure

(self)  pid?

Returns the current process’s PID.

procedure

(send! pid msg)  any/c

  pid : pid?
  msg : any/c
Sends msg to pid’s mailbox.

procedure

(receive)  any/c

Blocks until a message arrives.

procedure

(receive/timeout timeout-ms)  (or/c any/c #f)

  timeout-ms : exact-nonnegative-integer?
Like receive, but returns #f after timeout.

procedure

(receive-match predicate [timeout-ms])  (or/c any/c #f)

  predicate : (-> any/c boolean?)
  timeout-ms : (or/c #f exact-nonnegative-integer?) = #f
Returns first message matching predicate.

procedure

(pid? v)  boolean?

  v : any/c
Returns #t if v is a PID.

procedure

(pid-alive? pid)  boolean?

  pid : pid?
Returns #t if process is running.

17.4 Registry🔗ℹ

procedure

(register! name pid)  void?

  name : symbol?
  pid : pid?
Registers pid under name.

procedure

(unregister! name)  void?

  name : symbol?
Removes the registration.

procedure

(whereis name)  (or/c pid? #f)

  name : symbol?
Returns the PID registered as name, or #f.

procedure

(registered)  (listof symbol?)

Returns all registered names.

17.5 Links and Monitors🔗ℹ

procedure

(link! pid)  void?

  pid : pid?
Creates a bidirectional link.

procedure

(unlink! pid)  void?

  pid : pid?
Removes the link.

procedure

(monitor! pid)  monitor-ref?

  pid : pid?
Starts monitoring pid.

procedure

(demonitor! ref)  void?

  ref : monitor-ref?
Stops monitoring.

procedure

(set-trap-exit! pid trap?)  void?

  pid : pid?
  trap? : boolean?
When #t, exit signals become messages.

struct

(struct exit-signal (from reason)
    #:extra-constructor-name make-exit-signal)
  from : pid?
  reason : any/c
Received when a linked process exits (if trapping).

struct

(struct down-signal (ref pid reason)
    #:extra-constructor-name make-down-signal)
  ref : monitor-ref?
  pid : pid?
  reason : any/c
Received when a monitored process exits.

17.6 Runtime Mode🔗ℹ

procedure

(runtime-mode mode)  void?

  mode : (or/c 'green 'threaded)
Sets runtime mode. Call before spawning actors.

procedure

(runtime-mode-green?)  boolean?

Returns #t if in green thread mode.

procedure

(scheduler-start!)  void?

Starts the green thread scheduler.

procedure

(scheduler-stop!)  void?

Stops the scheduler.

procedure

(scheduler-run-until-done!)  void?

Runs until all actors complete.

procedure

(scheduler-running?)  boolean?

Returns #t if scheduler is active.

17.7 GenStatem🔗ℹ

procedure

(gen-statem-start impl args [#:name name])  pid?

  impl : statem?
  args : any/c
  name : (or/c symbol? #f) = #f
Starts a state machine.

procedure

(gen-statem-start-link impl    
  args    
  [#:name name])  pid?
  impl : statem?
  args : any/c
  name : (or/c symbol? #f) = #f
Like gen-statem-start, but links to caller.

procedure

(gen-statem-call statem    
  request    
  [#:timeout timeout])  any/c
  statem : (or/c pid? symbol?)
  request : any/c
  timeout : exact-positive-integer? = 5000
Synchronous call to state machine.

procedure

(gen-statem-cast! statem request)  void?

  statem : (or/c pid? symbol?)
  request : any/c
Async message to state machine.

17.8 Distribution🔗ℹ

procedure

(start-epmd! [port])  epmd-server?

  port : exact-positive-integer? = 4369
Starts an EPMD server.

procedure

(stop-epmd! epmd)  void?

  epmd : epmd-server?
Stops EPMD.

procedure

(start-node! name port [#:cookie cookie])  void?

  name : string?
  port : exact-positive-integer?
  cookie : string? = "nocookie"
Starts the local node.

procedure

(stop-node!)  void?

Stops the local node.

procedure

(connect-node! name)  boolean?

  name : string?
Connects to a remote node.

procedure

(nodes)  (listof string?)

Returns connected node names.

procedure

(global-register! name pid)  void?

  name : symbol?
  pid : pid?
Registers globally (all nodes).

procedure

(global-whereis name)  (or/c pid? #f)

  name : symbol?
Looks up a global name.

procedure

(global-send! name msg)  void?

  name : symbol?
  msg : any/c
Sends to a globally registered process.

procedure

(send-remote! pid msg)  void?

  pid : pid?
  msg : any/c
Sends directly to a remote PID.

procedure

(dist-link! pid)  void?

  pid : pid?
Creates a distributed link.

procedure

(dist-monitor! pid)  monitor-ref?

  pid : pid?
Monitors a remote process.

17.9 Hot Reload🔗ℹ

struct

(struct version-info (old-version new-version extra)
    #:extra-constructor-name make-version-info)
  old-version : any/c
  new-version : any/c
  extra : any/c
Version info for code changes.

procedure

(hot-reload! module-path)  void?

  module-path : path-string?
Reloads a module.

procedure

(trigger-code-change! pid version-info)

  (or/c 'ok (cons/c 'error any/c))
  pid : pid?
  version-info : version-info?
Triggers code-change callback.

procedure

(upgrade-process! pid version-info)

  (or/c 'ok (cons/c 'error any/c))
  pid : pid?
  version-info : version-info?
Suspends, triggers code-change, resumes.

procedure

(upgrade-supervisor-tree! sup-pid    
  version-info    
  [#:skip-ids skip-ids])  void?
  sup-pid : pid?
  version-info : version-info?
  skip-ids : (listof any/c) = '()
Upgrades all processes in a tree.

17.10 Testing Utilities🔗ℹ

 (require rakka/testing) package: Rakka

procedure

(wait-until pred [#:timeout timeout-ms])  boolean?

  pred : (-> boolean?)
  timeout-ms : exact-positive-integer? = 1000
Polls pred until true or timeout.

procedure

(check-process-exits pid    
  expected-reason    
  [#:timeout timeout-ms])  boolean?
  pid : pid?
  expected-reason : any/c
  timeout-ms : exact-positive-integer? = 1000
Asserts that pid exits.

syntax

(with-fresh-registry body ...)

Runs body with empty registry.

17.11 Process Groups🔗ℹ

procedure

(pg-join group pid-or-pids)  symbol?

  group : symbol?
  pid-or-pids : (or/c pid? (listof pid?))
Adds pid-or-pids to group. Creates group if needed. A process can join multiple times (refcounted). Accepts a single pid or list of pids. Returns 'ok.

procedure

(pg-leave group pid-or-pids)  symbol?

  group : symbol?
  pid-or-pids : (or/c pid? (listof pid?))
Removes pid-or-pids from group. Must leave same number of times as joined. Accepts a single pid or list of pids. Returns 'ok if at least one pid was removed, 'not_joined otherwise.

procedure

(pg-members group)  (listof pid?)

  group : symbol?
Returns all pids in group, or empty list if group doesn’t exist.

procedure

(pg-which-groups)  (listof symbol?)

Returns all known group names.

procedure

(pg-monitor group)  
pg-monitor-ref? (listof pid?)
  group : symbol?
Monitors group for membership changes. Returns a reference and current members. The caller will receive messages of the form:

procedure

(pg-demonitor ref)  symbol?

  ref : pg-monitor-ref?
Stops monitoring. Returns 'ok.

procedure

(pg-monitor-ref? v)  boolean?

  v : any/c
Returns #t if v is a process group monitor reference.

17.12 PubSub🔗ℹ

procedure

(pubsub-start name)  any/c

  name : symbol?
Starts a named pubsub system. Raises error if name already exists.

procedure

(pubsub-stop name)  symbol?

  name : symbol?
Stops a pubsub system. Raises error if name doesn’t exist.

procedure

(pubsub-subscribe pubsub topic)  symbol?

  pubsub : symbol?
  topic : string?
Subscribes the calling process to topic.

procedure

(pubsub-unsubscribe pubsub topic)  symbol?

  pubsub : symbol?
  topic : string?
Unsubscribes the calling process from topic.

procedure

(pubsub-broadcast pubsub topic message)  symbol?

  pubsub : symbol?
  topic : string?
  message : any/c
Sends message to all subscribers of topic.

procedure

(pubsub-broadcast-from pubsub topic message)  symbol?

  pubsub : symbol?
  topic : string?
  message : any/c
Like pubsub-broadcast, but excludes the calling process.

procedure

(pubsub-subscribers pubsub topic)  (listof pid?)

  pubsub : symbol?
  topic : string?
Returns all subscribers to topic. For debugging/monitoring.

17.13 Tables🔗ℹ

procedure

(table-new name #:type type [#:named named?])  table?

  name : symbol?
  type : (or/c 'set)
  named? : boolean? = #f
Creates a new table owned by the calling actor. The name is for identification (and registration if named? is #t). Currently only 'set type is supported. When named? is #t, the table is registered globally and can be looked up by name.

procedure

(table? v)  boolean?

  v : any/c
Returns #t if v is a table.

procedure

(table-alive? t)  boolean?

  t : table?
Returns #t if the table is still active (owner hasn’t died).

procedure

(table-whereis name)  (or/c table? #f)

  name : symbol?
Looks up a named table. Returns #f if no table is registered under name.

procedure

(table-insert! t key value)  void?

  t : (or/c table? symbol?)
  key : any/c
  value : any/c
Inserts key-value pair. The value is copied into the table. Accepts table reference or name. Raises error if value is not place-message-allowed?.

procedure

(table-lookup t key [default])  any/c

  t : (or/c table? symbol?)
  key : any/c
  default : any/c = #f
Returns the value for key, or default if not found. The returned value is a copy—mutations don’t affect the table.

procedure

(table-member? t key)  boolean?

  t : (or/c table? symbol?)
  key : any/c
Returns #t if key exists in the table.

procedure

(table-remove! t key)  void?

  t : (or/c table? symbol?)
  key : any/c
Removes key from the table. No error if key doesn’t exist.

procedure

(table-insert-new! t key value)  boolean?

  t : (or/c table? symbol?)
  key : any/c
  value : any/c
Inserts only if key doesn’t exist. Returns #t if inserted, #f if key already existed.

procedure

(table-update! t key proc default)  any/c

  t : (or/c table? symbol?)
  key : any/c
  proc : (-> any/c any/c)
  default : any/c
Atomic read-modify-write. Calls (proc old-value) where old-value is the existing value (or default if key doesn’t exist). Returns the new value.

procedure

(table-delete! t)  void?

  t : (or/c table? symbol?)
Destroys the table. Unregisters if named. All future operations will error.

procedure

(table-keys t)  (listof any/c)

  t : (or/c table? symbol?)
Returns all keys in the table.

procedure

(table-count t)  exact-nonnegative-integer?

  t : (or/c table? symbol?)
Returns the number of entries in the table.

procedure

(table-to-list t)  (listof (cons/c any/c any/c))

  t : (or/c table? symbol?)
Returns all entries as an association list. Values are copies.

procedure

(table-clear! t)  void?

  t : (or/c table? symbol?)
Removes all entries from the table (but keeps the table alive).

procedure

(table-info t)  hash?

  t : (or/c table? symbol?)
Returns metadata: 'name, 'type, 'size, 'named?, 'owner.

18 License🔗ℹ

Rakka is released under the MIT License.

MIT License

 

Copyright (c) 2024 Aldric Giacomoni

 

Permission is hereby granted, free of charge, to any person obtaining a copy

of this software and associated documentation files (the "Software"), to deal

in the Software without restriction, including without limitation the rights

to use, copy, modify, merge, publish, distribute, sublicense, and/or sell

copies of the Software, and to permit persons to whom the Software is

furnished to do so, subject to the following conditions:

 

The above copyright notice and this permission notice shall be included in all

copies or substantial portions of the Software.

 

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR

IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,

FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE

AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER

LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,

OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE

SOFTWARE.