Skip to main content

SochDB IPC Server Architecture

The SochDB IPC server provides a high-performance, multi-process interface to a single embedded SochDB database over a Unix domain socket. It lets several local processes share one on-disk database instance with low-latency, length-prefixed binary framing — no network stack, no serialization overhead of a full RPC layer.

Library API, not a standalone binary

The IPC server is a Rust library type, IpcServer (with a matching IpcClient), defined in sochdb-storage/src/ipc_server.rs (part of the core engine, v2.0.3). It is not shipped as a separate sochdb-server executable — you embed it in your own process, or reach it through the unified connect() client API (see below). The networked, multi-tenant surface is the separate gRPC server (sochdb-grpc-server).

The core engine (including this IPC server) is licensed AGPL-3.0-or-later with commercial licensing available; the language SDKs are Apache-2.0.

Connecting via connect() (ipc://)

The IPC server is the transport behind the ipc:// scheme in SochDB's unified connect() client API. The URI is the socket path:

ipc:///tmp/sochdb.sock

The unified URI → transport mapping is:

URI schemeTransport
file://./dataEmbedded on-disk database (in-process)
ipc:///tmp/sochdb.sockLocal IPC over a Unix domain socket (this page)
grpc://localhost:50051gRPC to a SochDB server (plaintext)
grpcs://prod.example.com:443gRPC over TLS
note

The ipc:// path is a filesystem socket path, not a host:port. There is no TCP port for the IPC server — it is local-only by design. The unified connect() surface is documented in the engine CHANGELOG; the in-repo language SDKs currently expose transport-specific clients, so the exact ipc:// parsing may vary by SDK version. Confirm support in your SDK before relying on it.

Architecture

The server uses a thread-per-client model optimized for low-latency local communication. A shared Arc<Database> kernel is accessed concurrently by one handler thread per connected client.

┌────────────────────────────────────────────────────────────────┐
│ IPC Server Process │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ Database Kernel (Arc<Database>) │ │
│ └────────────────────────────────────────────────────────┘ │
│ ▲ ▲ ▲ │
│ │ │ │ │
│ ┌────────┴────────┐ ┌────────┴────────┐ ┌────────┴────────┐ │
│ │ ClientHandler 1 │ │ ClientHandler 2 │ │ ClientHandler N │ │
│ └────────┬────────┘ └────────┬────────┘ └────────┬────────┘ │
│ │ │ │ │
│ ┌────────┴────────────────────┴────────────────────┴────────┐│
│ │ Unix Domain Socket Listener ││
│ │ /tmp/sochdb.sock ││
│ └──────────────────────────────────────────────────────────┘│
└────────────────────────────────────────────────────────────────┘

Key Components

  1. Listener Thread: Accepts incoming Unix socket connections and spawns a handler thread per client. Honors a max_connections limit (default 100), rejecting connections beyond it.
  2. Client Handlers: One dedicated thread per client that maintains per-connection transaction state and cursor isolation.
  3. Shared Kernel: A thread-safe Arc<Database> instance shared across all handlers.
  4. Transaction Map: Each handler maintains its own active_txns map (client_txn_idTxnHandle), ensuring per-connection isolation. On disconnect, the handler aborts any still-open transactions.

Configuration defaults

IpcServerConfig defaults (ipc_server.rs):

FieldDefaultMeaning
socket_path/tmp/sochdb.sockPath to the Unix socket file
max_connections100Maximum concurrent connections
thread_pool_size4Handler thread-pool hint
connection_timeout_secs300Connection timeout (5 minutes)

Security model

Local trust boundary — no in-band authentication

The IPC protocol has no handshake and no authentication. Access control is the filesystem permission on the socket file. On bind, the server best-effort chmods the socket to 0600 (owner read/write only), so other local users cannot connect to the unauthenticated endpoint. This is defense-in-depth for a local-only transport — do not expose the socket file to untrusted local users, and use the gRPC server with --auth/TLS for any networked or multi-tenant deployment.

Wire Protocol

Every message uses a lightweight length-prefixed binary frame designed for minimal overhead. Lengths are little-endian. The maximum payload size is 16 MB; larger frames are rejected.

Frame Format:

┌─────────────────┬─────────────────────┬──────────────────────────┐
│ OpCode (1 byte) │ Length (4 bytes LE) │ Payload (N bytes) │
└─────────────────┴─────────────────────┴──────────────────────────┘

Request OpCodes (Client → Server)

OpCodeHexNameDescription
10x01PUTAuto-commit key-value write
20x02GETRead value by key
30x03DELETEDelete key (auto-commit)
40x04BEGIN_TXNStart a new explicit transaction
50x05COMMIT_TXNCommit an active transaction
60x06ABORT_TXNRoll back / abort a transaction
70x07QUERYExecute a path query (returns TOON)
80x08CREATE_TABLEDefine a table schema
90x09PUT_PATHWrite to a hierarchical path
100x0AGET_PATHRead from a hierarchical path
110x0BSCANPrefix range scan
120x0CCHECKPOINTForce a durability flush to disk
130x0DSTATSServer runtime metrics
140x0EPINGHealth check
150x0FEXECUTE_SQLSubmit a SQL string (see note)
EXECUTE_SQL is a stub

EXECUTE_SQL (0x0F) is accepted but does not run SQL server-side. It returns a VALUE response containing a JSON error directing callers to perform SQL-to-KV mapping client-side (as the Python SDK does). For full SQL over the wire use the PostgreSQL wire protocol on the gRPC server.

Response Codes (Server → Client)

HexNameDescription
0x80OKOperation succeeded (no data)
0x81ERROROperation failed (payload = UTF-8 error message)
0x82VALUEData return (empty payload = key/path not found)
0x83TXN_IDTransaction ID / commit timestamp (u64 LE)
0x86STATS_RESPJSON statistics
0x87PONGHealth-check response
note

The ROW (0x84) and END_STREAM (0x85) opcodes are reserved in the protocol constants but are not emitted by the current server — QUERY returns a single VALUE frame containing the full TOON result rather than a streamed row sequence.

Server Statistics

The server maintains atomic counters for real-time monitoring, returned by the STATS opcode as a STATS_RESP (0x86) JSON payload.

Returned JSON Structure:

{
"connections_total": 150,
"connections_active": 12,
"requests_total": 45000,
"requests_success": 44995,
"requests_error": 5,
"bytes_received": 1024000,
"bytes_sent": 2048000,
"uptime_secs": 3600,
"memtable_size_bytes": 0,
"wal_size_bytes": 0,
"active_transactions": 3
}
note

memtable_size_bytes and wal_size_bytes are currently always reported as 0 — they are placeholders not yet wired to live storage metrics. active_transactions reflects the open transactions for the connection that issued the STATS request.

Developer Guide: Implementing a Client

If you are building a client driver in a new language (e.g. Go, Ruby, Node.js), follow these steps:

  1. Connect: Open a Unix domain socket to the configured path (default /tmp/sochdb.sock).
  2. Handshake: None — a successful connection implies readiness. (Access is gated by the socket file's 0600 permissions.)
  3. Send requests:
    • Write 1 byte OpCode.
    • Write 4 bytes little-endian Length.
    • Write Length bytes of Payload.
  4. Read responses:
    • Read 1 byte response OpCode.
    • Read 4 bytes little-endian Length.
    • Read Length bytes of Payload.
  5. Clean up: Close the socket when done. The server automatically aborts any open transactions for the connection.

Payload encodings

A few opcodes use sub-framing inside the payload (all integers little-endian):

  • PUT (0x01): key_len (u32) + key + value.
  • GET / DELETE (0x02 / 0x03): the raw key bytes.
  • PUT_PATH / GET_PATH (0x09 / 0x0A): segment_count (u16) + repeated [seg_len (u16) + segment] + trailing value (empty for GET).
  • COMMIT_TXN / ABORT_TXN (0x05 / 0x06): the 8-byte client_txn_id (u64).
  • SCAN (0x0B): the prefix string; the VALUE reply is count (u32) + repeated [key_len (u16) + key + val_len (u32) + value].

Example: Raw Python Client

This is a minimal raw-socket example for illustration. For production use, prefer the Python SDK (pip install sochdb, v0.5.9), which wraps this protocol for you.

import socket
import struct

SOCKET_PATH = "/tmp/sochdb.sock"

GET = 0x02
VALUE = 0x82
ERROR = 0x81


def simple_get(key_bytes: bytes) -> bytes | None:
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
s.connect(SOCKET_PATH)
try:
# Send GET: opcode (1) + length (4 LE) + payload
header = struct.pack("<BI", GET, len(key_bytes))
s.sendall(header + key_bytes)

# Read response header (opcode + length)
resp_header = s.recv(5)
code, length = struct.unpack("<BI", resp_header)

# Read response body
data = b""
while len(data) < length:
data += s.recv(length - len(data))

if code == VALUE:
return data or None # empty payload means "not found"
if code == ERROR:
raise RuntimeError(data.decode())
raise RuntimeError(f"unexpected opcode: {code:#x}")
finally:
s.close()

See also

  • gRPC Server — the networked, authenticated, multi-tenant surface.
  • Python SDK — the high-level embedded/server client (v0.5.9).