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

application process your codethe application's threads call the C API iClaustron · API nodelibiclaustron, linked in,with threads of its own (module 02) management servernode ids, configuration · port 1186 data node 1coordinator · the rows of its fragments data node 2coordinator · the rows of its fragments node id, configuration NDB protocol: KEY_OP_REQ out; KEY_OP_CONF, ROW_DATA_REP back

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

C, C++, others Rust programs ic_capithe C ABI: libiclaustron, shared and static, and the header ic_apid.h ic_apidthe Data API: connections, queries, transactions, scans, executors, programs ic_apicconfiguration client, management server ic_signalsNDB signals: numbers, blocks, framing ic_commsockets, poll sets, buffer pages ic_protocolbase64, management protocol words ic_utilcontainers, bitmaps, arrays, strings, a thread pool ic_portportability: the operating system, locks, time, errors, tracing

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 _init writes 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: a uint64_t in their connection, never 0; a scan's batch has an IC_SCAN_BATCH_ID of 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_default lays 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=N asks for that node id.
  • connect fetches a node id and the configuration, starts the library's threads and returns; data nodes link in the background.
  • wait_for_started waits up to 30 s here, for every data node (all is 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_OPTIONSDefaultMeaning
receive_threads1threads reading the data nodes' links; one per data node at most
reply_wait_ms0: 6 minhow long a transaction waits for replies before it ends with 4012
mgm_timeout_ms30 000how long a management server has to answer
auto_increment_prefetch4 096values 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_bind fetches 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_write is 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: found is 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 · modulesThe caller writesThreadsIn flightBatchingReads a second
Synchronous04 · 07one blocking call per query, on its own rowsthe caller, with its connection1 per threadnone: a send per call198 000 · 64 threads
Batched synchronous04 · 07queries on transactions, then flush until donethe caller, with its connectiona batch per threadone send per data node for the batch1.17–1.35 M · 1 thread, batch 200
Asynchronous05 · 07queries with callbacks; send, then pollthe caller, with its connectionmany transactions per threadall defined since the last send2.6 M · 1 thread, depth 4
Synchronous through an executor06 · 07a request of one operation, executedthe callers, no connection; the executor1 per calleracross callers, by the executor268 000 · 64 callers, 1 executor
Batched synchronous through an executor06 · 07a request of many operations: one transactionthe callers, no connection; the executora request per callerwithin the request and across callersnot 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

#ModuleTeaches
01Overviewthe library, its crates, the C conventions, connecting, errors, the five models
02Thread architectureuser, receive, send pool, heartbeat, connect and executor threads; node failure
03Object architectureglobal, connection, definitions, records, queries, transactions, scans, shapes, requests
04Key operations, synchronousSynchronous and Batched synchronous, on a connection
05Key operations, asynchronousAsynchronous: send, poll, callbacks, pipelines
06Key operations through an executorSynchronous and Batched synchronous through an executor
07Scansscans in the five models: row by row, a batch at a time, asynchronously, through a reader
08Advanced scansindex 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.

Next · 02 · Thread architecture