Rakka: Actor Model for Racket
| (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
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)
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 |
| |
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—
;; 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—
;; 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 sent with plain send!
Exit signals from linked processes (if trapping exits)
Monitor down signals
Timer messages
2.4 Priority Message Handling
Rakka processes system messages (exit signals, down signals, timeout signals) with priority over regular user messages. This means:
Supervisor signals are never starved by message floods
Exit signals from linked processes are processed promptly
Monitor notifications arrive reliably even under load
This matches Erlang’s behavior and is transparent to your code—
3 Tutorial: Key-Value Store
Let’s build a practical GenServer—
#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)
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
6.5 Dynamic Children with simple-one-for-one
The 'simple-one-for-one strategy is for dynamic worker pools where all children use the same specification. Unlike other strategies:
No children at startup: The supervisor starts empty
Single template spec: The child-spec list must have exactly one entry
Dynamic creation: Children are added via supervisor-start-child
Arguments passed through: The start-child argument goes to the start function
;; Worker that receives its name at startup (define (make-worker name) (lambda () (spawn-link (lambda () (printf "Worker ~a started\n" name) (let loop () (receive) (loop)))))) ;; Start supervisor with template spec (define sup (supervisor-start-link 'simple-one-for-one ; strategy 3 5 ; max restarts (list (child-spec* #:id 'worker-template #:start (lambda (name) ((make-worker name))) #:restart 'transient)))) ;; Dynamically add workers (supervisor-start-child sup '(worker-1)) (supervisor-start-child sup '(worker-2)) (supervisor-start-child sup '(worker-3)) ;; Check children (supervisor-count-children sup) ;; => ((specs . 1) (active . 3) (supervisors . 0) (workers . 3))
6.6 Managing Children Dynamically
Beyond 'simple-one-for-one, any supervisor can manage children dynamically:
;; Terminate a running child (by id) (supervisor-terminate-child sup 'my-worker) ;; Restart a terminated child (supervisor-restart-child sup 'my-worker) ; must be terminated first ;; Permanently remove a child spec (must terminate first) (supervisor-terminate-child sup 'my-worker) (supervisor-delete-child sup 'my-worker)
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) '())
A supervised tree of processes
Automatic restart on failures
Clean shutdown when the app stops
8 Process Dictionary
Each process has a private mutable dictionary for storing per-process state. Like Erlang’s process dictionary, this provides fast key-value storage without message passing.
When to use: Caching, counters, or state that doesn’t belong in your
GenServer/GenStatem state. Use sparingly—
(spawn (lambda () ;; Store a value (put 'counter 0) ;; Get a value (returns 'undefined if not found) (define count (get 'counter)) ; => 0 ;; Update and get old value (define old (put 'counter (add1 count))) ; => 0 ;; List all keys (get-keys) ; => '(counter) ;; Remove a key (erase 'counter) ; => 1 (returns old value) (get 'counter))) ; => 'undefined
Note: The process dictionary is local to each process. It cannot be accessed from other processes.
9 Under the Hood: Raw Actors
GenServer is built on lower-level primitives. You usually don’t need these, but understanding them helps.
9.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)
9.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)])))
Raw: You write the loop, handle replies manually
GenServer: You implement callbacks, infrastructure handles the rest
9.3 When to Use Raw Actors
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.
10 Green Threads and Scheduling
Rakka uses green threads by default—
10.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.
10.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)))
10.3 Performance Comparison
Operation |
| Green |
| Threaded |
Spawn 1000 actors |
| 2ms |
| 45ms |
50k messages |
| 32ms |
| 5,300ms |
Use green mode unless you need CPU parallelism.
11 Runtime Lifecycle
The Rakka runtime manages schedulers that execute your actors. Before spawning any processes, you must start the runtime.
11.1 Starting and Stopping
#lang racket/base (require rakka) ;; Start the runtime with default number of schedulers (one per CPU core) (start-runtime!) ;; Spawn actors and do work... (define pid (spawn (lambda () (displayln "Hello!")))) ;; When done, clean up (stop-runtime!)
By default, start-runtime! creates one scheduler per CPU core. You can override this:
;; Start with exactly 4 schedulers (start-runtime! #:schedulers 4)
11.2 Convenient Wrapper
For tests and scripts, with-runtime handles the lifecycle for you:
(with-runtime #:schedulers 2 (define pid (spawn (lambda () (displayln "Running!")))) (sleep 0.1)) ;; Runtime automatically stops here
11.3 Checking Runtime State
(runtime-started?) ; => #t if runtime is running (scheduler-count) ; => number of active schedulers
12 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
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.
12.1 Postponing Events
Sometimes an event arrives in a state that can’t handle it yet. Instead of dropping or erroring, use postpone to queue it for later:
(define (handle-event self event-type event state data) (match* (state (call-event-request event)) ;; In 'waiting state, postpone 'process-me until ready [('waiting 'process-me) (keep-state-and-data (list postpone))] ;; Handle 'become-ready to transition [('waiting 'become-ready) (next-state 'ready data '())] ;; In 'ready state, actually process it [('ready 'process-me) (keep-state-and-data (list (statem-reply event 'processed)))]))
The current event is saved in a queue
When the state machine receives its next event, postponed events are tried first
Postponed events stay queued until the machine enters a state that handles them
This is useful for connection state machines, protocol handlers, and any situation where events may arrive out of order.
13 Distribution: Actors Across Machines
Rakka supports Erlang-style distribution with EPMD protocol compatibility.
13.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.
13.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))
13.3 Global Registry
;; Register globally (visible on all nodes) (global-register! 'calculator my-pid) ;; Look up from any node (define calc (global-whereis 'calculator))
13.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)))
14 Hot Code Reload
Update running code without stopping your system.
14.1 Basic Reload
(hot-reload! "my-server.rkt")
14.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))
14.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))
15 Operational Features
15.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.
15.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)
15.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
15.4 Graceful Shutdown
(on-shutdown (lambda (ctx) (close-connections)) #:priority 10 #:name 'cleanup) (graceful-shutdown #:timeout 30000)
16 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)—
Note: This is local-only. Erlang’s pg module synchronizes across cluster nodes; ours does not yet.
17 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.
18 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.
18.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.
18.2 Why Tables Exist
Rakka actors don’t share memory—
Caches: Multiple actors checking the same cached data
Counters: Incrementing a shared metric
Lookup tables: A routing table that many actors consult
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—
18.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—
18.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.
18.5 Ownership and Lifecycle
Every table has an owner—
(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.
18.6 Common Patterns
(table-update! counters 'page-views add1 0) ; returns new value
(unless (table-member? cache key) (table-insert! cache key (expensive-computation))) (table-lookup cache key)
(when (table-insert-new! cache key computed-value) (log-info "Cache miss for ~a" key))
(for ([entry (table-to-list my-table)]) (printf "~a: ~a\n" (car entry) (cdr entry)))
19 Process Introspection
Rakka provides functions to inspect running processes, similar to Erlang’s erlang:processes/0 and erlang:process_info/2.
procedure
procedure
(process-info pid-or-name [key]) → (or/c hash? any/c #f)
pid-or-name : (or/c pid? symbol?) key : symbol? = #f
'pid — the process identifier
'registered-name — registered name or #f
'status — 'running, 'waiting, or 'suspended
'message-queue-len — number of pending messages
'links — list of linked process identifiers
'monitors — list of active monitor references
'monitored-by — list of processes monitoring this one
'trap-exit — whether exit signals are trapped
If key is provided, returns only that specific value.
Returns #f if the process is dead or does not exist.
20 API Reference
20.1 GenServer
procedure
(gen-server-start impl args [#:name name]) → process?
impl : server? args : any/c name : (or/c symbol? #f) = #f
procedure
(gen-server-start-link impl args [ #:name name]) → process? impl : server? args : any/c name : (or/c symbol? #f) = #f
procedure
(gen-server-call server request [ #:timeout timeout]) → any/c server : (or/c process? symbol?) request : any/c timeout : exact-positive-integer? = 5000
procedure
(gen-server-stop server [reason]) → void?
server : (or/c process? symbol?) reason : any/c = 'normal
struct
(struct reply (value new-state) #:extra-constructor-name make-reply) value : any/c new-state : any/c
struct
(struct stop (reason reply-value new-state) #:extra-constructor-name make-stop) reason : any/c reply-value : any/c new-state : any/c
20.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?)
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
procedure
→ (listof (list/c symbol? (or/c pid? 'undefined) (or/c 'worker 'supervisor) symbol?)) sup : pid?
id - child specification identifier (symbol)
child-status - pid if running, 'undefined if not started or terminated
type - 'worker or 'supervisor
modules - always 'dynamic (Racket does not track modules like Erlang)
procedure
→ (listof (cons/c symbol? exact-nonnegative-integer?)) sup : pid?
'specs - total child specifications
'active - currently running children
'supervisors - children of type 'supervisor
'workers - children of type 'worker
procedure
(supervisor-get-childspec sup child-id)
→ (cons/c (or/c 'ok 'error) (or/c child-spec? 'not-found)) sup : pid? child-id : symbol?
procedure
(supervisor-start-child sup spec-or-args)
→ (cons/c (or/c 'ok 'error) any/c) sup : pid? spec-or-args : any/c
For 'simple-one-for-one supervisors, spec-or-args is passed to the template’s start function. For other strategies, spec-or-args should be a full child-spec.
Returns (cons 'ok pid) on success.
procedure
(supervisor-terminate-child sup child-id)
→ (or/c 'ok (cons/c 'error any/c)) sup : pid? child-id : symbol?
procedure
(supervisor-restart-child sup child-id)
→ (cons/c (or/c 'ok 'error) any/c) sup : pid? child-id : symbol?
procedure
(supervisor-delete-child sup child-id)
→ (or/c 'ok (cons/c 'error any/c)) sup : pid? child-id : symbol?
20.3 Process Primitives
procedure
(spawn-link thunk) → process?
thunk : (-> any)
procedure
(receive/timeout timeout-ms) → (or/c any/c #f)
timeout-ms : exact-nonnegative-integer?
procedure
(receive-match predicate [timeout-ms]) → (or/c any/c #f)
predicate : (-> any/c boolean?) timeout-ms : (or/c #f exact-nonnegative-integer?) = #f
procedure
(process-alive? proc) → boolean?
proc : process?
20.4 Registry
procedure
(unregister! name) → void?
name : symbol?
procedure
(registered) → (listof symbol?)
20.5 Links and Monitors
procedure
(demonitor! ref) → void?
ref : monitor-ref?
procedure
(set-trap-exit! proc trap?) → void?
proc : process? trap? : boolean?
struct
(struct exit-signal (from reason) #:extra-constructor-name make-exit-signal) from : pid? reason : any/c
struct
(struct down-signal (ref proc reason) #:extra-constructor-name make-down-signal) ref : monitor-ref? proc : process? reason : any/c
20.6 Process Dictionary
20.7 Runtime
procedure
(start-runtime! [#:schedulers n]) → exact-positive-integer?
n : exact-positive-integer? = (processor-count)
procedure
(stop-runtime!) → void?
syntax
(with-runtime #:schedulers n body ...)
procedure
procedure
procedure
(runtime-mode-green?) → boolean?
procedure
(scheduler-start!) → void?
procedure
(scheduler-stop!) → void?
procedure
(scheduler-run-until-done!) → void?
procedure
(scheduler-running?) → boolean?
20.8 GenStatem
procedure
(gen-statem-start impl args [#:name name]) → process?
impl : statem? args : any/c name : (or/c symbol? #f) = #f
procedure
(gen-statem-start-link impl args [ #:name name]) → process? impl : statem? args : any/c name : (or/c symbol? #f) = #f
procedure
(gen-statem-call statem request [ #:timeout timeout]) → any/c statem : (or/c process? symbol?) request : any/c timeout : exact-positive-integer? = 5000
procedure
(gen-statem-stop statem [reason]) → void?
statem : (or/c process? symbol?) reason : any/c = 'normal
struct
(struct postpone-action () #:extra-constructor-name make-postpone-action)
value
20.9 Distribution
procedure
(start-epmd! [port]) → epmd-server?
port : exact-positive-integer? = 4369
procedure
(stop-epmd! epmd) → void?
epmd : epmd-server?
procedure
(start-node! name port [#:cookie cookie]) → void?
name : string? port : exact-positive-integer? cookie : string? = "nocookie"
procedure
(stop-node!) → void?
procedure
(connect-node! name) → boolean?
name : string?
procedure
(global-register! name pid) → void?
name : symbol? pid : (or/c process? dist-pid?)
procedure
(global-whereis name) → (or/c pid? #f)
name : symbol?
procedure
(global-send! name msg) → void?
name : symbol? msg : any/c
procedure
(send-remote! pid msg) → void?
pid : (or/c process? dist-pid?) msg : any/c
procedure
(dist-link! pid) → void?
pid : (or/c process? dist-pid?)
procedure
(dist-monitor! pid) → monitor-ref?
pid : (or/c process? dist-pid?)
20.10 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
procedure
(hot-reload! module-path) → void?
module-path : path-string?
procedure
(trigger-code-change! proc version-info)
→ (or/c 'ok (cons/c 'error any/c)) proc : process? version-info : version-info?
procedure
(upgrade-process! proc version-info)
→ (or/c 'ok (cons/c 'error any/c)) proc : process? version-info : version-info?
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) = '()
20.11 Testing Utilities
| (require rakka/testing) | package: Rakka |
procedure
(wait-until pred [#:timeout timeout-ms]) → boolean?
pred : (-> boolean?) timeout-ms : exact-positive-integer? = 1000
procedure
(check-process-exits proc expected-reason [ #:timeout timeout-ms]) → boolean? proc : process? expected-reason : any/c timeout-ms : exact-positive-integer? = 1000
20.12 Process Groups
procedure
(pg-members group) → (listof pid?)
group : symbol?
procedure
(pg-which-groups) → (listof symbol?)
procedure
(pg-monitor group) →
pg-monitor-ref? (listof pid?) group : symbol?
procedure
(pg-demonitor ref) → symbol?
ref : pg-monitor-ref?
procedure
(pg-monitor-ref? v) → boolean?
v : any/c
20.13 PubSub
procedure
(pubsub-start name) → any/c
name : symbol?
procedure
(pubsub-stop name) → symbol?
name : symbol?
procedure
(pubsub-subscribe pubsub topic) → symbol?
pubsub : symbol? topic : string?
procedure
(pubsub-unsubscribe pubsub topic) → symbol?
pubsub : symbol? topic : string?
procedure
(pubsub-broadcast pubsub topic message) → symbol?
pubsub : symbol? topic : string? message : any/c
procedure
(pubsub-broadcast-from pubsub topic message) → symbol?
pubsub : symbol? topic : string? message : any/c
procedure
(pubsub-subscribers pubsub topic) → (listof pid?)
pubsub : symbol? topic : string?
20.14 Tables
procedure
(table-alive? t) → boolean?
t : table?
procedure
(table-whereis name) → (or/c table? #f)
name : symbol?
procedure
(table-lookup t key [default]) → any/c
t : (or/c table? symbol?) key : any/c default : any/c = #f
procedure
(table-insert-new! t key value) → boolean?
t : (or/c table? symbol?) key : any/c value : any/c
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
procedure
(table-delete! t) → void?
t : (or/c table? symbol?)
procedure
(table-clear! t) → void?
t : (or/c table? symbol?)
procedure
(table-info t) → hash?
t : (or/c table? symbol?)
21 Tracing
Erlang-style tracing for debugging and observability. Watch messages flow between
processes, see spawns and exits, trace function calls—
For you if: You’re debugging process interactions, tracking down message ordering issues, or building production observability.
21.1 Trace a Process in 30 Seconds
See all messages a process sends and receives:
(require rakka) ;; 1. Set up a tracer (where events go) (tracer (make-tty-tracer)) ;; 2. Start your server and trace it (define pid (gen-server-start (my-server) #f)) (trace pid 'messages) ; trace send/receive ;; 3. Interact normally - trace events appear (gen-server-call pid 'get) ;; Output: [TRACE 1234567] send <pid> (list ...) ;; Output: [TRACE 1234568] receive <pid> ... ;; 4. Stop when done (stop-tracer)
21.2 Understanding Trace Flags
Trace flags control what gets traced. You can combine them:
Flag |
| What It Traces |
'send |
| outgoing messages from this process |
'receive |
| incoming messages to this process |
'messages |
| both send and receive (shorthand) |
'procs |
| spawn, exit, link, unlink, register |
'calls |
| function calls matching trace patterns |
'sos |
| set flags on spawned children |
'sol |
| set flags on linked processes |
'all |
| everything |
Example: Trace everything a process does:
(trace pid 'all)
Example: Trace lifecycle events only:
(trace pid 'procs) ;; Now you'll see when this process spawns children, ;; exits, links/unlinks, or registers names
21.3 Propagating Traces Through the System
The 'sos (set on spawn) and 'sol (set on link) flags let you trace entire subsystems without touching each process:
;; Parent traces messages and passes to children (trace parent-pid 'messages 'sos) ;; Now any process spawned by parent-pid also traces messages (define child (spawn ...)) ; automatically inherits 'messages flag
This is powerful for debugging supervisor trees—
21.4 Tracer Backends
Trace events need somewhere to go. Choose a backend:
;; Print to terminal (good for debugging) (tracer (make-tty-tracer)) ;; Collect events programmatically (define-values (handler ch) (make-channel-tracer)) (tracer handler) ;; Later: (sync ch) returns trace events ;; Write to file (for production) (tracer (make-file-tracer "/tmp/trace.log")) ;; Use Racket's logging system (tracer (make-logger-tracer))
21.5 Call Tracing for GenServer
Trace specific callback invocations:
;; Trace all handle-call invocations (trace-pattern 'gen-server 'handle-call #t) (trace pid 'calls) ;; Now you'll see 'call and 'return events for handle-call (gen-server-call pid 'get) ;; Output: [TRACE ...] call (handle-call (get ...)) ;; Output: [TRACE ...] return (handle-call (reply ...))
Filter by argument pattern:
;; Only trace calls where first arg is 'get (trace-pattern 'gen-server 'handle-call #t #:match '(get _ _))
21.6 API Reference
21.6.1 Tracer Control
procedure
handler : (-> trace-event? any)
procedure
(stop-tracer) → void?
procedure
(get-tracer) → (or/c (-> trace-event? any) #f)
21.6.2 Setting Trace Flags
procedure
(trace-flags proc) → (setof symbol?)
proc : process?
procedure
(clear-trace proc) → void?
proc : process?
21.6.3 Call Patterns
procedure
(trace-pattern module fn matcher [ #:match pattern #:guard guard]) → void? module : symbol? fn : symbol? matcher : any/c pattern : any/c = #t guard : (or/c #f (-> list? boolean?)) = #f
procedure
(clear-trace-pattern module fn) → void?
module : symbol? fn : symbol?
procedure
21.6.4 Tracer Backends
procedure
(make-tty-tracer) → (-> trace-event? any)
procedure
(make-channel-tracer) →
(-> trace-event? any) async-channel?
procedure
(make-logger-tracer) → (-> trace-event? any)
procedure
(make-file-tracer path) → (-> trace-event? any)
path : path-string?
21.6.5 Trace Events
struct
(struct trace-event (type pid timestamp data) #:transparent) type : symbol? pid : (or/c pid? #f) timestamp : real? data : any/c
22 Pattern-Matching Behavior Languages
Rakka provides specialized languages for defining gen-server and gen-statem behaviors with Erlang-style pattern matching.
22.1 #lang rakka/gen-server
A language for defining gen-server behaviors with pattern-matched callbacks.
#lang rakka/gen-server (init '() (ok (hash))) (init `(,filename) (ok (load-from-file filename))) (handle-call `(get ,key) _from state (reply (hash-ref state key #f) state)) (handle-call `(set ,key ,value) _from state (reply 'ok (hash-set state key value))) (handle-cast `(log ,msg) state (displayln msg) (noreply state)) (handle-info (exit-signal pid reason) state (noreply (handle-exit state pid))) (terminate 'normal state (log-info "clean shutdown"))
The server is automatically exported with a name derived from the filename. Use it with gen-server-start:
(require "kv-store.rkt") (define pid (gen-server-start kv-store '()))
22.2 #lang rakka/gen-statem
A language for defining gen-statem state machines with pattern-matched state functions.
#lang rakka/gen-statem (init '() (statem-ok 'idle (hash))) (state idle (call `(connect ,host) from data (next-state 'connected (hash-set data 'host host) (list (statem-reply from 'ok)))) (cast 'ping data (keep-state data '()))) (state connected (call 'disconnect from data (next-state 'idle (hash-remove data 'host) (list (statem-reply from 'ok)))) (timeout 'heartbeat data (keep-state data '())))
23 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. |