iClaustron training for C developers · module 1 of 8 · 15 min
iClaustron and its C API
A library that makes a program an API node of RonDB 26.10: where it sits, how C calls it, how it fails, and the five ways to program it.
What it is · an API node in a library
iClaustron: a library that makes your program an API node of RonDB
A library
Linked into the application. It joins a RonDB 26.10 cluster as an API node and speaks the NDB protocol to the data nodes itself.
Rust, MIT licensed
Written in Rust, in eight crates. A Rust program uses the crate ic_apid directly.
A C ABI
libiclaustron, shared and static, with the header iclaustron/ic_apid.h and a pkg-config file: for C, C++ and any language that calls C.
cargo xtask install /usr/local # or any prefix cc -o prog prog.c $(pkg-config --cflags --libs iclaustron)
iclaustron: README.md; LICENSE; doc/user-guide.md (opening, §1 Building and linking); crates/ic_capi/Cargo.toml ([lib] cdylib, staticlib); xtask/src/main.rs (install)
Scope · RonDB 26.10, release 0.1 and after
What iClaustron does today, and what it does not do yet
Does
- Key operations by primary key and unique key
- Transactions: commit, rollback, hints
- Scans of tables and ordered indexes
- Programs run in the data nodes
- AUTO_INCREMENT; tables without a primary key
- Node failure: heartbeats, reconnects, takeover
Not yet
- Blob columns: BLOB, TEXT, JSON, GEOMETRY
- Schema changes: use a MySQL server
- TLS; data nodes chosen by location domain
- Events, pushdown joins, aggregation
- Windows: Linux and macOS only
iclaustron: doc/user-guide.md (opening, §10 Node failure, §12 What is not done yet); crates/ic_apid/src (key_op.rs, transaction.rs, scan.rs, index_scan.rs, program.rs, auto_increment.rs); crates/ic_apid/src/handshake.rs (TLS disabled)
Where it sits · application, API node, data nodes
Where iClaustron sits: inside the application, linked to every data node
The application links the library. Its calls run on the application's own threads; the library adds a few threads of its own (module 02).
ic_apid_global_connect asks a management server for a node id and the cluster's configuration: the program is now an API node.
It then links to every data node and speaks the NDB protocol directly: requests out, replies and rows back. No MySQL server on the path.
iclaustron: doc/user-guide.md (§1 What you need, §2 Connecting); crates/ic_apid/src/apid_global.rs (ApidGlobal::start_with_options); include/iclaustron/ic_apid.h (ic_apid_global_connect); doc/rust/05-ndb-protocol.md (Names)
Inside the library · eight crates
The eight crates: the C ABI on top, the portability layer at the bottom
Each crate uses only crates below it. The Rust API in ic_apid is the implementation; ic_capi is a thin wrapper, one exported function per method.
iclaustron: Cargo.toml (workspace members); crates/*/Cargo.toml (description, dependencies; ic_capi [lib]); doc/rust/04-api-design.md (Three layers, one implementation)
C conventions · results
C API results: every call that can fail returns 0 or an error code
IC_TABLE_DEF *table; /* written only when the call gives 0 */
int32_t ret;
EX_CHECK(ic_apid_conn_table_bind(conn, EX_DB, EX_TABLE, &table));
/* an error the program expects: the same key inserted twice
(write_row calls ic_apid_conn_write) */
ret = write_row(conn, rec, IC_WRITE_KIND_INSERT, &row);
if (ret == IC_NDB_ERROR_ROW_EXISTS)
printf("a second insert: error %d, as it should\n", (int)ret);
else
ex_fail("the second insert", ret);
- The result is an
int32_t: 0, or the error code. - What a call makes comes back through an out pointer, written only on 0.
- The examples wrap calls in
EX_CHECK(t1.h): anything but 0 stops with the code's text. - A NULL handle or out pointer:
IC_ERROR_NULL_POINTER, 7157.
iclaustron: include/iclaustron/ic_apid.h (opening comment, Results; IC_ERROR_NULL_POINTER); examples/c/t1.h (EX_CHECK, ex_fail); examples/c/key_ops.c (main, write_row)
C conventions · option and argument structs
Structs with a size: _init(&s, sizeof s) first, then set fields
typedef struct {
uint32_t size; /* as ic_write_key_args_init was given it */
uint32_t kind; /* one of the IC_WRITE_KIND_* values */
uint32_t abort_option;
uint32_t batch_hint;
/* ... callback, user_ref ... */
} IC_WRITE_KEY_ARGS;
/* retry.c */
IC_WRITE_KEY_ARGS write_args;
ic_write_key_args_init(&write_args, sizeof write_args);
write_args.kind = IC_WRITE_KIND_UPDATE;
- The
_initwrites every default, and the size the program was built with. - A later release adds fields at the end only.
- Options (
IC_GLOBAL_OPTIONS,IC_EXECUTOR_OPTIONS): an older program gets the fields it knows, defaults for the rest. - Arguments (
IC_READ_KEY_ARGS,IC_SCAN_ARGS, …): one shorter than the library's is refused,IC_ERROR_INCONSISTENT_DATA(7020).
iclaustron: include/iclaustron/ic_apid.h (opening comment, Structs with a size; IC_WRITE_KEY_ARGS); crates/ic_capi/src/lib.rs (ic_global_options_init, ic_write_key_args_init, sized_from_c); examples/c/retry.c (main)
C conventions · handles and ids
Handles are pointers you free; queries, transactions and scans are ids
Handles
IC_APID_GLOBAL *,IC_APID_CONNECTION *,IC_TABLE_DEF *,IC_RECORD *,IC_APID_EXECUTOR *, …- Made by one function, given back to its
_free; the caller owns each until then. - Free connections and executors before their global. Module 03 has every object.
Ids
IC_QUERY_ID,IC_TRANS_ID,IC_SCAN_ID: auint64_tin their connection, never 0; a scan's batch has anIC_SCAN_BATCH_IDof its own.- A slot and its generation in one word; a free or a close moves the generation on.
- The old id is refused with an error, never followed: a later object always has an id of its own.
iclaustron: include/iclaustron/ic_apid.h (opening comment, Handles, Ids); crates/ic_util/src/ptr_array.rs (PtrId, PtrArray::remove); crates/ic_apid/src/query.rs (QueryId, TransId); doc/rust/04-api-design.md (Object model; C API rules, Ids)
C conventions · rows
Rows and records: a row is bytes, a record says where each field lies
struct t1_row {
uint32_t id; /* INT UNSIGNED NOT NULL, the primary key */
int32_t val; /* INT, nullable: null bit 0 */
uint32_t code; /* INT UNSIGNED NOT NULL, the unique key */
uint8_t name_len; /* VARCHAR(32) latin1, nullable: null bit 1 */
char name[32];
uint8_t null_bits;
};
/* t1.h, ex_t1_record: n fields, each its id, offset and null bit */
IC_FIELD_SPEC spec[4];
/* ... spec[n].field_id, .offset, .null_byte_offset, .null_bit ... */
EX_CHECK(ic_record_create(table, spec, n, sizeof(struct t1_row),
&rec));
- A row is a byte array. A record lists, for each field, its offset and its null bit: usually over a C struct.
- Values are kept as the data nodes keep them: little-endian integers, a VARCHAR as a length byte, then its bytes.
ic_record_create_defaultlays a row out for you.
iclaustron: examples/c/t1.h (struct t1_row, ex_t1_record); include/iclaustron/ic_apid.h (IC_FIELD_SPEC, ic_record_create, ic_record_create_default); doc/user-guide.md (§3 Tables, indexes and records)
Connecting · from examples/c/t1.h
Connecting: start a global, wait for the data nodes, make a connection
/* ex_connect(argc, argv, name, IC_APID_CONNECTION **conn) */
IC_GLOBAL_OPTIONS options;
IC_APID_GLOBAL *global;
uint32_t started = 0;
ic_global_options_init(&options, sizeof options);
EX_CHECK(ic_apid_global_connect(connect_string, name, &options,
&global));
EX_CHECK(ic_apid_global_wait_for_started(global, EX_START_WAIT_MS,
1, &started));
if (started == 0) {
/* ... no data node started ... */
}
EX_CHECK(ic_apid_global_create_connection(global, conn));
- The connectstring lists management servers,
host[:port], comma separated;nodeid=Nasks for that node id. connectfetches a node id and the configuration, starts the library's threads and returns; data nodes link in the background.wait_for_startedwaits up to 30 s here, for every data node (allis 1).
iclaustron: examples/c/t1.h (ex_connect, EX_START_WAIT_MS); include/iclaustron/ic_apid.h (ic_global_options_init, ic_apid_global_connect, ic_apid_global_wait_for_started, ic_apid_global_create_connection); doc/user-guide.md (§2 Connecting)
Connecting · the global's options
Global options; one global per cluster, one connection per thread
| IC_GLOBAL_OPTIONS | Default | Meaning |
|---|---|---|
| receive_threads | 1 | threads reading the data nodes' links; one per data node at most |
| reply_wait_ms | 0: 6 min | how long a transaction waits for replies before it ends with 4012 |
| mgm_timeout_ms | 30 000 | how long a management server has to answer |
| auto_increment_prefetch | 4 096 | values one refill reserves for a table |
- One global per cluster: a program that talks to two clusters starts two.
- A connection is one thread's view: its queries, transactions, replies. One thread at a time uses it.
- At most 4 711 connections per process, an executor's own included (7139).
- The global runs receive threads, a send pool, a heartbeat thread and a connect thread per data node: module 02.
iclaustron: include/iclaustron/ic_apid.h (IC_GLOBAL_OPTIONS, ic_apid_global_create_connection, IC_ERROR_TOO_MANY_USER_THREADS); crates/ic_apid/src/apid_global.rs (GlobalOptions, start_with_options); crates/ic_port/src/consts.rs (IC_MAX_THREAD_CONNECTIONS); doc/user-guide.md (§2)
A first program · examples/c/key_ops.c
A first program, part 1: bind a table, build a record, insert a row
IC_APID_CONNECTION *conn;
IC_APID_GLOBAL *global = ex_connect(argc, argv, "ex_key_ops", &conn);
IC_TABLE_DEF *table;
IC_RECORD *rec;
struct t1_row row;
EX_CHECK(ic_apid_conn_table_bind(conn, EX_DB, EX_TABLE, &table));
rec = ex_t1_record(table, 0); /* every field of t1 */
ex_fill(&row, 1, 10, 100, "one"); /* id 1, val 10, code 100 */
EX_CHECK(ic_apid_conn_write(conn, IC_WRITE_KIND_INSERT, rec,
(const uint8_t *)&row, sizeof row, rec,
(const uint8_t *)&row, sizeof row));
table_bindfetches the table's description from the cluster, once for all connections of the global.- One record serves as key record and attribute record: it holds the primary key.
ic_apid_conn_writeis a transaction of its own, sent and waited for before it returns.
iclaustron: examples/c/key_ops.c (main, write_row); examples/c/t1.h (ex_connect, ex_t1_record, ex_fill); include/iclaustron/ic_apid.h (ic_apid_conn_table_bind, ic_apid_conn_write); doc/user-guide.md (§3, §5 Synchronously)
A first program · examples/c/key_ops.c
A first program, part 2: read the row back, then free in order
struct t1_row key;
bool found;
ex_fill(&key, 1, 0, 0, NULL); /* the key: id 1 */
EX_CHECK(ic_apid_conn_read_committed(conn, rec, (const uint8_t *)&key,
sizeof key, rec, (uint8_t *)&row,
sizeof row, &found));
if (found)
ex_print("inserted", &row);
ic_record_free(rec);
ic_table_def_free(table);
ic_apid_conn_free(conn);
ic_apid_global_free(global);
- A committed read takes no lock: the last committed value.
- No row is no error:
foundis false. - Free each connection before the global, whose free stops the library's threads. Table and record handles may outlive the connection.
iclaustron: examples/c/key_ops.c (read_and_print, main); include/iclaustron/ic_apid.h (ic_apid_conn_read_committed, ic_apid_conn_free, ic_apid_global_free; opening comment, Handles)
Errors · what a code says
Errors: a code with a category, a severity and a text
Where from
7000 and up: iClaustron's own, IC_ERROR_*. Small positive: the operating system's. The rest: NDB's, as the data nodes report them, IC_NDB_ERROR_*.
Category
ic_error_category: User, the request was wrong (no such row, a duplicate key); Internal; External, a node failure, an overload, a timeout, a lost link.
Severity
ic_error_severity: Temporary, the same request may well succeed if tried again; Error, something must change; Stop, the library can no longer be used.
ic_error_message(code, buf, len) copies a code's text. ic_error_is_temporary(code) is the question to ask first. Modules 04 to 08 meet the codes of each model.
iclaustron: include/iclaustron/ic_apid.h (opening comment, Results; ic_error_category, ic_error_severity, ic_error_is_temporary, ic_error_message); crates/ic_port/src/err.rs (IcError); crates/ic_port/src/ndb_err.rs (ErrorClass::category, ErrorClass::severity); doc/user-guide.md (§9)
Errors · when to try again, from examples/c/retry.c
Retrying: a temporary error means the whole transaction again
for (attempt = 1; attempt <= ATTEMPTS; attempt++) {
/* ... copy the row into the query's key and attribute rows ... */
EX_CHECK(ic_apid_conn_start_transaction(conn, hint, table, &trans));
EX_CHECK(ic_apid_conn_write_key(conn, updater, trans, &write_args));
EX_CHECK(ic_apid_conn_commit_transaction(conn, trans));
ret = wait_for(conn, trans, updater); /* flush until done */
if (ret == 0)
break; /* committed */
if (!ic_error_is_temporary(ret))
ex_fail("the update", ret);
/* ... a pause that grows with each attempt ... */
}
- Temporary (266 lock wait, 4010 node failure, 7135 link lost, …): a new transaction, every query again, after a pause.
- 4012: the replies stopped, the outcome is unknown. It counts as temporary, but retry a write only once you know it did not commit, or when doing it twice does no harm.
- A user error, such as 626 no row, does not clear up by itself.
iclaustron: examples/c/retry.c (main, wait_for); include/iclaustron/ic_apid.h (ic_error_is_temporary, IC_NDB_ERROR_NO_REPLY); crates/ic_port/src/ndb_err.rs (ErrorClass::severity: UnknownResult); doc/user-guide.md (§9 When to try again)
Five programming models · at a glance
The five programming models: what the caller writes and what it gets
| Model · modules | The caller writes | Threads | In flight | Batching | Reads a second |
|---|---|---|---|---|---|
| Synchronous04 · 07 | one blocking call per query, on its own rows | the caller, with its connection | 1 per thread | none: a send per call | 198 000 · 64 threads |
| Batched synchronous04 · 07 | queries on transactions, then flush until done | the caller, with its connection | a batch per thread | one send per data node for the batch | 1.17–1.35 M · 1 thread, batch 200 |
| Asynchronous05 · 07 | queries with callbacks; send, then poll | the caller, with its connection | many transactions per thread | all defined since the last send | 2.6 M · 1 thread, depth 4 |
| Synchronous through an executor06 · 07 | a request of one operation, executed | the callers, no connection; the executor | 1 per caller | across callers, by the executor | 268 000 · 64 callers, 1 executor |
| Batched synchronous through an executor06 · 07 | a request of many operations: one transaction | the callers, no connection; the executor | a request per caller | within the request and across callers | not measured |
Reads of t9 by primary key on a Mac, two release data nodes on the same machine.
iclaustron: doc/rust/architecture.html (§1 What it does, measured; §3 Asynchronous model; §4 Synchronous model, table); doc/rust/12-performance-model.md (Calibration: 1 thread, depth 1, batch 200; Executors); doc/user-guide.md (§5 Three ways to work); include/iclaustron/ic_apid.h (Executors)
The training · eight modules
The training map, module by module
| # | Module | Teaches |
|---|---|---|
| 01 | Overview | the library, its crates, the C conventions, connecting, errors, the five models |
| 02 | Thread architecture | user, receive, send pool, heartbeat, connect and executor threads; node failure |
| 03 | Object architecture | global, connection, definitions, records, queries, transactions, scans, shapes, requests |
| 04 | Key operations, synchronous | Synchronous and Batched synchronous, on a connection |
| 05 | Key operations, asynchronous | Asynchronous: send, poll, callbacks, pipelines |
| 06 | Key operations through an executor | Synchronous and Batched synchronous through an executor |
| 07 | Scans | scans in the five models: row by row, a batch at a time, asynchronously, through a reader |
| 08 | Advanced scans | index ranges, order, partition pruning, filters in the data nodes, updating scanned rows |
hops_slideware: 2026-10/iclaustron-training/PLAN.md (Agenda, The five programming models)
Recap · module 01
What to take away from the overview
- iClaustron is a library: the program becomes an API node and talks to the data nodes directly.
- Every C call that can fail returns 0 or an error code; a struct with a size is filled by
_init(&s, sizeof s)first. - Handles are pointers you free, connections before the global; queries, transactions and scans are ids.
- One global per cluster, one connection per thread, at most 4 711 in a process.
- A temporary error means the whole transaction again; after 4012, find out first.
- Five models: Synchronous, Batched synchronous, Asynchronous, and the two through an executor.
iClaustron training for C developers · 8 modules · you are at 01 · this module for Rust developers