Rakka:   Actor Model for Racket
1 Quick Start
1.1 Your First Actor
2 Understanding Actors
2.1 The Mental Model
2.2 Spawning Processes
2.3 Sending and Receiving Messages
2.4 The Actor Loop Pattern
3 Green Threads vs OS Threads
3.1 When to Use Each
3.2 Switching Modes
3.3 Performance Characteristics
3.4 How It Works
4 Gen  Server:   Abstracting the Pattern
4.1 Why Gen  Server?
4.2 Implementing a Gen  Server
4.3 The Callbacks
4.4 Using a Gen  Server
5 Links and Monitors:   Handling Failure
5.1 Links:   Shared Fate
5.2 Monitors:   One-Way Watching
6 Supervisors:   Automatic Recovery
6.1 Why Supervisors?
6.2 Restart Strategies
6.3 Defining Children
6.4 Starting a Supervisor
7 Applications:   Putting It Together
8 Process Registry
9 API Reference
9.1 Process Primitives
spawn
spawn-link
self
send!
receive
receive/  timeout
receive-match
pid?
pid-alive?
9.2 Runtime Mode
runtime-mode
runtime-mode-green?
scheduler-start!
scheduler-stop!
scheduler-run-until-done!
scheduler-running?
9.3 Registry
register!
unregister!
whereis
registered
9.4 Links and Monitors
link!
unlink!
monitor!
demonitor!
set-trap-exit!
exit-signal
down-signal
9.5 Gen  Server
gen-server-start
gen-server-start-link
gen-server-call
gen-server-cast!
gen-server-stop
ok
stop-init
reply
noreply
stop
9.6 Supervisor
supervisor-start-link
child-spec*
strategy?
9.7 Testing Utilities
wait-until
check-process-exits
with-fresh-registry
10 #lang rakka:   Actor-Friendly Racket
10.1 Why #lang rakka?
10.2 Using #lang rakka
10.3 Manual Reduction Control
10.4 Anti-Pattern Warnings
11 Distribution:   Actors Across Machines
11.1 Starting a Distributed Node
11.2 Connecting Nodes
11.3 Global Registry
11.4 Remote Spawning
11.5 Distributed Links and Monitors
11.6 Distribution API Reference
start-epmd!
stop-epmd!
start-node!
stop-node!
connect-node!
nodes
global-register!
global-whereis
global-send!
send-remote!
dist-link!
dist-monitor!
12 Hot Code Reload
12.1 Basic Reload
12.2 State Migration with code-change
12.3 Triggering Code Change
12.4 Tree-Wide Upgrades
12.5 Rollback on Failure
12.6 Hot Reload API Reference
version-info
hot-reload!
trigger-code-change!
upgrade-process!
upgrade-supervisor-tree!
13 Gen  Statem:   Finite State Machines
13.1 Implementing a State Machine
13.2 Using a State Machine
13.3 Gen  Statem Callbacks
14 Operational Features
14.1 Logging
14.2 Metrics
14.3 Graceful Shutdown
14.4 Process Introspection
15 License
9.0.0.6

Rakka: Actor Model for Racket🔗ℹ

Aldric Giacomoni

 (require "main.rkt")

Rakka brings Erlang/OTP-style actors to Racket 9. Build concurrent systems where isolated processes communicate through message passing, crashes are contained, and supervision trees automatically recover from failures.

For you if: You’re building concurrent Racket applications and want fault-tolerant, message-passing architecture instead of shared-state threading.

    1 Quick Start

      1.1 Your First Actor

    2 Understanding Actors

      2.1 The Mental Model

      2.2 Spawning Processes

      2.3 Sending and Receiving Messages

      2.4 The Actor Loop Pattern

    3 Green Threads vs OS Threads

      3.1 When to Use Each

      3.2 Switching Modes

      3.3 Performance Characteristics

      3.4 How It Works

    4 GenServer: Abstracting the Pattern

      4.1 Why GenServer?

      4.2 Implementing a GenServer

      4.3 The Callbacks

      4.4 Using a GenServer

    5 Links and Monitors: Handling Failure

      5.1 Links: Shared Fate

      5.2 Monitors: One-Way Watching

    6 Supervisors: Automatic Recovery

      6.1 Why Supervisors?

      6.2 Restart Strategies

      6.3 Defining Children

      6.4 Starting a Supervisor

    7 Applications: Putting It Together

    8 Process Registry

    9 API Reference

      9.1 Process Primitives

      9.2 Runtime Mode

      9.3 Registry

      9.4 Links and Monitors

      9.5 GenServer

      9.6 Supervisor

      9.7 Testing Utilities

    10 #lang rakka: Actor-Friendly Racket

      10.1 Why #lang rakka?

      10.2 Using #lang rakka

      10.3 Manual Reduction Control

      10.4 Anti-Pattern Warnings

    11 Distribution: Actors Across Machines

      11.1 Starting a Distributed Node

      11.2 Connecting Nodes

      11.3 Global Registry

      11.4 Remote Spawning

      11.5 Distributed Links and Monitors

      11.6 Distribution API Reference

    12 Hot Code Reload

      12.1 Basic Reload

      12.2 State Migration with code-change

      12.3 Triggering Code Change

      12.4 Tree-Wide Upgrades

      12.5 Rollback on Failure

      12.6 Hot Reload API Reference

    13 GenStatem: Finite State Machines

      13.1 Implementing a State Machine

      13.2 Using a State Machine

      13.3 GenStatem Callbacks

    14 Operational Features

      14.1 Logging

      14.2 Metrics

      14.3 Graceful Shutdown

      14.4 Process Introspection

    15 License

1 Quick Start🔗ℹ

Get a working actor system in 30 seconds.

1.1 Your First Actor🔗ℹ

#lang racket/base
(require rakka racket/match)
 
;; Define an actor that echoes messages back
(define (echo-actor)
  (let loop ()
    (define msg (receive))
    (match msg
      [(list 'echo payload from)
       (send! from (list 'reply payload))
       (loop)]
      ['stop (void)]
      [_ (loop)])))
 
;; Run it
(define (main)
  (define echo (spawn echo-actor))
  (send! echo (list 'echo "hello!" (self)))
 
  (define reply (receive))
  (printf "Got: ~a\n" reply)
 
  (send! echo 'stop))
 
;; Must run inside a process
(spawn main)

Run this. You’ll see Got: (reply hello!). You just:
  • Spawned an isolated process

  • Sent it a message

  • Received a reply

2 Understanding Actors🔗ℹ

2.1 The Mental Model🔗ℹ

Think of actors like people in separate rooms, communicating by passing notes.

  • Isolation: Each actor has its own memory. No shared state.

  • Messages: The only way to communicate. Drop a note in their mailbox.

  • Mailboxes: Each actor has one. Messages queue up, processed one at a time.

  • Failures stay local: If one actor crashes, others keep running.

This differs from threads sharing memory:
  • No locks needed—no shared state to protect

  • No race conditions—messages processed sequentially

  • Crashes contained—one bad actor doesn’t corrupt others

2.2 Spawning Processes🔗ℹ

spawn creates a new actor running your function:

(define pid (spawn (lambda ()
                     (printf "I'm alive!\n")
                     (sleep 1)
                     (printf "Goodbye!\n"))))
 
(printf "Spawned: ~a\n" pid)
(printf "Alive? ~a\n" (pid-alive? pid))
(sleep 2)
(printf "Still alive? ~a\n" (pid-alive? pid))

The spawned process runs in parallel. pid-alive? checks if it’s still running.

2.3 Sending and Receiving Messages🔗ℹ

send! drops a message in an actor’s mailbox. It returns immediately—fire and forget.

(send! some-pid 'hello)
(send! some-pid (list 'data 1 2 3))
(send! some-pid (hash 'key "value"))

receive blocks until a message arrives:

(define msg (receive))  ; blocks

receive/timeout adds a deadline (in milliseconds):

(define msg (receive/timeout 1000))  ; wait up to 1 second
(if msg
    (printf "Got: ~a\n" msg)
    (printf "Timed out\n"))

2.4 The Actor Loop Pattern🔗ℹ

Most actors follow this pattern—loop forever, handle messages:

(define (counter-actor initial)
  (let loop ([count initial])
    (define msg (receive))
    (match msg
      ['increment
       (loop (add1 count))]
      [(list 'get from)
       (send! from count)
       (loop count)]
      ['stop
       (printf "Final count: ~a\n" count)]
      [_
       (loop count)])))  ; ignore unknown messages

State lives in the loop arguments. Each iteration can pass new state to the next.

3 Green Threads vs OS Threads🔗ℹ

Rakka 0.7.0 introduces green threadslightweight cooperative scheduling that runs thousands of actors efficiently on a single OS thread.

3.1 When to Use Each🔗ℹ

Mode

  

Use When

  

Trade-off

Green (default)

  

Many actors, I/O-bound, message-heavy

  

Fast spawning, no parallelism

Threaded

  

CPU-bound work, true parallelism needed

  

Slower spawning, real parallel

3.2 Switching Modes🔗ℹ

Use runtime-mode to select the execution model before spawning actors:

#lang racket/base
(require rakka)
 
;; Green mode (default) - lightweight, cooperative
(runtime-mode 'green)
(scheduler-start!)
 
(for ([i 10000])
  (spawn (lambda () (receive))))  ; 10,000 actors, no problem
 
(scheduler-run-until-done!)
(scheduler-stop!)

For CPU-bound parallel work:

;; Threaded mode - real OS threads
(runtime-mode 'threaded)
 
(spawn (lambda ()
  ;; CPU-bound work runs in parallel
  (heavy-computation)))

3.3 Performance Characteristics🔗ℹ

Green threads excel at actor-heavy workloads:

Operation

  

Green

  

Threaded

  

Green Advantage

Spawn 1000 actors

  

2ms

  

45ms

  

22× faster

50k messages

  

32ms

  

5,300ms

  

165× faster

Use green mode unless you need CPU parallelism.

3.4 How It Works🔗ℹ

Green threads use continuation-based cooperative scheduling:

  • No OS thread per actor: Thousands of actors share one thread

  • Cooperative yielding: Actors yield at message receive points

  • Two-list mailbox queue: O(1) amortized message operations

  • Timeout support: Actors can wait with deadlines

The scheduler maintains a ready queue of actors. When an actor calls receive with no messages, it suspends (saving its continuation) and the scheduler runs the next ready actor. When a message arrives, the waiting actor is added back to the ready queue.

4 GenServer: Abstracting the Pattern🔗ℹ

Writing message loops by hand gets repetitive. GenServer abstracts the pattern.

4.1 Why GenServer?🔗ℹ

Raw actors require you to:
  • Write the receive loop

  • Handle request/reply correlation

  • Manage timeouts

  • Handle shutdown

GenServer provides this infrastructure. You just implement callbacks.

4.2 Implementing a GenServer🔗ℹ

Define a struct that implements gen:server:

#lang racket/base
(require rakka racket/match)
 
(struct counter-server ()
  #:methods gen:server
  [(define (init self args)
     (ok args))  ; args becomes initial state
 
   (define (handle-call self msg state from)
     (match msg
       ['get (reply state state)]
       ['increment (reply 'ok (add1 state))]))
 
   (define (handle-cast self msg state)
     (match msg
       ['reset (noreply 0)]
       [_ (noreply state)]))
 
   (define (handle-info self msg state)
     (noreply state))
 
   (define (terminate self reason state)
     (void))])

4.3 The Callbacks🔗ℹ

Callback

  

When Called

  

Returns

init

  

Server starts

  

(ok state) or (stop-init reason)

handle-call

  

Synchronous request

  

(reply value new-state)

handle-cast

  

Async notification

  

(noreply new-state)

handle-info

  

Other messages

  

(noreply new-state)

terminate

  

Server stopping

  

(void)

4.4 Using a GenServer🔗ℹ

;; Start the server
(define pid (gen-server-start (counter-server) 0))
 
;; Synchronous call (blocks for reply)
(printf "Count: ~a\n" (gen-server-call pid 'get))      ; => 0
(printf "Inc: ~a\n" (gen-server-call pid 'increment))  ; => ok
(printf "Count: ~a\n" (gen-server-call pid 'get))      ; => 1
 
;; Async cast (fire and forget)
(gen-server-cast! pid 'reset)
(sleep 0.01)  ; let it process
(printf "Count: ~a\n" (gen-server-call pid 'get))      ; => 0
 
;; Stop
(gen-server-stop pid)

5 Links and Monitors: Handling Failure🔗ℹ

Actors crash. Links and monitors let you respond.

5.1 Links: Shared Fate🔗ℹ

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

;; Spawn and link in one step
(define pid (spawn-link (lambda ()
                          (sleep 1)
                          (error "boom!"))))
 
;; Or link explicitly
(define pid2 (spawn some-actor))
(link! pid2)

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

(set-trap-exit! (self) #t)
 
;; Now exit signals arrive as messages
(define msg (receive))
(match msg
  [(exit-signal from reason)
   (printf "~a died: ~a\n" from reason)])

5.2 Monitors: One-Way Watching🔗ℹ

Monitors watch without shared fate. The watcher gets notified, but doesn’t die.

(define ref (monitor! some-pid))
 
;; Later, if some-pid dies:
(define msg (receive))
(match msg
  [(down-signal ref pid reason)
   (printf "~a died: ~a\n" pid reason)])

Use demonitor! to stop watching.

6 Supervisors: Automatic Recovery🔗ℹ

Supervisors watch child processes and restart them on failure.

6.1 Why Supervisors?🔗ℹ

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

Supervisors implement this by:
  • Starting child processes

  • Monitoring them for failures

  • Restarting them according to a strategy

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 Defining Children🔗ℹ

Use child-spec* to define how to start each child:

(child-spec* #:id 'worker
             #:start (lambda () (gen-server-start-link (my-worker) '()))
             #:restart 'permanent)

Restart types:
  • 'permanent always restart

  • 'temporary never restart

  • 'transient restart only on abnormal exit

6.4 Starting a Supervisor🔗ℹ

(define sup-pid
  (supervisor-start-link 'one-for-one  ; strategy
                         3              ; max 3 restarts
                         5              ; in 5 seconds
                         (list
                           (child-spec* #:id 'worker1
                                        #:start worker1-start
                                        #:restart 'permanent)
                           (child-spec* #:id 'worker2
                                        #:start worker2-start
                                        #:restart 'permanent))))

If a child crashes more than 3 times in 5 seconds, the supervisor gives up and terminates (which may trigger its own supervisor to restart it).

7 Applications: Putting It Together🔗ℹ

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

(struct my-app ()
  #:methods gen:application
  [(define (start self args)
     ;; Start your supervision tree here
     (define sup (supervisor-start-link ...))
     (app-ok sup))
 
   (define (stop self state)
     (void))])
 
;; Start the application
(application-start (my-app) '())

8 Process Registry🔗ℹ

Name processes for easy lookup:

(register! 'cache cache-pid)
 
;; Later, from anywhere:
(define cache (whereis 'cache))
(gen-server-call cache 'get-stats)

whereis returns #f if not registered.

9 API Reference🔗ℹ

9.1 Process Primitives🔗ℹ

procedure

(spawn thunk)  pid?

  thunk : (-> any)
Spawns a new process running thunk. Returns immediately with the PID.

procedure

(spawn-link thunk)  pid?

  thunk : (-> any)
Like spawn, but links the new process to the current one.

procedure

(self)  pid?

Returns the PID of the current process. Error if not in a process.

procedure

(send! pid msg)  any/c

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

procedure

(receive)  any/c

Blocks until a message is available, then returns it.

procedure

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

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

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 the first message matching predicate. Non-matching messages stay in the mailbox.

procedure

(pid? v)  boolean?

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

procedure

(pid-alive? pid)  boolean?

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

9.2 Runtime Mode🔗ℹ

procedure

(runtime-mode mode)  void?

  mode : (or/c 'green 'threaded)
Sets the runtime mode. 'green uses lightweight cooperative scheduling; 'threaded uses OS threads. Must be called before spawning actors.

procedure

(runtime-mode-green?)  boolean?

Returns #t if currently in green thread mode.

procedure

(scheduler-start!)  void?

Starts the green thread scheduler. Required before spawning actors in green mode.

procedure

(scheduler-stop!)  void?

Stops the green thread scheduler.

procedure

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

Runs the scheduler until all actors have completed or are waiting indefinitely.

procedure

(scheduler-running?)  boolean?

Returns #t if the scheduler is currently active.

9.3 Registry🔗ℹ

procedure

(register! name pid)  void?

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

procedure

(unregister! name)  void?

  name : symbol?
Removes the registration for name.

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.

9.4 Links and Monitors🔗ℹ

procedure

(link! pid)  void?

  pid : pid?
Creates a bidirectional link with pid.

procedure

(unlink! pid)  void?

  pid : pid?
Removes the link with pid.

procedure

(monitor! pid)  monitor-ref?

  pid : pid?
Starts monitoring pid. Returns a reference for demonitor!.

procedure

(demonitor! ref)  void?

  ref : monitor-ref?
Stops monitoring.

procedure

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

  pid : pid?
  trap? : boolean?
When trap? is #t, exit signals become messages instead of killing the process.

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 exits).

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.

9.5 GenServer🔗ℹ

procedure

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

  impl : server?
  args : any/c
  name : (or/c symbol? #f) = #f
Starts a GenServer with impl and args. Optionally registers it 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 a reply. Raises an error on timeout.

procedure

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

  server : (or/c pid? symbol?)
  request : any/c
Sends request asynchronously. Does not wait for a reply.

procedure

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

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

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.

9.6 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.

9.7 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 it returns #t 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 within the timeout.

syntax

(with-fresh-registry body ...)

Runs body with an empty process registry, cleaning up after.

10 #lang rakka: Actor-Friendly Racket🔗ℹ

For long-running computations, use #lang rakka instead of #lang racket. It automatically yields control at function calls, preventing any single actor from monopolizing the scheduler.

10.1 Why #lang rakka?🔗ℹ

In green thread mode, actors cooperatively yield at receive points. But what if an actor computes for a long time without receiving? It starves other actors.

#lang rakka solves this by counting "reductions" (function calls). After 4000 reductions, it automatically yields. This is how Erlang achieves fair scheduling.

10.2 Using #lang rakka🔗ℹ

#lang rakka  ; <-- Just change this line
 
(require rakka)
 
;; Every function call now includes a yield point
(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 () (printf "I'm not blocked!~n")))

10.3 Manual Reduction Control🔗ℹ

#lang rakka
 
(set-reduction-limit! 1000)  ; Yield more often
(reset-reductions!)          ; Reset counter
(current-reductions)         ; Check current count

10.4 Anti-Pattern Warnings🔗ℹ

#lang rakka warns about patterns that break actor isolation:

#lang rakka
 
(define x 1)
(set! x 2)  ; Warning: mutation may break actor isolation
 
(thread (lambda () ...))  ; Warning: raw threads bypass actor scheduler

Disable with (disable-rakka-warnings!).

11 Distribution: Actors Across Machines🔗ℹ

Rakka supports Erlang-style distribution with EPMD (Erlang Port Mapper Daemon) protocol compatibility.

11.1 Starting a Distributed Node🔗ℹ

#lang racket
(require rakka)
 
;; Start EPMD (once per machine, typically port 4369)
(define epmd (start-epmd! 4369))
 
;; Start this node
(start-node! "mynode@localhost" 9001
             #:cookie "secret-cookie")

The cookie must match between nodes that want to communicate.

11.2 Connecting Nodes🔗ℹ

;; On node2
(start-node! "node2@localhost" 9002
             #:cookie "secret-cookie")
 
;; Connect to node1
(connect-node! "mynode@localhost")
 
;; See connected nodes
(nodes)  ; => '("mynode@localhost")

11.3 Global Registry🔗ℹ

Register processes visible across all connected nodes:

;; Register globally
(global-register! 'calculator my-pid)
 
;; From any connected node
(global-send! 'calculator '(add 1 2))
 
;; Lookup
(define calc-pid (global-whereis 'calculator))

11.4 Remote Spawning🔗ℹ

Spawn actors on remote nodes:

;; On the 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 '(initial args)))

11.5 Distributed Links and Monitors🔗ℹ

Links and monitors work across nodes:

;; Link to remote process
(dist-link! remote-pid)
 
;; Monitor remote process
(define ref (dist-monitor! remote-pid))

11.6 Distribution API Reference🔗ℹ

procedure

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

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

procedure

(stop-epmd! epmd)  void?

  epmd : epmd-server?
Stops the EPMD server.

procedure

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

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

procedure

(stop-node!)  void?

Stops the local node and disconnects from all remote nodes.

procedure

(connect-node! name)  boolean?

  name : string?
Connects to a remote node. Returns #t on success.

procedure

(nodes)  (listof string?)

Returns a list of connected node names.

procedure

(global-register! name pid)  void?

  name : symbol?
  pid : pid?
Registers pid under name in the global registry.

procedure

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

  name : symbol?
Looks up a globally registered name.

procedure

(global-send! name msg)  void?

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

procedure

(send-remote! pid msg)  void?

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

procedure

(dist-link! pid)  void?

  pid : pid?
Creates a distributed link to a remote process.

procedure

(dist-monitor! pid)  monitor-ref?

  pid : pid?
Monitors a remote process.

12 Hot Code Reload🔗ℹ

Update running code without stopping your system. Rakka supports Erlang-style code upgrades with state migration.

12.1 Basic Reload🔗ℹ

#lang racket
(require rakka)
 
;; Reload a module (re-evaluates the file)
(hot-reload! "my-server.rkt")

12.2 State Migration with code-change🔗ℹ

When code changes, your GenServer’s code-change callback transforms old state to new:

(struct my-server (data version)
  #:methods gen:server
  [;; ... other callbacks ...
 
   (define (code-change self version-info state)
     ;; version-info contains old-version, new-version, extra
     (match (version-info-new-version version-info)
       ["2.0"
        ;; Migrate state from v1 to v2
        (ok (struct-copy my-server state
                         [version "2.0"]
                         [data (upgrade-data (my-server-data state))]))]
       [_
        (ok state)]))])

12.3 Triggering Code Change🔗ℹ

;; Trigger code-change on a specific process
(trigger-code-change! pid (version-info "1.0" "2.0" #f))
 
;; Upgrade with suspend/resume (safer)
(upgrade-process! pid (version-info "1.0" "2.0" #f))

12.4 Tree-Wide Upgrades🔗ℹ

Upgrade an entire supervision tree safely (bottom-up order):

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

Children are upgraded before their supervisor, ensuring consistent state.

12.5 Rollback on Failure🔗ℹ

(hot-reload-with-rollback!
 "my-server.rkt"
 #:validate (lambda () (run-health-checks))
 #:timeout 5000)

If validation fails, the old code is restored.

12.6 Hot Reload API Reference🔗ℹ

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
Contains version information passed to code-change callbacks.

procedure

(hot-reload! module-path)  void?

  module-path : path-string?
Reloads a module using dynamic-rerequire.

procedure

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

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

procedure

(upgrade-process! pid version-info)

  (or/c 'ok (cons/c 'error any/c))
  pid : pid?
  version-info : version-info?
Suspends the process, triggers code-change, then 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 supervision tree, bottom-up.

13 GenStatem: Finite State Machines🔗ℹ

For state-dependent behavior, use GenStatem (generic state machine).

13.1 Implementing a State Machine🔗ℹ

(struct traffic-light ()
  #:methods gen:statem
  [(define (statem-init self args)
     (statem-ok 'red 0))  ; initial state = 'red, data = 0
 
   (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 'get-cycle-count)
        (keep-state-and-data (statem-reply event data))]))
 
   (define (statem-terminate self reason state data)
     (void))])

13.2 Using a State Machine🔗ℹ

(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 'get-cycle-count) ; => 1

13.3 GenStatem Callbacks🔗ℹ

Callback

  

Returns

statem-init

  

(statem-ok initial-state initial-data)

callback-mode

  

'handle-event

handle-event

  

next-state, keep-state, or keep-state-and-data

statem-terminate

  

(void)

14 Operational Features🔗ℹ

14.1 Logging🔗ℹ

Rakka provides structured logging with per-process log levels:

;; Set global log level
(current-rakka-log-level 'debug)
 
;; Set per-process log level
(set-process-log-level! some-pid 'error)
 
;; Log messages
(rakka-log-info "Starting server on port ~a" port)
(rakka-log-error "Connection failed: ~a" reason)

Lifecycle events are logged automatically: SPAWN, EXIT, CRASH, RESTART.

14.2 Metrics🔗ℹ

Built-in metrics for monitoring:

;; Read built-in metrics
(counter-value metrics-process-spawns)
(counter-value metrics-messages-sent)
(gauge-value metrics-active-processes)
 
;; Create custom metrics
(define requests (make-counter 'http-requests))
(counter-inc! requests)
 
(define connections (make-gauge 'active-connections))
(gauge-inc! connections)
(gauge-dec! connections)
 
(define latency (make-histogram 'request-latency))
(histogram-observe! latency 42.5)

14.3 Graceful Shutdown🔗ℹ

Register shutdown hooks for clean termination:

(on-shutdown
 (lambda (ctx)
   (printf "Cleaning up...~n")
   (close-database-connections))
 #:priority 10   ; higher = runs first
 #:name 'db-cleanup)
 
;; Trigger graceful shutdown
(graceful-shutdown #:timeout 30000 #:reason 'maintenance)

14.4 Process Introspection🔗ℹ

Inspect and control running processes:

;; Get current state of a GenServer
(sys-get-state some-pid)
 
;; Suspend/resume processing
(sys-suspend some-pid)
(sys-resume some-pid)
 
;; Enable tracing
(sys-trace some-pid #t)

15 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.