iClaustron training for C developers · module 7 of 8 · 20 min
Scans in the five programming models
How a scan runs on the threads and objects of modules 02 and 03, and how to read one row by row, a batch at a time, asynchronously and through an executor's reader.
How a scan runs · the objects
A scan is made once and run many times, each run with its receivers
ic_apid_scan_create makes a scan once in its connection: a table, a record, and the scan's own row, which each row read is unpacked into.
ic_apid_conn_start_scan starts a run. It takes a coordinator record from a pool per node; with none free it asks, and starts in a later poll.
A run has one receiver per fragment scanned at once, each bringing a batch of rows. IC_SCAN_BATCH_ID names a batch by an id of its own, which names nothing once the batch is handed back.
iclaustron: include/iclaustron/ic_apid.h (Scans; ic_apid_scan_create; IC_SCAN_IDLE … IC_SCAN_DONE; IC_SCAN_BATCH_ID) · crates/ic_apid/src/scan.rs (start_scan) · apid_conn.rs (take_scan_record, ask_scan_record, top_up_scan_pool)
How a scan runs · what each run asks for
IC_SCAN_ARGS: what a run asks for
| Field | Default | What it does |
|---|---|---|
| lock | IC_SCAN_LOCK_COMMITTED | SHARED, EXCLUSIVE: rows locked until their batch is handed back |
| parallelism | 0: every fragment | fragments scanned at once, one receiver each |
| batch_rows, batch_bytes | 0: from the configuration | a receiver's batch; at most 992 rows locking, 4 000 committed |
| partition | IC_NO_PARTITION | scan this partition only |
| timeout_ms | 0: no limit | close the run with IC_ERROR_TIMEOUT when it passes |
| callback, user_ref | NULL | each complete batch to the callback, not the ready list |
| read_ahead, continuous | false | committed reads: read ahead at the executor or in the data nodes |
| trans, ordered, descending | IC_NO_TRANS, false | taking rows over, index order: module 08 |
Fill it with ic_scan_args_init(&args, sizeof args), set what you want, and give it to each start of a run.
iclaustron: include/iclaustron/ic_apid.h (IC_SCAN_ARGS, ic_scan_args_init) · crates/ic_apid/src/scan.rs (ScanArgs, batch_size) · crates/ic_signals/src/scans.rs (IC_SCAN_MAX_LOCKED_ROWS, IC_SCAN_MAX_COMMITTED_ROWS)
How a scan runs · the signals
A run's signals: SCAN_REQ, rows, SCAN_CONF, SCAN_MORE_REQ
A run takes a coordinator record from a pool per node, then sends SCAN_REQ naming one receiver per fragment scanned at once.
The coordinator scans the fragments at once. Each row comes as a ROW_DATA_REP naming its receiver, which copies it into its batch.
SCAN_CONF gives each receiver's rows and words. A batch is complete once its entry and all its words have come, in either order.
The complete batch goes to the application: next_row, the ready list or a callback. A locking scan's rows stay locked meanwhile.
Handed back, its receiver is named in the scan's next SCAN_MORE_REQ, which goes with the next send or poll; that fragment goes on.
An entry naming the null record ends its receiver. With none left the run is done, and the record goes back to its pool.
iclaustron: doc/rust/02-architecture.md (Data flow for a scan) · doc/rust/05-ndb-protocol.md §7.1, §7.2 · crates/ic_apid/src/scan.rs (send_scan_req, take_scan_row, take_entry, check_batch, queue_more, coord_finished)
How a scan runs · flow control
The application sets the pace: a held batch holds its receiver
Each receiver has one batch out. It asks for the next only once the application hands that one back.
A batch held stalls only its own receiver: R2 asks for nothing meanwhile, and a shared or exclusive scan keeps those rows locked.
So memory stays within the receivers times the batch size. A batch held too long ends the scan with 296, IC_NDB_ERROR_SCAN_IDLE.
iclaustron: include/iclaustron/ic_apid.h (Scans; IC_SCAN_LOCK_SHARED, IC_NDB_ERROR_SCAN_IDLE) · crates/ic_apid/src/scan.rs (release_scan_batch, check_batch) · doc/rust/04-api-design.md (Scans: Batches, Why so)
Synchronous · the code
Synchronous scans: a run read row by row with next_row
EX_CHECK(ic_apid_scan_create(conn, table, rec, &scan));
ic_scan_args_init(&args, sizeof args);
EX_CHECK(ic_apid_conn_start_scan(conn, scan, &args));
for (;;) {
EX_CHECK(ic_apid_conn_next_row(conn, scan, &more));
if (!more)
break;
rows = ic_apid_scan_row(conn, scan, &len);
memcpy(&row, rows, sizeof row);
count++;
/* ... */
}
- The defaults: a committed read of every fragment.
next_rowhands each batch back once its rows are read, and polls while none is complete. ic_apid_scan_rowis the scan's own row, overwritten by each row;ic_apid_conn_next_row_intofills yours, a copy less.moreis false at the end; a failed run gives its error and drops the rows not read.
iclaustron: examples/c/scan.c (main, row by row; shortened) · include/iclaustron/ic_apid.h (ic_apid_conn_next_row, ic_apid_conn_next_row_into, ic_apid_scan_row)
Synchronous · threads and objects
Synchronous scans on the threads: next_row polls as it waits
Each ic_apid_conn_next_row unpacks the next row of the batch it holds into the scan's row: no wait while the batch lasts.
When that batch is used up, next_row hands it back itself, so that its receiver, R1, asks for more.
With no other batch of this scan complete, next_row polls. The poll first sends what is due, R1's SCAN_MORE_REQ, then waits.
The receive thread routes every reply for this thread to its inbox, and the poll runs them all: this scan's and the connection's other work.
So other queries complete and their callbacks run, and other scans' batches go on the ready list, there to wait for their reader.
next_row takes this scan's oldest complete batch, R2's, and returns its first row.
iclaustron: crates/ic_apid/src/scan.rs (next_row_with, wait_scan_batch, wait_for_scan, take_ready_batch_of, deliver) · apid_conn.rs (poll_with_requests) · doc/rust/02-architecture.md (Reading a scan)
Batched synchronous · the code
Batched synchronous scans: a batch at a time with next_batch
args.batch_rows = 16;
EX_CHECK(ic_apid_conn_start_scan(conn, scan, &args));
for (;;) {
EX_CHECK(ic_apid_conn_next_batch(conn, scan, &batch, &more));
if (!more)
break;
for (;;) {
EX_CHECK(ic_apid_conn_next_scan_row(conn, batch, &more));
if (!more)
break;
again++; /* ... */
}
EX_CHECK(ic_apid_conn_release_scan_batch(conn, batch));
}
next_batchwaits asnext_rowdoes, and gives a whole batch;ic_apid_scan_batch_rowssays how many rows.next_scan_rowunpacks the next row into the scan's row, or_intoyours.- The application holds the batch as long as it likes, several at once if it wants; its receiver waits for
release_scan_batch.
iclaustron: examples/c/scan.c (main, a batch at a time; shortened) · include/iclaustron/ic_apid.h (ic_apid_conn_next_batch, ic_apid_scan_batch_rows, ic_apid_conn_next_scan_row, ic_apid_conn_release_scan_batch)
Batched synchronous · threads and objects
Several scans on one connection, each read in turn
Two scans started on one connection run at once, each run with a coordinator record of its own.
next_batch on scan A polls, and the poll completes whatever comes: batches of A and of B alike.
It takes only A's batch. B's goes on the ready list and waits there; B's receiver asks for nothing meanwhile.
A's batch read and released: A's receiver is asked again with the next send or poll.
next_batch on B finds a batch already complete, and returns it without a wait.
Read every scan in turn: a batch left waiting holds its receiver and a locking scan's rows, and held too long ends the scan with 296.
iclaustron: crates/ic_apid/src/scan.rs (next_batch, wait_scan_batch, take_ready_batch_of, release_scan_batch) · doc/rust/04-api-design.md (Consuming scans) · doc/rust/06-phases.md §8.4
Asynchronous · the code
Asynchronous scans: after each poll, batches from the ready list
ic_scan_args_init(&args, sizeof args);
ret = ic_apid_conn_start_scan(conn, scan, &args);
ret = ic_apid_conn_send(conn, false);
while (!ic_apid_scan_is_done(conn, scan)) {
ret = ic_apid_conn_poll(conn, 100, NULL);
while (ic_apid_conn_get_next_scan_batch(conn, &batch)) {
while ((ret = ic_apid_conn_next_scan_row(conn, batch, &more)) == 0 &&
more) {
row = ic_apid_scan_row(conn, scan, &len);
}
ret = ic_apid_conn_release_scan_batch(conn, batch);
}
}
- The request goes with the next send or poll; each poll completes batches.
get_next_scan_batchtakes the batch completed longest ago, of any scan with no callback;ic_apid_scan_batch_scan(conn, batch)names its scan.- Read it, release it: that receiver goes on. Queries and other scans complete in the same polls.
iclaustron: doc/rust/04-api-design.md (Scans, the C example; comments left out) · include/iclaustron/ic_apid.h (ic_apid_conn_get_next_scan_batch, ic_apid_scan_batch_scan, ic_apid_conn_poll)
Asynchronous · a callback per batch
A scan callback: each batch handed over inside the poll
typedef void (*IC_SCAN_CALLBACK)(IC_APID_CONNECTION *conn,
IC_SCAN_BATCH_ID batch,
void *user_ref);
typedef struct {
/* ... */
/* Called with each batch as it completes, or NULL for the
ready list: an IC_SCAN_CALLBACK. */
void (*callback)(IC_APID_CONNECTION *conn,
IC_SCAN_BATCH_ID batch,
void *user_ref);
void *user_ref; /* Given back to the callback. */
/* ... */
} IC_SCAN_ARGS;
- Each complete batch of the run goes to the callback, on the thread that polls, inside the poll; never to the ready list.
- It may read the batch and release it there, or keep it and release it later.
next_rowandnext_batchrefuse a scan with a callback (IC_ERROR_NOT_SUPPORTED); inside a callback a poll does nothing.
iclaustron: include/iclaustron/ic_apid.h (IC_SCAN_CALLBACK, IC_SCAN_ARGS, ic_apid_conn_next_row) · crates/ic_apid/src/scan.rs (ScanCallback, deliver, wait_scan_batch) · apid_conn.rs (poll_with_requests)
Asynchronous · threads and objects
Asynchronous scans on the threads: one poll serves them all
One thread starts scan A with a callback and scan B without, and defines key queries besides.
One ic_apid_conn_send sends everything defined, one write per data node, the scans' requests with it.
ic_apid_conn_poll waits once for whatever comes; the receive thread has routed it all to this thread's inbox.
Inside the poll, A's complete batch is handed to A's callback, on this thread.
B's batch goes on the ready list, and the queries complete, their callbacks run too.
After the poll, get_next_scan_batch takes B's batch; read and released, B's receiver is asked again with the next send or poll.
iclaustron: crates/ic_apid/src/scan.rs (deliver, get_next_scan_batch, release_scan_batch) · crates/ic_apid/src/transaction.rs (send_queries) · doc/rust/06-phases.md §8.4 (Asynchronous)
Synchronous through an executor · the code
Scans through an executor: a shape, a reader, a submitter
/* made once, shared */
EX_CHECK(ic_scan_shape_create(table, rec, &shape));
/* on each thread, with no connection of its own */
EX_CHECK(ic_apid_executor_submitter(q->executor, &submitter));
EX_CHECK(ic_scan_reader_create(q->shape, &reader));
ic_scan_args_init(&args, sizeof args); /* ... */
EX_CHECK(ic_apid_submitter_scan(submitter, reader, &args));
for (;;) {
EX_CHECK(ic_scan_reader_next_row(reader, &more));
if (!more)
break;
rows = ic_scan_reader_row(reader, &len);
}
ic_scan_reader_free(reader);
ic_apid_submitter_scanreturns once the run is queued at the executor;ic_scan_reader_next_rowwaits for its rows.- Refused (
IC_ERROR_NOT_SUPPORTED): a transaction or a callback in the arguments, and a call on the executor's own thread. - A reader runs again and again;
_is_doneand_errorsay how its run ended.
iclaustron: examples/c/scan_executor.c (read_quarter; ranges and order left out) · doc/rust/04-api-design.md (Consuming scans: ic_scan_shape_create) · include/iclaustron/ic_apid.h (ic_apid_submitter_scan, ic_scan_reader_*)
Synchronous through an executor · threads and objects
The executor's feed: batches move to the reader without a copy
ic_apid_submitter_scan puts the run in the executor's inbox and returns. Scans wait in a queue of their own, 64 running at most by default.
The executor runs it as a scan of the shape on its own connection, kept from run to run. ic_scan_reader_next_row sleeps meanwhile.
Rows and SCAN_CONF come to the executor's inbox, and its poll completes the batch.
The batch moves to the reader's feed without a copy: the receiver's buffers swap places with an empty pair. The reader wakes and reads.
Moving on, the reader hands the batch back on the feed and nudges the executor: a word on a list of its own, one waiting per scan at most.
Served the nudge, the executor hands the batch back to its receiver, which asks for more. A slow reader stalls only its own scan.
iclaustron: crates/ic_apid/src/executor.rs (run, admit_scan, take_scan_batches, serve_nudge) · scan_reader.rs (module note; hand_back) · thread_conn.rs (Nudges) · scan.rs (swap_scan_batch)
Batched synchronous through an executor · threads and objects
Batched through an executor: several readers on one thread
One thread starts two readers. Each ic_apid_submitter_scan returns once its run is queued, so both run at once.
The executor runs both on its connection, each as a scan of its shape.
Each complete batch moves to its own reader's feed.
The thread reads A. Each batch it finishes goes back with a nudge, and A's receivers go on.
B's batches wait in B's feed, one per receiver, and B's receivers wait with them until B is read.
Then B is read the same way. In the scan_ tests, while one reader held a batch, five others on the same executor each read the whole table: 7 ms together.
iclaustron: crates/ic_apid/src/executor.rs (Submitter::scan, take_scan_batches) · scan_reader.rs (begin_run, next_row_with) · doc/rust/06-phases.md §8.4 (the scan_ integration group, 7 ms)
Batched synchronous through an executor · reading ahead
read_ahead: the next batch comes while the reader reads
By default a receiver asks for its next batch only when the reader hands the last one back, so the reader waits for that round trip.
With read_ahead the receiver goes on as soon as its batch has moved to the reader: two batches out at most, twice the memory.
Committed reads through an executor only: refused with a lock, with continuous, and on a connection, where you release batches yourself.
iclaustron: crates/ic_apid/src/executor.rs (take_scan_batches, serve_nudge, AheadReceiver, IC_READ_AHEAD_BATCHES) · scan.rs (start_scan) · include/iclaustron/ic_apid.h (IC_SCAN_ARGS read_ahead, continuous; ic_apid_submitter_scan) · doc/user-guide.md §5 (Scans)
All five models · closing early
Closing a run early: the coordinator stops at once
ic_apid_conn_close_scan ends a run before it reads to the end; the batches the application holds are let go of.
The stop goes at once in a SCAN_MORE_REQ, without waiting for the batches asked for; pending takeovers go first (module 08).
Rows still on their way name a receiver of no live run: dropped, and counted as late.
The bare end of data confirms the close, in a later poll: the record goes back to its pool, and the scan can run again.
Through an executor, ic_scan_reader_close does the same and waits for the executor to finish; ic_scan_reader_free does not wait.
iclaustron: include/iclaustron/ic_apid.h (ic_apid_conn_close_scan, ic_scan_reader_close, ic_scan_reader_free) · crates/ic_apid/src/scan.rs (close_scan, hold_batches, late_scan_signal, take_scan_conf) · scan_reader.rs (close)
All five models · when a run fails
Scan errors are temporary: start the run again
| Code | Name | What happened |
|---|---|---|
| 296 | IC_NDB_ERROR_SCAN_IDLE | the run was left too long without asking for more: a batch not handed back |
| 274 | IC_NDB_ERROR_SCAN_LOCK_TIMEOUT | the scan waited too long for a lock, probably in a deadlock |
| 266 | IC_NDB_ERROR_LOCK_TIMEOUT | a lock wait timed out, probably in a deadlock |
| 4028 | IC_NDB_ERROR_SCAN_NODE_FAILURE | the coordinator's node failed, and the run's record with it |
| 4012 | IC_NDB_ERROR_NO_REPLY | the coordinator, its link up, sent nothing for six minutes |
All temporary (ic_error_is_temporary). Nothing resumes a run: start it again, and its rows come again from the start.
iclaustron: include/iclaustron/ic_apid.h (IC_NDB_ERROR_*, ic_error_is_temporary) · crates/ic_port/src/ndb_err.rs (classes) · crates/ic_apid/src/scan.rs (fail_lost_scans, expire_scans, IC_SCAN_ANSWER_WAIT_MS) · doc/user-guide.md §5, §9
Summary · scans in the five programming models
Scans in the five models, side by side
| Model | Calls | Who polls | Who holds a batch | Memory | Use it for |
|---|---|---|---|---|---|
| Synchronous | next_row | next_row | the library | a batch per receiver | one scan at a time, simplest |
| Batched synchronous | next_batch, next_scan_row, release_scan_batch | next_batch | the application | a batch per receiver | whole batches; several scans in turn |
| Asynchronous | poll, then get_next_scan_batch or a callback | the application | the application | a batch per receiver | scans and queries in flight |
| Synchronous through an executor | submitter_scan, reader_next_row | the executor | the reader | a batch per receiver | threads with no connection |
| Batched synchronous through an executor | several readers, read_ahead | the executor | the readers | two per receiver with read_ahead | several scans per thread |
Calls without their ic_apid_conn_, ic_apid_ and ic_scan_ prefixes. Every model runs the same signals.
iclaustron: include/iclaustron/ic_apid.h (Scans) · doc/rust/04-api-design.md (Consuming scans) · crates/ic_apid/src/scan.rs, scan_reader.rs, executor.rs
Recap · module 07
Scans in five models: what to take away
- A scan is made once and run many times; a run takes a pooled coordinator record and one receiver per fragment.
- A receiver asks for its next batch only once the last is handed back: the application sets the pace, and locks last until then.
- On a connection:
next_rowrow by row,next_batcha batch at a time, or batches after a poll, from the ready list or a callback. - Through an executor: a shape, a reader,
ic_apid_submitter_scan; batches move without a copy, and a slow reader stalls only its own scan. read_aheadlets a committed read fetch ahead at twice the memory.- Scan errors are temporary: start the run again.
iClaustron training for C developers · 8 modules · you are at 07 · this module for Rust developers