On this page
Serving requests
With a source fd in hand, a source spends its life in one loop: read a request, decode it, do the work, reply. This guide builds that loop. The exhaustive per-op detail is in rsi/request.h and rsi/response.h.
The loop skeleton
unsigned char buf[64 * 1024]; /* size generously — a short buffer is EMSGSIZE */
for (;;) {
ssize_t n = rsi_read_request(src_fd, buf, sizeof buf);
if (n == 0) break; /* EOF — the source is closing */
if (n < 0) { perror("read_request"); break; }
struct rsi_request req;
if (rsi_parse_request(buf, n, &req) != 0) continue; /* EBADMSG — skip */
switch (req.op_code) {
case RSI_LOOKUP: handle_lookup(src_fd, &req); break;
case RSI_SET_VALUE: handle_set_value(src_fd, &req); break;
/* … a case per op you support … */
default:
/* Unknown or unsupported op: reply with a non-OK status
* (RSI_TXN_NOT_SUPPORTED for transaction ops you don't implement). */
rsi_respond_status(src_fd, &req, RSI_INVALID);
break;
}
}
Three details in that skeleton matter:
rsi_read_requestblocks until a request is queued, and returns0at EOF — that's your exit. Sizebufgenerously; a frame larger thanbufreturns-1/EMSGSIZE.rsi_parse_requestgives youreq.op_codeto dispatch on, plusreq.request_id(which every reply must echo — the helpers do this for you) andreq.txn_id(nonzero when the request is inside a transaction).- Always reply. Every request expects exactly one response. If you can't handle an op, reply with a non-OK status rather than dropping it.
Decoding a request
Inside a handler, decode the payload with the matching rsi_request_* parser. For a LOOKUP:
void handle_lookup(int fd, const struct rsi_request *req)
{
struct rsi_lookup q;
if (rsi_request_lookup(req, &q) != 0) { /* EBADMSG */
rsi_respond_status(fd, req, RSI_INVALID);
return;
}
/* q.parent_guid (by value), q.child_name / q.child_name_len (borrowed).
Resolve the child in your storage across its layers … */
/* … then reply with the resolved path entries + metadata: */
rsi_respond_lookup(fd, req, entries, entry_count, metadata, metadata_count);
}
And for a SET_VALUE, a status-only op:
void handle_set_value(int fd, const struct rsi_request *req)
{
struct rsi_set_value v;
if (rsi_request_set_value(req, &v) != 0) {
rsi_respond_status(fd, req, RSI_INVALID);
return;
}
/* Honour the CAS guard before storing. */
if (v.expected_sequence != 0 && current_seq(&v) != v.expected_sequence) {
rsi_respond_status(fd, req, RSI_CAS_FAILED);
return;
}
store_value(&v); /* your storage */
rsi_respond_status(fd, req, RSI_OK);
}
The borrow rule
Every decoded name and data field — q.child_name, v.value_name, v.data, and the rest — is a (ptr, len) pair that points into buf. Those pointers are valid only until the next rsi_read_request reuses the buffer. So:
- Consume them within the handler (store the bytes, resolve the name) before the loop comes around again, or
- Copy out anything you need to keep. Never stash a raw borrowed pointer across iterations.
GUIDs and scalars in the decoded struct are copied by value, so those are always safe to keep — it's only the borrowed (ptr, len) fields that have the lifetime.
Transactions
When the kernel sends BEGIN_TRANSACTION, buffer subsequent writes tagged with that transaction_id instead of applying them; req.txn_id on each later request tells you which transaction it belongs to (0 = none). On COMMIT_TRANSACTION apply the buffered set atomically; on ABORT_TRANSACTION discard it. All three are status-only replies. The kernel coordinates transaction boundaries — your job is to buffer, then commit or discard on command.
Next
- Building responses — choosing and filling the right reply.
rsi/request.hreference — every op's decoder and struct.