IPC Guide
Client-server communication: starting a server, connecting from clients, remote queries, authentication, multi-process patterns, and the C API.
Rayforce includes a built-in IPC layer that turns any Rayforce process into a query server. Clients connect over TCP, send queries or data, and receive serialized results back. This guide walks through the full workflow from starting a server to building multi-process architectures.
1. Getting Started
The fastest way to try IPC: start a server in one terminal and connect from another.
Start a Server
The -p flag starts Rayforce in server mode, listening on the given port:
./rayforce -p 5000
The server starts, prints a listening message, and waits for connections. It also runs a REPL, so you can interact with it directly.
Connect from a Client
In a second terminal, start a regular Rayforce REPL and open a connection:
;; Open a handle to the server
(set h (.ipc.open "127.0.0.1:5000"))
The .ipc.open function returns a connection handle. Use .ipc.send to send a query string to the server, which parses, evaluates, and returns the result:
;; Send a query, get the result back
(.ipc.send h "(+ 1 2)")
;; => 3
When you are done, close the connection:
(.ipc.close h)
2. Loading Data on the Server
A server is most useful when it holds data that clients query remotely. Use an init script to load data at startup.
Start with an Init Script
./rayforce -p 5000 init.rfl
The server executes init.rfl before accepting connections. A typical init script creates tables:
;; init.rfl — load data into the server
(set trades (table [sym price qty time]
(list [AAPL GOOG AAPL MSFT GOOG]
[150.5 2800.0 151.2 310.0 2795.0]
[100 50 200 75 120]
[09:30:00 09:30:01 09:30:05 09:30:10 09:30:12])))
(set quotes (table [sym bid ask]
(list [AAPL GOOG MSFT]
[150.0 2790.0 309.5]
[150.5 2800.0 310.5])))
(println "Loaded trades and quotes")
Query from a Client
Once the server has data, clients can query it remotely:
(set h (.ipc.open "127.0.0.1:5000"))
;; Query the remote trades table
(.ipc.send h "trades")
;; => the full trades table is returned
;; Run a filtered query
(.ipc.send h "(select {from: trades where: (= sym 'AAPL)})")
(.ipc.close h)
3. Remote Queries
The server evaluates any valid Rayfall expression sent as a string. This means you can run filters, aggregations, joins, and any other operation remotely.
Filtered Queries
;; All trades where price exceeds 100
(.ipc.send h "(select {from: trades where: (> price 100)})")
Aggregations
;; Total quantity traded
(.ipc.send h "(select {from: trades total: (sum qty)})")
;; Volume-weighted average price by symbol
(.ipc.send h "(select {from: trades by: sym vwap: (% (sum (* price qty)) (sum qty))})")
Joins
;; Join trades with quotes on sym
(.ipc.send h "(select {from: (left-join trades quotes 'sym)})")
4. Dynamic Queries & Expression Payloads
.ipc.send accepts any serializable value, not just strings. The server behavior depends on what you send:
| Payload type | Server behavior |
|---|---|
| String | Parsed as Rayfall code and evaluated |
| List with function head | Evaluated as a function call — no parsing needed |
| Data (vector, table, atom) | Evaluated as identity — returned as-is |
Expression payloads
Instead of strings, construct the query as a list. Builtins resolve to function objects — use them directly as the list head. Dict literals are self-evaluating, so column references inside them are preserved for server-side resolution:
;; Arithmetic
(.ipc.send h (list + 1 2))
;; => 3
;; Select with filter — dict is self-evaluating, select resolves columns
(.ipc.send h (list select {from: trades where: (> price 200)}))
;; => filtered trades table
;; Aggregation by group
(.ipc.send h (list select {from: trades by: sym total: (sum qty)}))
;; Map a lambda over server-side data
(.ipc.send h (list map (fn [x] (* x 2)) (list til 10)))
;; => [0 2 4 6 8 10 12 14 16 18]
For dynamic queries, substitute runtime values into the expression:
;; Dynamic filter threshold
(set threshold 200)
(.ipc.send h (list select {from: trades where: (list > (quote price) threshold)}))
;; Dynamic grouping
(set group-col 'sym)
(.ipc.send h (list select {from: trades by: group-col total: (list sum (quote qty))}))
{from: trades where: (> price 200)} preserves its contents as data. When the server evaluates the list, select resolves trades and price in the table context. Use (list ...) inside the dict when you need to splice runtime values into an expression.
5. Authentication
Rayforce supports password-based authentication with an optional read-only restriction.
Password Authentication
Start the server with -u to require a password from all clients:
./rayforce -p 5000 -u secretpass
Clients must supply the password when connecting:
(set h (.ipc.open "127.0.0.1:5000:user:secretpass"))
The connection string format is host:port:user:password. The username is transmitted but not validated on the server — only the password is checked.
Read-Only Mode
Use -U instead of -u to enable authentication with read-only restrictions:
./rayforce -p 5000 -U secretpass
In read-only mode, clients can run queries and read data, but mutating operations (set, insert, upsert, update, file writes, system commands) are blocked. The server returns an error if a restricted builtin is called.
6. Multi-Process Architecture
A common pattern is one data server with multiple query clients. Each client connects independently and runs queries against the shared data.
Example: Trade Analytics System
Terminal 1 — Data server:
# Start the server with trade data
./rayforce -p 5000 init.rfl
# Server output: "Loaded trades and quotes"
# Server is now listening on port 5000
Terminal 2 — Analytics client:
./rayforce
;; Connect to the data server
(set h (.ipc.open "127.0.0.1:5000"))
;; Run aggregations
(.ipc.send h "(select {from: trades by: sym avg_price: (avg price) total_qty: (sum qty)})")
;; Find the most active symbol
(.ipc.send h "(select {from: trades by: sym n: (count) desc: 'n take: 1})")
Terminal 3 — Monitoring client:
./rayforce
(set h (.ipc.open "127.0.0.1:5000"))
;; Check current row count
(.ipc.send h "(count trades)")
;; => 5
;; Check table schema
(.ipc.send h "(meta trades)")
7. Error Handling
Several categories of errors can occur during IPC operations.
Connection Refused
If the server is not running or the port is wrong, .ipc.open returns an error:
(.ipc.open "127.0.0.1:9999")
;; => error: connection refused
Authentication Errors
Connecting without credentials to a password-protected server, or supplying the wrong password:
;; No credentials — server requires auth
(.ipc.open "127.0.0.1:5000")
;; => error: authentication required
;; Wrong password
(.ipc.open "127.0.0.1:5000:user:wrongpass")
;; => error: authentication failed
Read-Only Restriction
When connected to a -U server, mutating operations are rejected:
;; Trying to set a variable on a read-only server
(.ipc.send h "(set x 42)")
;; => error: restricted in read-only mode
Network Errors
If the server shuts down or the network drops while a query is in flight, .ipc.send returns an error. Use try to handle errors gracefully:
(try
(println "Trade count:" (.ipc.send h "(count trades)"))
(fn [e] (println "Query failed:" e)))
8. C API
The IPC layer is also accessible from C, enabling you to embed Rayforce clients in applications.
Functions
| Function | Description |
|---|---|
ray_ipc_connect(host, port, user, password) |
Open a TCP connection. Returns a handle. Pass NULL for user/password if no auth. |
ray_ipc_send(handle, msg) |
Synchronous send: serialize msg, send, wait for response, return deserialized result. |
ray_ipc_send_async(handle, msg) |
Fire-and-forget send: serialize and send msg without waiting for a response. |
ray_ipc_close(handle) |
Close the connection and free the handle. |
ray_ipc_listen(poll, port) |
Register a server socket on the given poll loop. Used internally by -p mode. |
Example: C Client
#include <rayforce.h>
#include "core/ipc.h"
int main(int argc, char** argv) {
ray_runtime_t* rt = ray_runtime_create(argc, argv);
// Connect to a running server (returns handle or negative error)
int64_t h = ray_ipc_connect("127.0.0.1", 5000, NULL, NULL);
if (h < 0) { printf("Connection failed\n"); return 1; }
// Send a query string
ray_t* query = ray_str("(+ 1 2)", 7);
ray_t* result = ray_ipc_send(h, query);
ray_release(query);
if (!RAY_IS_ERR(result)) {
printf("Result: %lld\n", result->i64);
ray_release(result);
}
ray_ipc_close(h);
ray_runtime_destroy(rt);
return 0;
}
ray_ipc_send_async to avoid blocking on the round-trip.
Next Steps
- IPC & Serialization Reference — Wire format specification and full API details
- Storage Guide — Persisting data with CSV, columnar files, and partitioned tables
- Core C API — Working with
ray_tobjects, vectors, and tables in C - Getting Started Tutorial — Hands-on introduction to Rayfall