iClaustron training for C developers · module 3 of 8 · 20 min
Object architecture
The objects a program makes, the ones the library keeps for it, which thread owns each, and how a reply from a data node finds its object.
The map · ownership as nesting
The object map: global, connection, executor
The global is shared by every thread of the process: one per cluster, with its per-node state, its thread table, its dictionary cache and its threads.
Each user thread makes a connection from the global: its own block number, inbox and objects, used by one thread at a time, with no lock on its objects.
An executor is a thread of the library's with a connection of its own; application threads reach it through submitters.
Table and index definitions, records and programs are handles: made once, used by any thread. A definition or a record may outlive the connection that bound it.
iclaustron: crates/ic_apid/src/apid_global.rs (ApidShared, ApidGlobal::start_with_options); apid_conn.rs (ApidConnection); executor.rs (Runner, Submitter); include/iclaustron/ic_apid.h (Handles, Threads)
GlobalConnectionExecutorHandles
Global · IC_APID_GLOBAL
The global: one per cluster per process, shared by every thread
IC_APID_GLOBAL * any thread may use itic_apid_global_connectic_apid_global_wait_for_startedic_apid_global_create_connectionic_apid_executor_startic_apid_global_free after its connections and executors- One per cluster: a program that talks to two clusters starts two.
- One NodeShared per data node, fixed at start. Each node is dealt to one receive thread; never more receive threads than nodes.
iclaustron: crates/ic_apid/src/apid_global.rs (NodeShared, Sender, ApidShared, ApidGlobal::start_with_options); thread_conn.rs (ThreadTable); auto_increment.rs (CounterSlot); include/iclaustron/ic_apid.h (ic_apid_global_connect, ic_apid_global_free); doc/user-guide.md §2
GlobalConnectionExecutorHandles
Global · made first, freed last
Lifetime: the global is made first and freed last
ic_global_options_init(&options, sizeof options); ic_apid_global_connect(connect_string, name, &options, &global); ic_apid_global_create_connection(global, &conn); ic_apid_conn_table_bind(conn, EX_DB, EX_TABLE, &table); rec = ex_t1_record(table, 0); /* ic_record_create */ ic_apid_executor_start(global, NULL, &executor); ic_apid_executor_submitter(executor, &submitter); /* ... */ ic_apid_submitter_free(submitter); ic_apid_executor_free(executor); ic_record_free(rec); ic_table_def_free(table); ic_apid_conn_free(conn); ic_apid_global_free(global);
- Free in the reverse order: submitters and executors, then connections, then the global.
ic_apid_global_freestops every thread of the library and waits for each; no other thread may use the global meanwhile.ic_apid_conn_freefirst waits for the answers to the coordinator records it asked for: well under a millisecond as a rule, 5 s at most. A transaction left open does not commit.- Table, index and record handles may outlive the connection that bound them.
iclaustron: examples/c/t1.h (ex_connect); doc/user-guide.md §2; examples/c/scan_executor.c (main; EX_CHECK left out, the scan shape, index and val_rec lines elided); include/iclaustron/ic_apid.h (Handles; ic_apid_global_free, ic_apid_conn_free)
GlobalConnectionExecutorHandles
Connection · IC_APID_CONNECTION
A connection is one user thread's view of the cluster
IC_APID_CONNECTION * one thread at a timeic_apid_global_create_connection from any threadic_apid_conn_* call, and the ic_apid_query_*, ic_apid_transaction_*, ic_apid_scan_* calls on its idsic_apid_conn_free its queries, transactions and scans with it- It may change threads between calls; only one thread uses it at a time.
- At most 4 711 per process, executors' own included (7139).
iclaustron: crates/ic_apid/src/apid_conn.rs (ApidConnection, CoordRecord, Expectations); thread_conn.rs (ThreadConnection::block_number, ThreadTable::allocate); include/iclaustron/ic_apid.h (Threads; ic_apid_global_create_connection, ic_apid_conn_free); doc/user-guide.md §2
GlobalConnectionExecutorHandles
Connection · ids, not pointers
Queries, transactions and scans are named by generational uint64_t ids
An id is a place in the connection's object map with a generation over it. No pointer crosses the wire, and a valid id is never 0.
A freed query's place is reused under the next generation, and a place whose generation would wrap is retired: every call refuses or passes over the old id, never follows it; a late reply naming it is dropped.
iclaustron: crates/ic_util/src/ptr_array.rs (PtrId, wire_word, PtrArray::insert, remove, id_of_wire); crates/ic_apid/src/scan.rs (IC_RECEIVER_TAG, fits_below_tag, ScanBatchId, deliver); include/iclaustron/ic_apid.h (Ids)
GlobalConnectionExecutorHandles
Handles · table and index definitions
Table and index definitions are bound once and shared through a cache
IC_TABLE_DEF *IC_INDEX_DEF * any threadic_apid_conn_table_bindic_apid_conn_index_bindic_apid_submitter_table_bindic_table_def_field_idic_table_def_field into IC_FIELD_INFOic_index_def_tableic_table_def_freeic_index_def_free; ic_apid_conn_table_unbind lets the connection's goThe first bind fetches the table's description from a data node, once for every connection of the global, and keeps it in the cache by name.
Every later bind, from any thread, finds it there and shares the same definition: no round trip.
When a data node says the table changed, or a query fails with a schema error, the definition is let go of: bind again and retry.
iclaustron: crates/ic_apid/src/dict_cache.rs (DictCache, TableDef, IndexDef); rec_thread.rs (TABLE_CHANGED_REP); apid_conn.rs (table_changed); transaction.rs (schema_error); include/iclaustron/ic_apid.h (ic_apid_conn_table_bind, ic_apid_conn_table_unbind, ic_table_def_field); doc/user-guide.md §3
GlobalConnectionExecutorHandles
Handles · records
A record lays a row out over a C struct: offset and null bit per field
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;
};
IC_FIELD_SPEC spec[4] = {
{ id_of_id, offsetof(struct t1_row, id), IC_NO_NULL_BIT, 0 },
{ id_of_val, offsetof(struct t1_row, val),
offsetof(struct t1_row, null_bits), 0 },
/* ... code, name ... */
};
ic_record_create(table, spec, 4, sizeof(struct t1_row), &rec);
IC_RECORD *IC_FIELD_SPECic_record_createic_record_create_defaultic_record_fieldic_record_row_sizeic_record_set_nullic_record_is_nullic_record_free a query keeps its own copyic_record_create_default(table, NULL, 0, &rec)lays the row out for you;ic_record_fieldsays where each field went.- Checked once, when made: every field there, nothing overlapping. Any thread may use it.
iclaustron: doc/user-guide.md §3 (the C record; ret = left out); examples/c/t1.h (struct t1_row, ex_t1_record); crates/ic_apid/src/record.rs (Record::new); include/iclaustron/ic_apid.h (ic_record_create, ic_record_create_default, ic_record_free)
GlobalConnectionExecutorHandles
Handles · values in a row
A row holds each value as the data nodes keep it: nothing is converted
| Type | In the row |
|---|---|
| TINYINT, SMALLINT, MEDIUMINT, INT, BIGINT | 1, 2, 3, 4, 8 bytes, little-endian, signed or not |
| FLOAT, DOUBLE | 4, 8 bytes, IEEE, little-endian |
| CHAR(n), BINARY(n) | the field's bytes, padded: CHAR with the character set's spaces, BINARY with zeros |
| VARCHAR, VARBINARY | one length byte up to 255 bytes, else two, little-endian; then the bytes |
| DECIMAL | MySQL's packed binary decimal |
| DATE, TIME, DATETIME, TIMESTAMP, YEAR | as the data nodes keep them, fractional forms included |
| BIT(n) | n bits, in 32-bit words |
| NULL | a null bit, set for NULL; a NOT NULL field has none |
A field takes the room of its longest value with its length bytes: VARCHAR(32) is 33 bytes in latin1, 129 in utf8mb4. Blob columns stay out of records.
iclaustron: doc/user-guide.md §3 (How values are kept; Blob columns); crates/ic_apid/src/record.rs (module notes)
GlobalConnectionExecutorHandles
Connection · queries
A query is made once, owns its key and attribute rows, and is reused
/* once, for each slot of the pipeline */ ic_apid_query_create(conn, p.table, p.rec, p.rec, &slots[i].query); /* each write: the query owns its rows; the struct is copied in */ key = ic_apid_query_key_row(conn, s->query, &key_len); attr = ic_apid_query_attr_row(conn, s->query, &attr_len); memcpy(key, &row, sizeof row); memcpy(attr, &row, sizeof row); /* ... start a transaction, define the write, commit ... */ /* at the end */ ic_apid_query_free(conn, slots[i].query);
IC_QUERY_ID a uint64_t in its connectionic_apid_query_createic_apid_query_create_uniqueic_apid_query_key_rowic_apid_query_attr_rowic_apid_conn_read_keyic_apid_conn_write_keyic_apid_query_erroric_apid_query_free when idle or completed- Two records: the key record holds every primary key field, or every indexed field; one record over every field serves as both, as
p.rechere.
iclaustron: examples/c/pipeline.c (main, start_write; EX_CHECK and the NULL check left out); crates/ic_apid/src/query.rs (ApidQuery, check_records); include/iclaustron/ic_apid.h (Rows; ic_apid_query_create, ic_apid_query_free); doc/user-guide.md §3, §4
GlobalConnectionExecutorHandles
Connection · a query's life
A query goes Idle, Defined, Sent, Completed, and back to Idle
A query starts idle: the program fills its key row, and for a write its attribute row, through the pointers the query gives.
ic_apid_conn_read_key or _write_key defines it on a transaction: KEY_OP_REQ is packed at once into its node's staged buffer.
ic_apid_conn_send, or a flush, writes every node's staged requests, one write per data node: the query is sent.
In a poll, KEY_OP_CONF and the row's ROW_DATA_REP, or a KEY_OP_REF, complete it: its row is in the attribute row, or its error is set.
Taken from the executed list, or once its callback returns, it is idle and can be defined again. From define to completion its rows are the library's.
iclaustron: crates/ic_apid/src/query.rs (QueryState, Execution); transaction.rs (read_key, take_row, get_next_executed_query); include/iclaustron/ic_apid.h (IC_QUERY_*, Rows); doc/user-guide.md §4, §8
GlobalConnectionExecutorHandles
Connection · transactions
A transaction is local until its first send; the hint picks its coordinator
IC_TRANS_IDIC_TRANSACTION_HINTic_apid_conn_start_transactionic_apid_conn_start_transaction_on_indexic_apid_conn_hint_for_keyic_apid_conn_commit_transactionic_apid_conn_rollback_transactionic_apid_transaction_is_doneic_apid_conn_close_transaction once doneic_apid_conn_hint_for_key hashes the key as the data nodes do, character columns in their collation, and names the partition that holds the row.
Starting takes a coordinator record at a started node holding that partition: a free one of this connection's, or a new one, a round trip.
The transaction holds its 64-bit transaction id, its coordinator record, and its defined and sent queries. The data node knows nothing yet.
Its first KEY_OP_REQ carries the start flag: only then does the coordinator hear of it. A hint only chooses a node; any row may be touched.
iclaustron: crates/ic_apid/src/transaction.rs (Transaction, start_transaction, choose_start_node, hint_for_key); apid_conn.rs (coord_record, next_transaction_id); include/iclaustron/ic_apid.h (ic_apid_conn_hint_for_key, ic_apid_conn_start_transaction); doc/user-guide.md §4
GlobalConnectionExecutorHandles
Connection · ending a transaction
A transaction is done when it has ended and every query has completed
ic_apid_transaction_state one of IC_COMMIT_STATE_*ic_apid_transaction_erroric_apid_transaction_error_dataic_apid_transaction_gci the global checkpoint of a commitic_apid_conn_close_transaction its query lists kept for the nextA started transaction takes queries. Commit or rollback is asked for, and goes with the next send, on its last query or alone.
The coordinator commits it and says so, in KEY_OP_CONF or COMMIT_CONF, with the global checkpoint the commit went into.
A rollback asked for, a failed write that aborts on error, or the coordinator's TRANS_ABORTED_REP ends it rolled back; its error says why.
Done is ended with every query completed: its coordinator record goes back to the free list. Close it then.
iclaustron: crates/ic_apid/src/transaction.rs (CommitState, Transaction::is_done, take_key_conf, end_transaction, release_if_done, close_transaction); include/iclaustron/ic_apid.h (IC_COMMIT_STATE_*, ic_apid_transaction_is_done); doc/user-guide.md §4
GlobalConnectionExecutorHandles
Connection · scans
A scan holds its row and, while it runs, a receiver per fragment
IC_SCAN_IDIC_SCAN_BATCH_IDic_apid_scan_createic_apid_index_scan_createic_apid_conn_start_scanic_apid_conn_next_rowic_apid_conn_next_batchic_apid_conn_release_scan_batchic_apid_scan_free not while it runsA scan is made once over a table and a record, and holds the row read last. Each run takes a coordinator record of its own.
While a run lasts it has one receiver per fragment scanned at once. A receiver's id has bit 23 set, which no query's id has.
A receiver brings a batch at a time, to the ready list or a callback; handed back, it asks for more. Module 07 teaches the use.
iclaustron: crates/ic_apid/src/scan.rs (Scan, Receiver, ScanState, ScanBatchId, IC_RECEIVER_TAG, deliver); apid_conn.rs (scan_pools, receivers, batches, scan_of_record); include/iclaustron/ic_apid.h (Scans; ic_apid_scan_create, ic_apid_scan_free); doc/rust/02-architecture.md (Data flow for a scan)
GlobalConnectionExecutorHandles
Executor · executors and submitters
An executor runs other threads' requests on a connection of its own
IC_APID_EXECUTOR *IC_APID_SUBMITTER *ic_apid_executor_startic_apid_executor_submitter one per threadic_apid_submitter_table_bindic_apid_submitter_executeic_apid_submitter_scanic_apid_executor_statsic_apid_submitter_freeic_apid_executor_free before the globalAny number of application threads, each with its own submitter, put requests into the executor's inbox, beside the signal pages of the receive threads.
The executor runs each request as a transaction on its connection, with queries kept per shape, and leaves the result in the submitter's slot.
iclaustron: crates/ic_apid/src/executor.rs (Executor::start, Submitter, Slot, Runner, Pool, ScanPool, IC_EXECUTOR_QUEUE); thread_conn.rs (Inbox); include/iclaustron/ic_apid.h (Executors; ic_apid_executor_start, ic_apid_executor_free, ic_apid_executor_submitter)
GlobalConnectionExecutorHandles
Executor · shapes, requests, readers
Shapes describe an operation once; a request carries one transaction
IC_REQUEST *req; uint32_t op; size_t len; ic_request_create(&req); ic_request_add_read(req, shape, IC_READ_KIND_COMMITTED, &op); memcpy(ic_request_key_row(req, op, &len), &key, sizeof key); if ((ret = ic_apid_submitter_execute(sub, req)) != 0) goto error; ret = ic_request_op_error(req, op); if (ret == IC_NDB_ERROR_NO_SUCH_ROW) /* no such row */; else if (ret != 0) goto error; else memcpy(&row, ic_request_attr_row(req, op, &len), sizeof row);
IC_OP_SHAPE *ic_op_shape_createic_op_shape_create_uniqueic_op_shape_freeIC_REQUEST *ic_request_createic_request_add_readic_request_add_writeic_request_freeIC_SCAN_SHAPE *ic_scan_shape_createic_scan_shape_create_indexic_scan_shape_freeIC_SCAN_READER *ic_scan_reader_createic_scan_reader_next_rowic_scan_reader_free- A shape is made once and shared by every thread; a request owns its rows and is run again with new keys. Module 06 runs them, module 07 the readers.
iclaustron: doc/user-guide.md §5 (Through an executor, the C request); crates/ic_apid/src/executor.rs (OpShape, KeyOp, Request); scan_reader.rs (ScanShape, ScanReader); include/iclaustron/ic_apid.h (ic_op_shape_create, ic_request_create, ic_scan_shape_create, ic_scan_reader_create)
GlobalConnectionExecutorHandles
Handles · programs, run in the data node
A program is made once over a table and run by any number of operations
ic_program_builder_create(table, &b);
same = ic_program_compare(b, IC_CMP_EQ, ic_program_column(b, val),
ic_program_input(b, 0));
ic_program_builder_finish(b, same, &still);
ic_program_builder_free(b);
ic_apid_query_create(conn, table, rec, name_rec, &guarded);
ic_apid_query_set_program(conn, guarded, still);
/* ... */
ic_apid_query_set_input(conn, guarded, 0, 0);
/* ... define the update, commit, wait ... */
ic_apid_query_set_input(conn, guarded, 0, 5);
/* ... */
ic_program_free(still);
IC_PROGRAM *IC_PROGRAM_BUILDER *ic_program_builder_createic_program_builder_finishic_program_builder_finish_no_conditionic_apid_query_set_programic_apid_conn_set_scan_programic_op_shape_with_programic_scan_shape_with_programic_program_builder_freeic_program_free what runs it keeps it- Only the inputs change from one operation to the next: up to 16 integers each. Module 08 uses programs.
iclaustron: examples/c/conditional_update.c (main, part 1; EX_CHECK left out); crates/ic_apid/src/program.rs (Program, ProgramBuilder); include/iclaustron/ic_apid.h (Programs; IC_PROGRAM_INPUTS; ic_program_builder_finish, ic_program_free); doc/user-guide.md §6
GlobalConnectionExecutorHandles
Replies · finding the object
A reply finds its object: the block number, then one index
Each request names our block, 0x8000 plus the thread id, as the address for its reply; the reply's first word is a number of ours.
A receive thread reads the block number only, and posts the signal to that thread's inbox, a page per round. The user thread decodes it.
In its poll, a single request's reply, such as COORD_ALLOC_CONF, matches an expected reply: its request number, signal and node.
KEY_OP_CONF, COMMIT_CONF and ROLLBACK_CONF carry our pointer for the coordinator record: its place indexes active[], which holds the transaction.
ROW_DATA_REP and KEY_OP_REF name the query by its id; a scan's rows name a receiver, bit 23 set. Each is one index, no search.
iclaustron: crates/ic_apid/src/thread_conn.rs (Router, ThreadConnection::block_number); apid_conn.rs (receive_view, Expectations::offer, coord_index_of); transaction.rs (route_reply, trans_named, take_row); scan.rs (scan_named, receiver_named); doc/rust/architecture.html §2
GlobalConnectionExecutorHandles
Replies · the bits
Two numbers in a reply: our record pointer and the transaction id
A late reply, left from an earlier use of the same place, carries another transaction id: it matches nothing and is dropped, and counted.
iclaustron: crates/ic_apid/src/apid_conn.rs (IC_COORD_PTR_TAG, coord_ptr_of, coord_index_of, transaction_id, trans_count_after, next_transaction_id, note_stray); thread_conn.rs (ThreadTable::trans_counter); transaction.rs (trans_named); doc/rust/architecture.html §2
Memory · once warm
Memory: once warm, nothing is allocated per transaction or per signal
Queries own their rows
Made with the query, sized by its records. From define to completion the library holds no pointer into the program's memory; C gets pointers into the query's own rows.
Kept, not freed
A closed transaction's query lists, up to 8 192 spare; coordinator records on free lists; send buffers swapped with a spare; signal pages at their largest size.
Copies at the edges
The synchronous calls copy the caller's rows in and out. An executor copies a request's rows into its query and back. A batch moves to a reader without a copy.
On the define, send and poll path, a thread with hundreds of transactions in flight starts each in constant time and allocates nothing for it.
iclaustron: doc/rust/architecture.html §2 (Memory rules); crates/ic_apid/src/transaction.rs (IC_SPARE_QUERY_LISTS, close_transaction); apid_conn.rs (coord_record); apid_global.rs (Sender, take_waiting); signal_page.rs; executor.rs (admit, completion copies); scan_reader.rs (module notes); include/iclaustron/ic_apid.h (Rows)
Summary · objects by model
Objects in the five programming models
| Model | The program makes | The library uses for it | Owned by |
|---|---|---|---|
| Synchronous | a connection, definitions, records; a scan | per key call a coordinator record and an expected reply; a scan's receivers | the calling thread |
| Batched synchronous | queries, transactions; scans and their batches | coordinator records, requests staged per node, receivers | the calling thread |
| Asynchronous | queries with a callback and user_ref, transactions; scans | the executed list and the ready list; callbacks run inside the poll | the thread that polls |
| Synchronous through an executor | an executor, a submitter per thread, op shapes, a request; scan shapes, readers | the executor's connection: a kept query per operation, a transaction per request, a kept scan | the executor's thread; each caller its submitter, request, reader |
| Batched synchronous through an executor | a request of many operations; several readers | the same, with one send per data node for every caller's requests | the same |
Modules 04 to 08 teach each model; this table says which objects each one stands on.
iclaustron: crates/ic_apid/src/key_op.rs (read_committed, write_key, run_op, read_unique_committed); transaction.rs; scan.rs; executor.rs (Runner, Flight, Pool, ScanPool); scan_reader.rs (Feed); include/iclaustron/ic_apid.h (Executors, Scans); doc/user-guide.md §5
Recap · module 03
Object architecture: what to take away
- One global per cluster, shared by every thread and freed last.
- A connection per user thread holds its queries, transactions and scans, named by generational uint64_t ids.
- Table and index definitions, records and programs are handles any thread may use.
- A query owns its rows; a transaction is local until its first send, and its hint picks the coordinator.
- An executor runs other threads' requests on its own connection: submitters, shapes, requests, readers.
- A reply finds its object by the block number, then one index: no search, no lock.
iClaustron training for C developers · 8 modules · you are at 03 · this module for Rust developers