CUBRID CDC (Change Data Capture) — Code-Level Deep Dive
Where this document fits: The high-level analysis
cubrid-cdc.mdcovers design intent and theoretical background. This document traces every branch and field at the code level. Each chapter is self-contained, but reading in order follows the full lifecycle of a captured change — from the transaction-log record, through the log reader, to a change handed to a CDC client.
Contents:
Chapter 1: Data Structure Map
Section titled “Chapter 1: Data Structure Map”This chapter answers: what structs and enums hold CDC state, and how do they point at each other from server producer to client log item? State lives in two address spaces sharing no struct, talking over the network: the server (src/transaction/log_impl.h, global cdc_Gl of type CDC_GLOBAL) and a libcubridcs client (src/api/cubrid_log.h, a CUBRID_LOG_ITEM list). Producer/consumer model: see cubrid-cdc.md. SUPPLEMENT_REC_TYPE (Ch 2’s input) is only named. Code is verbatim; annotations live in the tables.
1.1 The server panorama: cdc_Gl
Section titled “1.1 The server panorama: cdc_Gl”extern CDC_GLOBAL cdc_Gl; holds a client connection, the two worker contexts, and the lock-free queue between them.
// cdc_global -- src/transaction/log_impl.htypedef struct cdc_global{ css_conn_entry conn; CDC_PRODUCER producer; CDC_CONSUMER consumer; /* *INDENT-OFF* */ lockfree::circular_queue<CDC_LOGINFO_ENTRY *> *loginfo_queue; /* *INDENT-ON* */ LOG_LSA first_loginfo_queue_lsa; LOG_LSA last_loginfo_queue_lsa; bool is_queue_reinitialized;} CDC_GLOBAL;The queue holds pointers: producer heap-allocates and enqueues, consumer dequeues and frees.
| Field | Role / Why |
|---|---|
conn | css_conn_entry, the one CDC client; consumer sends through it. |
producer | Producer-owned log-reader context (1.2). |
consumer | Consumer-owned request/response context (1.3). |
loginfo_queue | Pointer to SPSC ring of CDC_LOGINFO_ENTRY *; reinitializable alone. |
first_loginfo_queue_lsa | LSA of oldest buffered entry; “buffered?” test. |
last_loginfo_queue_lsa | LSA of newest pushed; producer high-water mark. |
is_queue_reinitialized | Queue-rebuilt flag; re-seed from start_lsa. |
Invariant — the LSA window brackets the live queue. Every entry’s next_lsa lies in [first_loginfo_queue_lsa, last_loginfo_queue_lsa]: producer bumps the upper bound on push, consumer the lower on pop. If violated, Ch 4’s “buffered?” test mis-fires; is_queue_reinitialized is the escape hatch.
flowchart LR
subgraph server["Server — cdc_Gl (CDC_GLOBAL)"]
P["CDC_PRODUCER<br/>producer"]
C["CDC_CONSUMER<br/>consumer"]
Q["loginfo_queue<br/>circular_queue<CDC_LOGINFO_ENTRY*>"]
CONN["css_conn_entry<br/>conn"]
P -->|push entry*| Q
Q -->|pop entry*| C
C -->|pack + send| CONN
end
subgraph client["Client — libcubridcs"]
LI["CUBRID_LOG_ITEM list"]
end
CONN -.->|network wire| LI
Figure 1-1. Producer-queue-consumer-network-client panorama. The queue carries pointers; the wire carries bytes.
1.2 CDC_PRODUCER — the log-reader context
Section titled “1.2 CDC_PRODUCER — the log-reader context”// cdc_producer -- src/transaction/log_impl.htypedef struct cdc_producer{ LOG_LSA next_extraction_lsa; int all_in_cond; /* configuration */ int num_extraction_user; char **extraction_user; int num_extraction_class; UINT64 *extraction_classoids; volatile CDC_PRODUCER_STATE state; volatile CDC_PRODUCER_REQUEST request; int produced_queue_size; pthread_mutex_t lock; pthread_cond_t wait_cond; CDC_TEMP_LOGBUF temp_logbuf[2];/* *INDENT-OFF* */ std::unordered_map <TRANID, char *> tran_user; /*to clear when log producer ends suddenly */ std::unordered_map<TRANID, int > tran_ignore; /* *INDENT-ON* */} CDC_PRODUCER;| Field | Role / Why |
|---|---|
next_extraction_lsa | Read cursor; next record to decode (Ch 5). |
all_in_cond | Config: full old-image in “cond” — every column, not just key (Ch 7). |
num_extraction_user / extraction_user | Count + names; user filter, empty = none. |
num_extraction_class / extraction_classoids | Count + class OIDs; table filter, empty = all. |
state | volatile CDC_PRODUCER_STATE; publishes WAIT/RUN/DEAD. |
request | volatile CDC_PRODUCER_REQUEST; consumer pauses/kills. |
produced_queue_size | Outstanding queued bytes; bumped on push, periodically reduced by consumed_queue_size; backpressure gauge (1.10). |
lock | Mutex over state/request hand-off. |
wait_cond | Condvar for zero-CPU WAIT sleep. |
temp_logbuf[2] | Two scratch buffers (1.5); a record may straddle a page. |
tran_user | map<TRANID,char*> committing user; filter / user field, cleared on abrupt end. |
tran_ignore | map<TRANID,int> ignore flags; filtered transactions skip cheaply. |
Invariant — state is what-IS, request is what-is-WANTED. Producer writes only state, consumer only request, both volatile; write the field you do not own and they deadlock (producer in STATE_WAIT while request says RUN; Ch 3). The one nested struct is temp_logbuf[2]; maps key on TRANID.
1.3 CDC_CONSUMER — the request-serving context
Section titled “1.3 CDC_CONSUMER — the request-serving context”// cdc_consumer -- src/transaction/log_impl.htypedef struct cdc_consumer{ int extraction_timeout; int max_log_item; char *log_info; /* log info list. it is used as buffer to send to client */ int log_info_size; /* total length of data in log_info */ int log_info_buf_size; /* size of buffer for log_info */ int num_log_info; /* how many log info is stored in log_infos (log info list) */ int consumed_queue_size; volatile CDC_CONSUMER_REQUEST request; LOG_LSA start_lsa; /* first LSA of log info that should be sent */ LOG_LSA next_lsa; /* next LSA to be sent to client */} CDC_CONSUMER;| Field | Role / Why |
|---|---|
extraction_timeout | Wait seconds for fill; on expiry returns ..._EXTRACTION_TIMEOUT. |
max_log_item | Cap on items per extract. |
log_info | Heap buffer of packed wire bytes; staging before send. |
log_info_size | Valid bytes; send length, <= log_info_buf_size. |
log_info_buf_size | Allocated capacity; grown on overflow. |
num_log_info | Count of packed entries; gated by max_log_item. |
consumed_queue_size | Bytes popped since the last reconcile; subtracted from produced_queue_size then reset to 0. |
request | volatile CDC_CONSUMER_REQUEST; RUN/WAIT/NONE. |
start_lsa | First LSA wanted; from client LSA (Ch 4). |
next_lsa | LSA to request next; client resume cursor. |
Invariant — the output triplet is self-consistent. 0 <= log_info_size <= log_info_buf_size and num_log_info matches the entries in those bytes; Ch 7 bumps size and count together. Overshoot overflows the heap; count/bytes mismatch walks the Ch 8 decoder off the end.
Invariant — next_lsa is monotonic non-decreasing across calls. Each extract advances it past everything returned and seeds the next start_lsa; backward motion re-delivers consumed changes. Sole exception: CUBRID_LOG_SUCCESS_WITH_ADJUSTED_LSA (Ch 4) snaps forward to the nearest valid record.
1.4 CDC_LOGINFO_ENTRY — one queued change
Section titled “1.4 CDC_LOGINFO_ENTRY — one queued change”// cdc_loginfo_entry -- src/transaction/log_impl.htypedef struct cdc_loginfo_entry{ LOG_LSA next_lsa; int length; char *log_info;} CDC_LOGINFO_ENTRY;| Field | Role / Why |
|---|---|
next_lsa | Resume LSA after this entry; sets CDC_CONSUMER::next_lsa. |
length | Byte length of log_info; copy + size accounting. |
log_info | Heap blob, one serialized change (Ch 6-7); consumer copies. |
Producer allocates entry + blob and enqueues the pointer; consumer copies the blob into CDC_CONSUMER::log_info, takes next_lsa, frees the entry. Blob built in Ch 7, decoded in Ch 8.
1.5 CDC_TEMP_LOGBUF — a scratch log page
Section titled “1.5 CDC_TEMP_LOGBUF — a scratch log page”// cdc_temp_logbuf -- src/transaction/log_impl.htypedef struct cdc_temp_logbuf{ LOG_PAGE *log_page_p; char log_page[IO_MAX_PAGE_SIZE + MAX_ALIGNMENT];} CDC_TEMP_LOGBUF;| Field | Role / Why |
|---|---|
log_page_p | LOG_PAGE * view set up via the CDC_GET_TEMP_LOGPAGE macros; aligned handle, no separate alloc. |
log_page | Inline char[IO_MAX_PAGE_SIZE + MAX_ALIGNMENT]; one page by value plus alignment slack. |
Two of these because a record may span a page. The CDC_GET_TEMP_LOGPAGE / CDC_CHECK_TEMP_LOGPAGE / CDC_UPDATE_TEMP_LOGPAGE macros key into temp_logbuf[(process_lsa)->pageid % 2], logpb_fetch_page + memcpy only on a page-id miss — buffers alternate by parity.
1.6 The signalling enums
Section titled “1.6 The signalling enums”Drive the handshake; transitions belong to Chapter 3.
// cdc_producer_state -- src/transaction/log_impl.htypedef enum cdc_producer_state{ CDC_PRODUCER_STATE_WAIT, CDC_PRODUCER_STATE_RUN, CDC_PRODUCER_STATE_DEAD } CDC_PRODUCER_STATE;
// cdc_consumer_request -- src/transaction/log_impl.htypedef enum cdc_consumer_request{ CDC_REQUEST_CONSUMER_TO_WAIT, CDC_REQUEST_CONSUMER_TO_RUN, CDC_REQUEST_CONSUMER_NONE } CDC_CONSUMER_REQUEST;
// cdc_producer_request -- src/transaction/log_impl.htypedef enum cdc_producer_request{ CDC_REQUEST_PRODUCER_TO_WAIT, CDC_REQUEST_PRODUCER_TO_BE_DEAD, CDC_REQUEST_PRODUCER_NONE } CDC_PRODUCER_REQUEST;| Enum | Values, in order, and meaning |
|---|---|
CDC_PRODUCER_STATE | WAIT parked on wait_cond / RUN decoding+pushing / DEAD terminated. |
CDC_CONSUMER_REQUEST | TO_WAIT idle / TO_RUN serve current request / NONE no pending. |
CDC_PRODUCER_REQUEST | TO_WAIT pause→STATE_WAIT / TO_BE_DEAD terminate→STATE_DEAD / NONE no pending. |
Asymmetry: the producer has both a state (observed) and a request (commanded); the consumer has only a request — its progress is read from the output triplet and queue LSAs.
1.7 The classification enums
Section titled “1.7 The classification enums”Tag what kind of change a queued entry is. Set server-side during decode (Chapter 6), re-asserted client-side.
// cdc_dataitem_type -- src/transaction/log_impl.htypedef enum cdc_dataitem_type{ CDC_DDL = 0, CDC_DML, CDC_DCL, CDC_TIMER } CDC_DATAITEM_TYPE;
// cdc_dcl_type -- src/transaction/log_impl.htypedef enum cdc_dcl_type{ CDC_COMMIT = 0, CDC_ABORT } CDC_DCL_TYPE;
// cdc_dml_type -- src/transaction/log_impl.htypedef enum cdc_dml_type{ CDC_INSERT = 0, CDC_UPDATE, CDC_DELETE, CDC_TRIGGER_INSERT, CDC_TRIGGER_UPDATE, CDC_TRIGGER_DELETE } CDC_DML_TYPE;| Enum | Values and meaning |
|---|---|
CDC_DATAITEM_TYPE | DDL schema / DML row / DCL transaction boundary / TIMER heartbeat. |
CDC_DCL_TYPE | COMMIT / ABORT — which boundary. |
CDC_DML_TYPE | INSERT/UPDATE/DELETE plus TRIGGER_* variants done by a trigger. |
CDC_TIMER is the heartbeat: with no change in the window the producer emits a timer item so the resume cursor still advances. These integers go verbatim into the wire payload as data_item_type and the per-arm *_type fields.
1.8 Client side: CUBRID_LOG_ITEM and CUBRID_DATA_ITEM
Section titled “1.8 Client side: CUBRID_LOG_ITEM and CUBRID_DATA_ITEM”The client sees only this self-contained list: cubrid_log_extract returns the head, the caller walks next and frees with cubrid_log_clear_log_item.
// cubrid_log_item -- src/api/cubrid_log.htypedef struct cubrid_log_item CUBRID_LOG_ITEM;struct cubrid_log_item{ int transaction_id; char *user; int data_item_type; CUBRID_DATA_ITEM data_item; CUBRID_LOG_ITEM *next;};
// cubrid_data_item -- src/api/cubrid_log.htypedef union cubrid_data_item CUBRID_DATA_ITEM;union cubrid_data_item{ DDL ddl; DML dml; DCL dcl; TIMER timer;};| Field | Role / Why |
|---|---|
transaction_id | Owning transaction id. |
user | Committing user (heap string); from tran_user. |
data_item_type | CDC_DATAITEM_TYPE tag; selects the live arm. |
data_item | CUBRID_DATA_ITEM union; one of DDL/DML/DCL/TIMER. |
next | Next item or NULL; forms the list. |
CUBRID_DATA_ITEM | Untagged union of the four arms; tagged externally. |
Invariant — data_item_type is the union tag. Reading data_item.dml when the tag is CDC_DDL is UB. The decoder (Ch 8) sets the tag, then fills the matching arm; cubrid_log_clear_log_item reads the tag to free the right pointers. Mismatch = garbage reads or wrong frees.
1.9 The client payload member structs
Section titled “1.9 The client payload member structs”The four union arms; server packs, Ch 8 decodes.
// ddl -- src/api/cubrid_log.htypedef struct ddl DDL;struct ddl{ int ddl_type; int object_type; uint64_t oid; uint64_t classoid; char *statement; int statement_length;};
// dml -- src/api/cubrid_log.htypedef struct dml DML;struct dml{ int dml_type; uint64_t classoid; int num_changed_column; int *changed_column_index; char **changed_column_data; int *changed_column_data_len; int num_cond_column; int *cond_column_index; char **cond_column_data; int *cond_column_data_len;};
// dcl -- src/api/cubrid_log.htypedef struct dcl DCL;struct dcl{ int dcl_type; time_t timestamp;};
// timer -- src/api/cubrid_log.htypedef struct timer TIMER;struct timer{ time_t timestamp;};DDL fields:
| Field | Role / Why |
|---|---|
ddl_type | Kind of schema change; DDL sub-discriminant. |
object_type | Target object class. |
oid | OID of the object. |
classoid | Class OID context. |
statement | Heap SQL string; replayable, freed on clear. |
statement_length | Byte length; may carry embedded bytes. |
DML fields — two index-parallel column-array triplets: changed (new image) and cond (old/identifying). all_in_cond (1.2) decides whether cond carries just the key or every column.
| Field | Role / Why |
|---|---|
dml_type | CDC_DML_TYPE; insert/update/delete + triggers. |
classoid | Class OID of the table. |
num_changed_column | Changed-column count; length of changed triplet. |
changed_column_index | int* positions of new-image columns. |
changed_column_data | char** new value blobs. |
changed_column_data_len | int* new value byte lengths. |
num_cond_column | Cond-column count; length of cond triplet. |
cond_column_index | int* positions of identifying columns. |
cond_column_data | char** old/identifying value blobs. |
cond_column_data_len | int* cond value byte lengths. |
DCL and TIMER fields:
| Struct::Field | Role / Why |
|---|---|
DCL::dcl_type | CDC_DCL_TYPE commit/abort discriminant. |
DCL::timestamp | time_t commit/abort time. |
TIMER::timestamp | time_t heartbeat tick (1.7); advances the client cursor. |
1.10 Caps and the queue element type
Section titled “1.10 Caps and the queue element type”// MAX_CDC_LOGINFO_QUEUE -- src/transaction/log_impl.h#define MAX_CDC_LOGINFO_QUEUE_ENTRY 2048#define MAX_CDC_LOGINFO_QUEUE_SIZE 32 * 1024 * 1024 /*32 MB */| Macro | Value | Role |
|---|---|---|
MAX_CDC_LOGINFO_QUEUE_ENTRY | 2048 | Cap on CDC_LOGINFO_ENTRY * slots; ring capacity. |
MAX_CDC_LOGINFO_QUEUE_SIZE | 32 MB | Byte cap; checked against produced_queue_size (outstanding bytes) to bound payload regardless of count. |
Entries bound how many pending changes, bytes how much memory — both feed Ch 5’s backpressure. The byte gauge is not a forever-running total: the producer periodically does produced_queue_size -= consumed_queue_size; consumed_queue_size = 0, so produced_queue_size reflects bytes still in the queue. The queue is lockfree::circular_queue<CDC_LOGINFO_ENTRY *>, an SPSC ring of pointers matching the allocate/free ownership in 1.4.
1.11 Chapter summary — key takeaways
Section titled “1.11 Chapter summary — key takeaways”- One server global, one client list. Server state on
cdc_Gl(CDC_GLOBAL,log_impl.h); client on theCUBRID_LOG_ITEMlist (cubrid_log.h). No shared struct — bytes cross viacdc_Gl.conn. - The queue carries pointers, not values.
loginfo_queueislockfree::circular_queue<CDC_LOGINFO_ENTRY *>: producer allocates and enqueues the pointer, consumer copies out and frees. - state vs request is the core handshake.
CDC_PRODUCER::stateis what-IS (producer-written),requestwhat-is-WANTED (consumer-written), bothvolatile; the consumer has only arequest. - Two LSA windows stay consistent.
cdc_Gl.first/last_loginfo_queue_lsabrackets the live queue;CDC_CONSUMER::start_lsa/next_lsatrack progress,next_lsamonotonic except theADJUSTED_LSAsnap. - The output triplet is self-consistent.
log_info/log_info_size/log_info_buf_size+num_log_infomust hold0 <= size <= buf_size, count matching bytes, else overflow or runaway decode. - Discriminated unions on both ends.
CDC_DATAITEM_TYPE(+CDC_DCL_TYPE/CDC_DML_TYPE) tags server entries; clientdata_item_typeselects theCUBRID_DATA_ITEMarm — read the tag first. - Backpressure has two dimensions.
MAX_CDC_LOGINFO_QUEUE_ENTRY(2048) bounds the ring by count;MAX_CDC_LOGINFO_QUEUE_SIZE(32 MB) bounds it by bytes viaproduced_queue_size, which the producer keeps as outstanding bytes by periodically subtracting (and zeroing)consumed_queue_size.
Chapter 2: Producer Side Supplemental Record Emission
Section titled “Chapter 2: Producer Side Supplemental Record Emission”The reader question: before any consumer connects, how does an ordinary
DML or DDL transaction deposit the supplemental records CDC later reads
out of the WAL? The high-level companion (cubrid-cdc.md, “What CDC
captures” and “Supplemental logging”) covers why CDC piggy-backs on the
write-ahead log; this chapter traces the four write-side appenders in
log_manager.c and the on-disk shape of what they emit, all on the
server transaction thread of the writer, long before a CDC client
exists (the consumer side is Chapters 5-8).
Annotation convention. Inline
/* <- ... */comments are editorial; all other comment text is verbatim from source.
2.1 The one log type, eleven record subtypes
Section titled “2.1 The one log type, eleven record subtypes”Every supplemental record CDC reads is a single WAL log type,
LOG_SUPPLEMENTAL_INFO, in the master log_rectype enum:
// LOG_SUPPLEMENTAL_INFO -- src/transaction/log_record.hpp LOG_SUPPLEMENTAL_INFO = 52, /* used for supplemental logs to support CDC interface. * it contains transaction user info, DDL statement, undo lsa, redo lsa for DML, * or undo images that never retrieved from the log. */The payload kind is not in the WAL log type but one level down, in the
record’s own header as a SUPPLEMENT_REC_TYPE. Recovery, flashback, and
CDC dispatch on LOG_SUPPLEMENTAL_INFO uniformly (prior_lsa_gen_record
sets data_header_length = sizeof (LOG_REC_SUPPLEMENT)); a CDC reader
then peels the inner rec_type.
// SUPPLEMENT_REC_TYPE -- src/transaction/log_record.hpptypedef enum supplement_rec_type{ LOG_SUPPLEMENT_TRAN_USER, LOG_SUPPLEMENT_UNDO_RECORD, /*Contains undo raw record that can not be retrieved from the logs */ LOG_SUPPLEMENT_DDL, /* Contains lsa of logs which contain undo, redo raw record (UPDATE, DELETE, INSERT) * | LOG_REC_HEADER | SUPPLEMENT_REC_TYPE | LENGTH | CLASS OID | UNDO LSA (sizeof LOG_LSA) | REDO LSA | */ LOG_SUPPLEMENT_INSERT, LOG_SUPPLEMENT_UPDATE, LOG_SUPPLEMENT_DELETE, LOG_SUPPLEMENT_TRIGGER_INSERT, /* INSERT, UPDATE, DELETE logs appended by a trigger action */ LOG_SUPPLEMENT_TRIGGER_UPDATE, LOG_SUPPLEMENT_TRIGGER_DELETE, LOG_SUPPLEMENT_LARGER_REC_TYPE,} SUPPLEMENT_REC_TYPE;All eleven values map to a single emitter table:
| Value | Emitted by | Body shape |
|---|---|---|
LOG_SUPPLEMENT_TRAN_USER | commit / DDL site | db_user string; one per txn (2.7) |
LOG_SUPPLEMENT_UNDO_RECORD | ..._undo_record | recdes.type + raw bytes the reader can’t re-fetch (2.5) |
LOG_SUPPLEMENT_DDL | ..._serial; network site | packed ints + OIDs + inline SQL (2.6) |
LOG_SUPPLEMENT_INSERT | ..._lsa | OID + REDO LSA, no prior image (2.4) |
LOG_SUPPLEMENT_UPDATE | ..._lsa | OID + UNDO + REDO LSA, both images |
LOG_SUPPLEMENT_DELETE | ..._lsa | OID + UNDO LSA, no new image |
LOG_SUPPLEMENT_TRIGGER_INSERT/UPDATE/DELETE | DML, trigger_involved set | identical to DML triple |
LOG_SUPPLEMENT_LARGER_REC_TYPE | never | range sentinel; must stay last |
The inline comment after LOG_SUPPLEMENT_DDL documents the body layout
the DML group and their TRIGGER_* twins share (LSAs, not images, 2.4).
Invariant — the inner rec_type is the only discriminator. All
eleven subtypes ride inside one WAL log type; nothing in the outer
LOG_RECORD_HEADER distinguishes them. Inserting a subtype before
LOG_SUPPLEMENT_LARGER_REC_TYPE silently widens every rec_type < LOG_SUPPLEMENT_LARGER_REC_TYPE range check — the sentinel must stay last.
2.2 The supplemental record header
Section titled “2.2 The supplemental record header”A LOG_SUPPLEMENTAL_INFO body begins with a fixed two-field header that
prior_lsa_gen_record reserves as the data_header:
// log_rec_supplement -- src/transaction/log_record.hpptypedef struct log_rec_supplement LOG_REC_SUPPLEMENT;struct log_rec_supplement{ SUPPLEMENT_REC_TYPE rec_type; int length;};| Field | Role | Why it exists |
|---|---|---|
rec_type | Which of the eleven payloads follows | Sole discriminator for the reader’s decode switch (2.1) |
length | Byte length of the payload after the header | Lets the reader skip/copy exactly; high bit doubles as a zip flag |
The payload is not in this struct; it rides as the prior node’s undo
data (2.3). length is overloaded: a compressed payload carries
MAKE_ZIP_LEN(length) ((length) | 0x80000000), with ZIP_CHECK /
GET_ZIP_LEN in log_compress.h reading it back.
Invariant — length’s top bit means “zipped”, not “negative”. A
reader must ZIP_CHECK before treating length as a raw size; otherwise
a zipped record over-reads by ~2 GB. This is why 2.3 stores
MAKE_ZIP_LEN(...) only on the zipped branch.
On-disk nesting is three layers: outer LOG_RECORD_HEADER (type = LOG_SUPPLEMENTAL_INFO), inner LOG_REC_SUPPLEMENT, then the payload
riding as the prior node’s undo data.
2.3 log_append_supplemental_info — the generic appender
Section titled “2.3 log_append_supplemental_info — the generic appender”Every supplemental record is born here — the only caller of
prior_lsa_alloc_and_copy_data for supplemental logging; the helpers in
2.4-2.6 are thin formatters over it. Load-bearing lines:
// log_append_supplemental_info -- src/transaction/log_manager.c assert (prm_get_integer_value (PRM_ID_SUPPLEMENTAL_LOG) > 0); if (length >= log_Zip_min_size_to_compress && log_Zip_support) /* branch A */ { zip_undo = log_append_get_zip_undo (thread_p); if (zip_undo == NULL) { return; /* <- no zip buffer: record silently dropped */ } log_zip (zip_undo, length, data); // ... rebind length = zip_undo->data_length, data = zip_undo->log_data; is_zipped = true ... } /* supplement data will be stored at undo data */ node = prior_lsa_alloc_and_copy_data (thread_p, LOG_SUPPLEMENTAL_INFO, RV_NOT_DEFINED, NULL, length, (char *) data, 0, NULL); if (node == NULL) { return; /* <- allocation failed: record dropped */ } supplement = (LOG_REC_SUPPLEMENT *) node->data_header; supplement->rec_type = rec_type; supplement->length = is_zipped ? MAKE_ZIP_LEN (zip_undo->data_length) : length; /* <- src uses if/else */ prior_lsa_next_record (thread_p, node, tdes);Figure 2-1 carries every branch; the two non-obvious ones are the
silent drops — a NULL zip buffer and a NULL node both return
without raising an error. The entry assert (> 0) is compiled out in
release, so the callers gate per 2.8.
Invariant — supplemental records are recovery-inert.
RV_NOT_DEFINED makes crash recovery skip them; they are never replayed,
only read forward by CDC/flashback, so correctness depends only on their
presence in LSA order.
flowchart TD
S["caller (gated by PRM_ID_SUPPLEMENTAL_LOG)"] --> Z{"length >= 255<br/>and log_Zip_support?"}
Z -- yes --> ZB{"zip buffer?"}
ZB -- no --> R1["return: dropped"]
ZB -- yes --> ZC["log_zip; rebind; is_zipped=true"]
Z -- no --> AL
ZC --> AL["prior_lsa_alloc_and_copy_data(LOG_SUPPLEMENTAL_INFO)"]
AL --> NN{"node == NULL?"}
NN -- yes --> R2["return: dropped"]
NN -- no --> HF["set rec_type; length"]
HF --> PUB["prior_lsa_next_record"]
Figure 2-1. Every branch, including the two silent-drop early returns.
2.4 The DML formatter — log_append_supplemental_lsa
Section titled “2.4 The DML formatter — log_append_supplemental_lsa”Workhorse for INSERT/UPDATE/DELETE and trigger variants. Its defining property is indirection: it logs LSAs into existing WAL undo/redo records, never the row images themselves.
// log_append_supplemental_lsa -- src/transaction/log_manager.c int size; /* sizeof (OID) = 8, sizeof (LOG_LSA) = 8; raw memcpy, not OR_PUT_*, so OR_OID_SIZE/OR_LOG_LSA_SIZE unused */ char data[24]; assert (prm_get_integer_value (PRM_ID_SUPPLEMENTAL_LOG) > 0); switch (rec_type) { case LOG_SUPPLEMENT_INSERT: case LOG_SUPPLEMENT_TRIGGER_INSERT: assert (redo_lsa != NULL); size = sizeof (OID) + sizeof (LOG_LSA); /* <- OID + redo only */ // ... memcpy classoid, redo_lsa ... break; case LOG_SUPPLEMENT_UPDATE: case LOG_SUPPLEMENT_TRIGGER_UPDATE: assert (undo_lsa != NULL && redo_lsa != NULL); size = sizeof (OID) + sizeof (LOG_LSA) + sizeof (LOG_LSA); /* <- OID + undo + redo */ // ... memcpy classoid, undo_lsa, redo_lsa ... break; case LOG_SUPPLEMENT_DELETE: case LOG_SUPPLEMENT_TRIGGER_DELETE: assert (undo_lsa != NULL); size = sizeof (OID) + sizeof (LOG_LSA); /* <- OID + undo only */ // ... memcpy classoid, undo_lsa ... break; default: assert (false); return ER_FAILED; /* <- non-DML rec_type */ } log_append_supplemental_info (thread_p, rec_type, size, (void *) data); return NO_ERROR;Three live case groups plus a default error path: INSERT packs
OID + redo (size = 16); UPDATE packs OID + undo + redo (size = 24, filling data[24]); DELETE packs OID + undo (size = 16); each
TRIGGER_* case is byte-for-byte identical to its plain twin, differing
only in the rec_type tag; default trips assert(false) and returns
ER_FAILED, emitting nothing (the function is contractually DML-only).
Each of the three callers in heap_file.c picks the tag via
thread_p->trigger_involved ? LOG_SUPPLEMENT_TRIGGER_* : LOG_SUPPLEMENT_*.
The three emit-site guards are not uniform: heap_insert_logical adds
!LSA_ISNULL (&context->supp_redo_lsa) && context->recdes_p->type != REC_ASSIGN_ADDRESS to the do_supplemental_log check — address-only
placeholders have no real image LSA and are skipped — whereas
heap_delete_logical and heap_update_logical gate on
context->do_supplemental_log alone, their supp_undo_lsa (and, for
update, supp_redo_lsa) already populated from the tail LSA.
Invariant — DML supplemental records store LSAs, not images. Payload is at most 24 bytes regardless of row width; CDC follows the undo/redo LSAs into the already-written WAL records to materialize images later (Chapter 7). The price is a second WAL read at decode time and a reachability requirement on the referenced record — which motivates the next subtype.
2.5 log_append_supplemental_undo_record — when the image can’t be re-fetched
Section titled “2.5 log_append_supplemental_undo_record — when the image can’t be re-fetched”Some undo images live in overflow or forwarded heap records a CDC reader cannot reconstruct by following an LSA (the chain may have been reorganized), so the producer inlines the raw bytes:
// log_append_supplemental_undo_record -- src/transaction/log_manager.c int length = undo_recdes->length + sizeof (undo_recdes->type); char *data = (char *) malloc (length); if (data == NULL) { er_set (ER_ERROR_SEVERITY, ARG_FILE_LINE, ER_OUT_OF_VIRTUAL_MEMORY, 1, length); return ER_OUT_OF_VIRTUAL_MEMORY; /* <- only error path */ } memcpy (data, &undo_recdes->type, sizeof (undo_recdes->type)); /* <- prepend the record type */ memcpy (data + sizeof (undo_recdes->type), undo_recdes->data, undo_recdes->length); log_append_supplemental_info (thread_p, LOG_SUPPLEMENT_UNDO_RECORD, length, data); free_and_init (data); return NO_ERROR;Two branches. The malloc failure is the only error path; the
success path prepends the 2-byte recdes.type (so the consumer knows
REC_HOME, REC_BIGONE, etc.), hands off to the generic appender, then
frees the buffer. Because payloads can be large, this subtype most often
hits the zip branch (2.3).
2.6 log_append_supplemental_serial — DDL-shaped record for serials
Section titled “2.6 log_append_supplemental_serial — DDL-shaped record for serials”Serial next-value consumption is logged as a LOG_SUPPLEMENT_DDL record
carrying a synthesized SQL statement, so a consumer replays the advance
as a statement rather than chasing the catalog change:
// log_append_supplemental_serial -- src/transaction/log_manager.c int ddl_type = 1; /* <- 1 = DDL */ int obj_type = 2; /* <- 2 = serial */ if (cached_num == 0) { cached_num = 1; /* <- normalize so the generated SQL is well-formed */ } sprintf (stmt, "SELECT SERIAL_NEXT_VALUE(%s, %d);", serial_name, cached_num); // ... size data_len; malloc (+MAX_ALIGNMENT); on fail er_set ER_OUT_OF_VIRTUAL_MEMORY, return (only error) ... ptr = start_ptr = supplemental_data; // ... or_pack_int ddl_type, obj_type; or_pack_oid classoid, serial_oid; or_pack_int strlen; or_pack_string stmt ... data_len = ptr - start_ptr; /* <- recompute: packing may pad to alignment */ log_append_supplemental_info (thread_p, LOG_SUPPLEMENT_DDL, data_len, (void *) supplemental_data);Two branches: the cached_num == 0 normalization and the malloc
failure (the only error return). Unlike the DML formatter’s raw memcpy,
the DDL body is or_pack-encoded (ddl_type | obj_type | class OID | serial OID | stmt len | stmt text) for its variable-length string, with
data_len recomputed from the cursor after packing. This layout is
mirrored at the real DDL-statement site (2.7).
2.7 The DDL emit site and the two TRAN_USER sites
Section titled “2.7 The DDL emit site and the two TRAN_USER sites”The full-statement DDL record is emitted from the network handler in
network_interface_sr.cpp, not log_manager.c. Its shape matches 2.6,
and this is the first of two LOG_SUPPLEMENT_TRAN_USER sites:
// (DDL statement supplemental emit) -- src/communication/network_interface_sr.cpp if (prm_get_integer_value (PRM_ID_SUPPLEMENTAL_LOG) == 1) { // ... unpack ddl_type, obj_type, classoid, oid, stmt_text; or_pack into supplemental_data ... tdes = LOG_FIND_CURRENT_TDES (thread_p); if (!tdes->has_supplemental_log) { log_append_supplemental_info (thread_p, LOG_SUPPLEMENT_TRAN_USER, /* ... db_user ... */); tdes->has_supplemental_log = true; /* <- first supplemental record in this txn */ } log_append_supplemental_info (thread_p, LOG_SUPPLEMENT_DDL, data_len, supplemental_data); }A DDL that is the first supplemental activity logs the user eagerly,
then flips has_supplemental_log. The second site is at commit, for
transactions whose only supplemental activity was DML (an identical block
appears in log_append_repl_info_and_commit_log, the HA variant writing
repl + commit under one hold of prior_lsa_mutex):
// log_append_commit_log -- src/transaction/log_manager.c if (tdes->has_supplemental_log) { log_append_supplemental_info (thread_p, LOG_SUPPLEMENT_TRAN_USER, /* ... db_user ... */); tdes->has_supplemental_log = false; /* <- consume the flag at commit */ } // ... log_append_donetime_internal (... LOG_COMMIT ...) writes the commit record ...Why the user name is separate from the row event. A DML row event (2.4) carries no per-row user attribution; the db_user is a transaction-level fact emitted once per transaction, which the consumer associates with every row event in the same transaction window (Chapter 6).
Invariant — at most one LOG_SUPPLEMENT_TRAN_USER per transaction.
has_supplemental_log is the guard (Figure 2-2 traces its lifecycle);
two emitters would double-count or mis-attribute. The flag is also
cleared on abort (log_append_abort_log), so a rolled-back transaction
leaves no dangling expectation.
stateDiagram-v2 [*] --> NoSupp: txn begins, flag false NoSupp --> UserLogged: first DDL -> emit TRAN_USER, set flag NoSupp --> DmlPending: first DML -> set flag, no user yet DmlPending --> DmlPending: more DML, flag stays set UserLogged --> UserLogged: more DDL or DML, flag stays true DmlPending --> Committed: commit -> emit TRAN_USER, clear flag UserLogged --> Committed: commit, flag already cleared at DDL site DmlPending --> Aborted: abort -> clear flag, no user record Committed --> [*] Aborted --> [*]
Figure 2-2. The has_supplemental_log state machine guaranteeing one
user record per transaction.
2.8 The opt-in gate
Section titled “2.8 The opt-in gate”The gate PRM_ID_SUPPLEMENTAL_LOG is re-tested at the network DDL site
(the == 1 test in 2.7) rather than trusted once, because the source
comment there explains the broker (CAS) and server hold independent
parameter copies: “CAS and Server are able to have different parameter
value, when broker restarted with changed parameter value.”
Supplemental logging adds WAL volume and CPU for the occasional zip, so it is off by default. Disabled, a transaction writes no supplemental records and is invisible to CDC — the contract Chapter 4 relies on.
2.9 Chapter summary — key takeaways
Section titled “2.9 Chapter summary — key takeaways”- One WAL type, eleven subtypes. Everything CDC reads is
LOG_SUPPLEMENTAL_INFO(52); the discriminator is the innerLOG_REC_SUPPLEMENT.rec_type, withLOG_SUPPLEMENT_LARGER_REC_TYPElast as range sentinel. log_append_supplemental_infois the single chokepoint. It owns compression (MAKE_ZIP_LEN/lengthtop-bit),RV_NOT_DEFINEDallocation (recovery-inert), and two silent-drop early returns.- DML records carry LSAs, not images.
log_append_supplemental_lsapacks OID + undo/redo LSAs in 16-24 bytes per the INSERT/UPDATE/DELETE (and TRIGGER_*) switch, cost independent of row width. - Two body encodings. DML uses raw
memcpy; DDL/serial usesor_packfor its variable-length SQL; therecdes-inlining undo-record path exists for images an LSA can’t re-fetch. - One user record per transaction.
LOG_SUPPLEMENT_TRAN_USERis emitted eagerly at the first DDL site or lazily at commit, gated byhas_supplemental_log. - The whole producer side is opt-in.
PRM_ID_SUPPLEMENTAL_LOGgates every emit path, re-tested at the network site because broker and server can disagree.
Chapter 3: Initialization and the Producer Consumer Daemon Pair
Section titled “Chapter 3: Initialization and the Producer Consumer Daemon Pair”This chapter answers one question: when a CDC consumer attaches, how is the subsystem bootstrapped, and what threads and memory get created? The structs (CDC_GLOBAL, CDC_PRODUCER, CDC_CONSUMER, the lock-free loginfo_queue, temp_logbuf[2]) were covered field-by-field in Chapter 1; here we trace the code that brings them to life, tears them down, and the signalling primitives the rest of the document leans on. For the why of CDC, see the companion cubrid-cdc.md (“Producer/consumer split”, “Log info queue”).
Two lifecycles stay separate. Boot — cdc_daemons_init / cdc_daemons_destroy, run once per server start/shutdown from src/transaction/boot_sr.c, creates the single producer daemon and the global memory. Session — cdc_set_configuration, cdc_reinitialize_queue, cdc_cleanup, run by the network_interface_sr.cpp handlers on each attach/re-seek/detach. The daemon is not recreated per session; it is paused and resumed.
3.1 The boot-time entry point: cdc_daemons_init
Section titled “3.1 The boot-time entry point: cdc_daemons_init”cdc_daemons_init is the gate. CDC is opt-in via supplemental_log, so it returns immediately when that is off — no daemon, no memory.
// cdc_daemons_init -- src/transaction/log_manager.cvoid cdc_daemons_init (){ if (prm_get_integer_value (PRM_ID_SUPPLEMENTAL_LOG) == 0) return; /* <- CDC disabled: nothing allocated */ cdc_Logging = prm_get_bool_value (PRM_ID_CDC_LOGGING_DEBUG); /* <- enables cdc_log() tracing */ cdc_initialize (); /* <- zero state + allocate queue + wire temp_logbuf */ cdc_loginfo_producer_daemon_init (); /* <- create the cubthread daemon */}Order matters: cdc_initialize runs before cdc_loginfo_producer_daemon_init, because the daemon task cdc_loginfo_producer_execute reads producer.state / .request and the loginfo_queue pointer the moment it is scheduled.
Invariant — CDC global state is fully zeroed before the daemon exists.
cdc_initializecompletes (setsstate = CDC_PRODUCER_STATE_DEAD, allocatesloginfo_queue, aligns bothtemp_logbufslots) strictly beforecreate_daemon. The daemon’s first act is to readstate/requestand the queue pointer; against a NULLloginfo_queueor unalignedtemp_logbufthe firstis_full()check or page memcpy would crash. The ordering incdc_daemons_initis the enforcement.
cdc_daemons_init is called from src/transaction/boot_sr.c (boot_restart_server); cdc_daemons_destroy from the two shutdown paths in the same file. The block is wrapped in #if defined (SERVER_MODE) — no producer daemon in standalone (SA) mode.
3.2 cdc_initialize — zeroing state and allocating the queue
Section titled “3.2 cdc_initialize — zeroing state and allocating the queue”Pure state setup; it never blocks and never touches the mutex (the mutex is created later, by daemon-init). Straight-line, no branches. Each assignment maps to a Chapter 1 field:
// cdc_initialize -- src/transaction/log_manager.c cdc_Gl.conn.fd = -1; cdc_Gl.conn.status = CONN_CLOSED; /* <- no consumer attached */ cdc_Gl.producer.request = CDC_REQUEST_PRODUCER_NONE; cdc_Gl.producer.state = CDC_PRODUCER_STATE_DEAD; cdc_Gl.loginfo_queue = new lockfree::circular_queue <CDC_LOGINFO_ENTRY *> (MAX_CDC_LOGINFO_QUEUE_ENTRY); cdc_Gl.producer.temp_logbuf[0].log_page_p = (LOG_PAGE *) PTR_ALIGN (cdc_Gl.producer.temp_logbuf[0].log_page, MAX_ALIGNMENT); /* <- slot 1 likewise */ // ... condensed: extraction_user/classoids = NULL; consumer.request = NONE; produced/consumed_queue_size = 0; // first/last_loginfo_queue_lsa, consumer.log_info*, start_lsa, next_lsa all LSA_SET_NULL / 0 ...Three subtleties: (1) the queue holds pointers — MAX_CDC_LOGINFO_QUEUE_ENTRY = 2048 slots (a slot cap); entries and log_info payloads are heap-allocated separately by the producer, and the byte cap MAX_CDC_LOGINFO_QUEUE_SIZE = 32 MB is tracked by produced_queue_size; either limit pauses the producer. (2) each temp_logbuf[i].log_page_p is the first MAX_ALIGNMENT-aligned address inside log_page[IO_MAX_PAGE_SIZE + MAX_ALIGNMENT] — a two-page window indexed by pageid % 2. (3) tran_user / tran_ignore are untouched (those std::unordered_map members default-construct empty in the static cdc_Gl); cdc_finalize (§3.6) frees their contents.
3.3 cdc_loginfo_producer_daemon_init — registering the daemon
Section titled “3.3 cdc_loginfo_producer_daemon_init — registering the daemon”The only CDC thread is born here. The file-scope REGISTER_DAEMON (cdc_loginfo_producer) macro reserves a daemon slot with multiplicity 1 — one producer.
// cdc_loginfo_producer_daemon_init -- src/transaction/log_manager.c assert (cdc_Loginfo_producer_daemon == NULL); /* <- invariant: exactly one daemon ever */ pthread_mutex_init (&cdc_Gl.producer.lock, NULL); pthread_cond_init (&cdc_Gl.producer.wait_cond, NULL); /* <- the producer's sleep primitive */ LSA_SET_NULL (&cdc_Gl.producer.next_extraction_lsa); cdc_Gl.producer.request = CDC_REQUEST_PRODUCER_TO_WAIT; /* <- born paused, set before create_daemon */ cubthread::looper looper = cubthread::looper (std::chrono::milliseconds (10)); /* <- 10 ms poll period */ cdc_Loginfo_producer_daemon = cubthread::get_manager ()->create_daemon (looper, new cubthread::entry_callable_task (cdc_loginfo_producer_execute), "cdc-loginfo-producer");The mutex and condvar are created here, not in cdc_initialize, so the signalling primitives (§3.8-§3.9) are only valid after the daemon exists. request = TO_WAIT is set before create_daemon: the daemon body (Chapter 5) sets state = RUN, sees TO_WAIT on its first iteration, sets state = WAIT, and parks — born paused, doing no extraction until a consumer seeds a start LSA. The 10 ms period bounds the latency of noticing new log records and the LOG_HA_DUMMY_SERVER_STATUS heartbeat (Chapter 5).
flowchart TD A["cdc_daemons_init"] -->|"supplemental_log == 0"| Z["return, nothing allocated"] A -->|"enabled"| B["cdc_Logging = prm CDC_LOGGING_DEBUG"] B --> C["cdc_initialize"] C --> C1["zero conn / filters / counters"] C --> C2["new circular_queue 2048 slots"] C --> C3["PTR_ALIGN temp_logbuf 0 and 1"] C --> C4["state = DEAD, requests = NONE"] C --> D["cdc_loginfo_producer_daemon_init"] D --> D1["pthread_mutex_init + cond_init"] D --> D2["request = TO_WAIT, next_extraction_lsa = NULL"] D --> D3["create_daemon looper 10ms -> cdc_loginfo_producer_execute"] D3 --> E["daemon parks: state = WAIT on wait_cond"]
Figure 3-1: boot-time bring-up; the daemon ends parked, awaiting a seed LSA.
3.4 cdc_set_configuration — installing the extraction filters
Section titled “3.4 cdc_set_configuration — installing the extraction filters”On attach, the CDC handler in network_interface_sr.cpp unpacks the client’s filter parameters and calls this. Session-scoped; no queue alloc, no threads.
// cdc_set_configuration -- src/transaction/log_manager.c (straight-line, no branches) cdc_free_extraction_filter (); /* <- defensive: drop stale filter from a crashed prior session */ cdc_Gl.consumer.extraction_timeout = timeout; cdc_Gl.consumer.max_log_item = max_log_item; /* <- consumer */ cdc_Gl.producer.all_in_cond = all_in_cond; /* <- producer: filter combine mode */ cdc_Gl.producer.extraction_user = user; cdc_Gl.producer.num_extraction_user = num_user; /* <- takes ownership */ cdc_Gl.producer.extraction_classoids = classoids; cdc_Gl.producer.num_extraction_class = num_class;Filters split across the boundary: the consumer owns extraction_timeout and max_log_item (fetch wait, items per RPC); the producer owns all_in_cond, extraction_user[] (used by cdc_is_filtered_user, Chapter 6) and extraction_classoids[] (cdc_is_filtered_class); an empty list means “all pass.” The user/classoids pointers come from the network unpack — CDC now owns them. The leading cdc_free_extraction_filter is load-bearing: a crashed consumer never ran teardown, so its filter arrays would leak into the next attach; re-freeing first makes reconnection idempotent.
3.5 Teardown: cdc_free_extraction_filter, cdc_cleanup, cdc_cleanup_consumer
Section titled “3.5 Teardown: cdc_free_extraction_filter, cdc_cleanup, cdc_cleanup_consumer”A nested hierarchy. cdc_free_extraction_filter is innermost — it frees only the filter arrays:
// cdc_free_extraction_filter -- src/transaction/log_manager.c if (cdc_Gl.producer.extraction_user != NULL) { for (int i = 0; i < cdc_Gl.producer.num_extraction_user; i++) /* <- bound is num_extraction_user */ if (cdc_Gl.producer.extraction_user[i] != NULL) free_and_init (cdc_Gl.producer.extraction_user[i]); /* <- each username string, then the array below */ free_and_init (cdc_Gl.producer.extraction_user); } if (cdc_Gl.producer.extraction_classoids != NULL) free_and_init (cdc_Gl.producer.extraction_classoids);free_and_init frees and NULLs, so a double call is safe — what cdc_set_configuration’s re-free relies on. The user count and array are only ever set together, preserving the pairing.
cdc_cleanup is the session-end path — pause, free filters, drain the queue, reset counters:
// cdc_cleanup -- src/transaction/log_manager.c if (cdc_Gl.producer.state != CDC_PRODUCER_STATE_WAIT) cdc_pause_producer (); /* <- guard: skip if already parked, avoids needless round-trip */ cdc_free_extraction_filter (); while (!cdc_Gl.loginfo_queue->is_empty ()) /* <- drain: lock-free, no mutex needed */ { CDC_LOGINFO_ENTRY *tmp = NULL; cdc_Gl.loginfo_queue->consume (tmp); if (tmp->log_info != NULL) free (tmp->log_info); if (tmp != NULL) free (tmp); } /* <- payload then entry */ cdc_Gl.consumer.consumed_queue_size = 0; cdc_Gl.producer.produced_queue_size = 0; LSA_SET_NULL (&cdc_Gl.first_loginfo_queue_lsa); LSA_SET_NULL (&cdc_Gl.last_loginfo_queue_lsa); pthread_mutex_lock (&cdc_Gl.producer.lock); /* <- under lock: daemon reads next_extraction_lsa */ LSA_SET_NULL (&cdc_Gl.producer.next_extraction_lsa); /* in its hot loop (Ch.5), avoid torn read */ pthread_mutex_unlock (&cdc_Gl.producer.lock); cdc_cleanup_consumer (); /* <- inner: free the send buffer */cdc_cleanup_consumer is the leaf — frees only the server→client send buffer; safe standalone:
// cdc_cleanup_consumer -- src/transaction/log_manager.c if (cdc_Gl.consumer.log_info_size != 0) /* <- frees log_info only inside this branch */ { cdc_Gl.consumer.log_info_size = 0; cdc_Gl.consumer.num_log_info = 0; if (cdc_Gl.consumer.log_info != NULL) free_and_init (cdc_Gl.consumer.log_info); } LSA_SET_NULL (&cdc_Gl.consumer.start_lsa); LSA_SET_NULL (&cdc_Gl.consumer.next_lsa);log_info and log_info_size move together (Chapter 7) so the guard is sound; allocating log_info without bumping log_info_size would leak.
3.6 cdc_finalize and cdc_daemons_destroy — the shutdown path
Section titled “3.6 cdc_finalize and cdc_daemons_destroy — the shutdown path”cdc_daemons_destroy is the boot-lifecycle teardown, gated by the supplemental_log check plus a NULL-daemon guard (double shutdown is a no-op):
// cdc_daemons_destroy -- src/transaction/log_manager.c if (prm_get_integer_value (PRM_ID_SUPPLEMENTAL_LOG) == 0 || cdc_Loginfo_producer_daemon == NULL) return; /* <- disabled or already destroyed */ cdc_kill_producer (); /* <- request DEAD, spin until daemon confirms */ cubthread::get_manager ()->destroy_daemon (cdc_Loginfo_producer_daemon); cdc_finalize (); /* <- only now free queue + tran maps */Invariant — the daemon is dead before its memory is freed.
cdc_kill_producerdoes not return untilstate == DEAD, anddestroy_daemonjoins the thread, both beforecdc_finalizedeletesloginfo_queueand frees thetran_userstrings. Acdc_finalizerunning while the daemon still extracted would dereference a freed queue. The kill-then-destroy-then-finalize ordering is the enforcement; reordering it is a use-after-free.
cdc_finalize mirrors cdc_cleanup but frees the per-transaction tran_user strings and deletes the queue rather than draining it:
// cdc_finalize -- src/transaction/log_manager.c cdc_free_extraction_filter (); for (auto iter:cdc_Gl.producer.tran_user) /* <- usernames captured per live tran */ if (iter.second != NULL) free_and_init (iter.second); if (cdc_Gl.loginfo_queue != NULL) { while (!cdc_Gl.loginfo_queue->is_empty ()) /* <- same drain as cleanup: payload + entry */ { /* ... consume tmp; free_and_init (tmp->log_info); free_and_init (tmp); ... */ } delete cdc_Gl.loginfo_queue; cdc_Gl.loginfo_queue = NULL; } /* <- destroy the ring itself */ cdc_Gl.consumer.consumed_queue_size = 0; cdc_Gl.producer.produced_queue_size = 0; pthread_mutex_lock (&cdc_Gl.producer.lock); /* <- next_extraction_lsa cleared under lock, as in cleanup */ LSA_SET_NULL (&cdc_Gl.producer.next_extraction_lsa); pthread_mutex_unlock (&cdc_Gl.producer.lock); LSA_SET_NULL (&cdc_Gl.last_loginfo_queue_lsa); LSA_SET_NULL (&cdc_Gl.first_loginfo_queue_lsa);cdc_cleanup (session end) pauses the producer, drains but keeps the queue, leaves tran_user intact, frees the send buffer via cdc_cleanup_consumer; cdc_finalize (shutdown) runs after the producer is killed, drains then deletes the queue, and frees tran_user — only it makes the global unusable until a fresh cdc_initialize. Two asymmetries: cdc_finalize does not call cdc_cleanup_consumer (send buffer is freed at detach or by the OS at exit), and tran_ignore is not iterated (plain int values, no heap).
3.7 cdc_reinitialize_queue — re-seeking without losing the queue
Section titled “3.7 cdc_reinitialize_queue — re-seeking without losing the queue”On a mid-session re-seek, the queue may hold entries the consumer no longer wants. Rather than tear down and rebuild, this trims or resets in place and sets is_queue_reinitialized for the producer to key off.
// cdc_reinitialize_queue -- src/transaction/log_manager.c CDC_LOGINFO_ENTRY *consume = NULL; if (cdc_Gl.producer.produced_queue_size == 0) goto end; /* <- branch 1: empty queue, nothing to trim */ cdc_Gl.is_queue_reinitialized = true; /* <- producer reads this to resync (Chapter 5) */ if (LSA_LT (&cdc_Gl.first_loginfo_queue_lsa, start_lsa) && LSA_GE (&cdc_Gl.last_loginfo_queue_lsa, start_lsa)) { /* branch 2: start_lsa inside [first,last] -> trim the front only */ LOG_LSA next_consume_lsa; LSA_COPY (&next_consume_lsa, &cdc_Gl.first_loginfo_queue_lsa); while (LSA_LT (&next_consume_lsa, start_lsa)) { cdc_Gl.loginfo_queue->consume (consume); cdc_Gl.consumer.consumed_queue_size += consume->length; LSA_COPY (&next_consume_lsa, &consume->next_lsa); if (consume->log_info != NULL) free_and_init (consume->log_info); } /* <- payload only, not the entry */ cdc_Gl.producer.produced_queue_size -= cdc_Gl.consumer.consumed_queue_size; /* <- keep gauge accurate */ cdc_Gl.consumer.consumed_queue_size = 0; } else { /* branch 3: start_lsa outside range -> drain everything, delete + new the ring */ while (!cdc_Gl.loginfo_queue->is_empty ()) { cdc_Gl.loginfo_queue->consume (consume); if (consume->log_info != NULL) free_and_init (consume->log_info); } cdc_Gl.producer.produced_queue_size = 0; cdc_Gl.consumer.consumed_queue_size = 0; delete cdc_Gl.loginfo_queue; cdc_Gl.loginfo_queue = new lockfree::circular_queue <CDC_LOGINFO_ENTRY *> (MAX_CDC_LOGINFO_QUEUE_ENTRY); }end: cdc_log ("... reinitialize end");Branch 2 is the cheap re-seek-slightly-forward case; branch 3 handles seeks backward (before first) or far forward (past last). Both free consume->log_info but not the consume entry struct in the trim loop — unlike cdc_cleanup/cdc_finalize, which free both; the // TODO: please check consume is NULL comments in the source mark this rough edge, treat it as live when hardening. is_queue_reinitialized is never cleared here — the producer clears it once it has resynced its extraction LSA (Chapter 5).
3.8 Pausing and killing the producer (consumer → producer)
Section titled “3.8 Pausing and killing the producer (consumer → producer)”Three functions manipulate producer.request and read back producer.state. The key property: no tight ping-pong. The consumer sets a request and either fire-and-forgets (wakeup) or busy-waits on the state (pause/kill). The daemon owns state; the consumer owns request.
// cdc_pause_producer / cdc_wakeup_producer / cdc_kill_producer -- src/transaction/log_manager.ccdc_pause_producer: cdc_Gl.producer.request = CDC_REQUEST_PRODUCER_TO_WAIT; /* <- set request */ while (cdc_Gl.producer.state != CDC_PRODUCER_STATE_WAIT) sleep (1); /* <- spin on state, no signal */cdc_wakeup_producer: cdc_Gl.producer.request = CDC_REQUEST_PRODUCER_NONE; pthread_cond_signal (&cdc_Gl.producer.wait_cond); /* <- fire-and-forget nudge */cdc_kill_producer: cdc_Gl.producer.request = CDC_REQUEST_PRODUCER_TO_BE_DEAD; while (cdc_Gl.producer.state != CDC_PRODUCER_STATE_DEAD) { pthread_cond_signal (&cdc_Gl.producer.wait_cond); sleep (1); } /* <- signal in loop: may be parked */The asymmetry is deliberate. cdc_pause_producer skips the signal because the daemon is still looping (10 ms) and will see TO_WAIT on its next tick. cdc_kill_producer must signal inside its spin, because the daemon may already be parked on wait_cond (e.g. backpressure) — a request flip alone would never be observed, so per-second signalling wakes it to re-check request. cdc_wakeup_producer is the inverse of pause, used after a re-seed (Chapter 4).
Invariant — request and state never alias. The consumer writes only
producer.request; the daemon writes onlyproducer.state. Both arevolatile. This single-writer-per-field discipline is why the pause/kill busy-waits are correct without the mutex: each side reads a field written by exactly one other thread. Break it — e.g. have the consumer setstatedirectly — and the busy-wait can deadlock or miss a transition.
3.9 Pausing the consumer (producer → consumer)
Section titled “3.9 Pausing the consumer (producer → consumer)”The reverse direction is far simpler: the producer flips consumer.request. No condvar, no busy-wait — the consumer checks its flag at its own fetch cadence:
// cdc_pause_consumer / cdc_wakeup_consumer -- src/transaction/log_manager.cvoid cdc_pause_consumer () { cdc_Gl.consumer.request = CDC_REQUEST_CONSUMER_TO_WAIT; }void cdc_wakeup_consumer () { cdc_Gl.consumer.request = CDC_REQUEST_CONSUMER_TO_RUN; }Used by the producer’s backpressure path (Chapter 5): when produced_queue_size exceeds 32 MB or the ring is full, the producer calls cdc_pause_consumer before parking, then cdc_wakeup_consumer after the queue drains. Purely advisory — the consumer may finish its in-flight fetch before honouring the flag.
stateDiagram-v2 direction LR [*] --> DEAD : cdc_initialize sets state DEAD DEAD --> RUN : daemon starts, sets state RUN RUN --> WAIT : request TO_WAIT seen, park on wait_cond WAIT --> RUN : wakeup_producer signals cond RUN --> DEAD : request TO_BE_DEAD seen, exit loop WAIT --> DEAD : kill signals cond then sees TO_BE_DEAD
Figure 3-2: producer state machine. state is daemon-owned; transitions are driven by the consumer-owned request the daemon polls.
The attach sequence ties §3.1-§3.9 together: server boot runs cdc_daemons_init once (queue + daemon, born parked in WAIT); per attach the handler calls cdc_set_configuration, seeds a start LSA (Chapter 4), and cdc_wakeup_producer; detach calls cdc_cleanup (queue kept); shutdown calls cdc_daemons_destroy → kill → destroy → cdc_finalize (queue deleted). No thread is created per session.
3.10 Chapter summary — key takeaways
Section titled “3.10 Chapter summary — key takeaways”- Bring-up is two-phase and ordered.
cdc_daemons_initrunscdc_initialize(queue alloc,temp_logbufalignment, zeroed state) strictly beforecdc_loginfo_producer_daemon_init(mutex/condvar + the single daemon), so the daemon never observes uninitialized state. - One daemon, born paused.
daemon_initsetsrequest = TO_WAITbeforecreate_daemon, so the body setsstate = RUN, immediately seesTO_WAIT, and parks onwait_conduntil a consumer seeds an LSA and callscdc_wakeup_producer. - Filters are session-scoped and ownership-transferring.
cdc_set_configurationstores the network-unpacked arrays and defensively re-frees any stale filter first, making reconnect after an abnormal client exit idempotent. - Teardown is nested by lifetime:
cdc_free_extraction_filter⊂cdc_cleanup_consumer⊂cdc_cleanup(session: pause, drain, keep queue) ⊂cdc_finalize(shutdown: kill, free tran_user,deletequeue). Pick by whether the queue should survive. cdc_reinitialize_queuere-seeks in place via three branches — empty (no-op), in-range (front trim with byte accounting), out-of-range (delete+new) — settingis_queue_reinitialized; the trim loop frees payloads but not entry structs (flagged TODO).- request and state are single-writer-per-field (
producer.requestconsumer-only,producer.statedaemon-only, bothvolatile), which is why pause/kill can busy-wait onstatewithout the mutex. - Pause vs kill differ by signalling.
cdc_pause_produceronly sets the request and polls (no-op whenstateis already WAIT);cdc_kill_producersignalswait_condinside its spin because the target may already be parked.
Chapter 4: Seeding and Validating the Start LSA
Section titled “Chapter 4: Seeding and Validating the Start LSA”The producer daemon (Ch. 3) is awake but idle until it knows where to
start walking. This chapter answers: how does the consumer pick the
starting LSA, and how does the server prove it is still safe to read?
A consumer names its start point two ways, both converging on
cdc_set_extraction_lsa:
- Explicit LSA — resume from an LSA durably committed downstream;
gated by
cdc_validate_lsa. - Wall-clock time — “start near 14:00 yesterday”; translated via
cdc_find_lsa, which scans active and archive volumes for a record whoseat_timebrackets the request.
The validation chain ties back to the same watermark the archive remover honours (cubrid-cdc.md §“Archive retention”).
4.1 The two seed sources and where they dispatch
Section titled “4.1 The two seed sources and where they dispatch”network_interface_sr.cpp routes the two requests to two handlers.
scdc_find_lsa (time path) calls cdc_find_lsa, accepts an adjusted
result, then runs the pause/reinit/wakeup choreography (§4.8) before
packing the reply. scdc_get_loginfo_metadata (explicit path) validates
first and seeds only on success.
// scdc_find_lsa -- src/communication/network_interface_sr.cpperror_code = cdc_find_lsa (thread_p, &input_time, &start_lsa);if (error_code == NO_ERROR || error_code == ER_CDC_ADJUSTED_LSA) /* <- adjusted is OK */ { if (cdc_Gl.producer.state != CDC_PRODUCER_STATE_WAIT) cdc_pause_producer (); cdc_set_extraction_lsa (&start_lsa); // ... cdc_reinitialize_queue / cdc_wakeup_producer, see 4.8 ... ptr = or_pack_int (reply, error_code); /* <- pack AFTER the daemon calls */ ptr = or_pack_log_lsa (ptr, &start_lsa); or_pack_int64 (ptr, input_time); }else goto error;
// scdc_get_loginfo_metadata -- src/communication/network_interface_sr.cppif (LSA_ISNULL (&cdc_Gl.consumer.next_lsa)) /* <- no cursor yet (fresh / restarted) */ { error_code = cdc_validate_lsa (thread_p, &start_lsa); /* must pass exactly */ if (error_code != NO_ERROR) goto error; /* <- reject: archive gone, bad LSA */ cdc_set_extraction_lsa (&start_lsa); }ER_CDC_ADJUSTED_LSA is not an error here: it is a notification
(ER_NOTIFICATION_SEVERITY) — “could not honour the exact request, here
is the closest LSA” — packed into the reply with the adjusted LSA and
time, surfaced to the client as CUBRID_LOG_SUCCESS_WITH_ADJUSTED_LSA
(§4.6).
flowchart LR T["set LSA by time"] --> FL["cdc_find_lsa"] X["set explicit LSA"] --> VL["cdc_validate_lsa"] FL -->|NO_ERROR or ADJUSTED_LSA| SE["cdc_set_extraction_lsa"] FL -->|hard error| ERR["reply error"] VL -->|NO_ERROR| SE VL -->|INVALID_LOG_LSA / ER_FAILED| ERR SE --> NEXT["next_extraction_lsa + consumer.next_lsa"]
Figure 4-1 — The two seed sources converge on cdc_set_extraction_lsa; time-based seeding tolerates an adjusted LSA, explicit seeding must pass cdc_validate_lsa exactly.
4.2 cdc_set_extraction_lsa — seeding the cursor
Section titled “4.2 cdc_set_extraction_lsa — seeding the cursor”// cdc_set_extraction_lsa -- src/transaction/log_manager.cpthread_mutex_lock (&cdc_Gl.producer.lock);LSA_COPY (&cdc_Gl.producer.next_extraction_lsa, lsa); /* <- producer cursor */pthread_mutex_unlock (&cdc_Gl.producer.lock);LSA_COPY (&cdc_Gl.consumer.next_lsa, lsa); /* <- consumer cursor */It writes two cursors. next_extraction_lsa (under producer.lock,
since the producer reads it every tick — Ch. 5) is where the forward walk
begins. consumer.next_lsa is where the consumer expects the next batch
to start; the LSA_EQ (&cdc_Gl.consumer.next_lsa, &start_lsa) guard at
the head of scdc_get_loginfo_metadata uses it to detect an out-of-sync
resume.
Key invariant: consumer.start_lsa is NOT seeded here. That third
cursor (Ch. 1) is the archive-remover watermark (§4.7); it stays at its
cdc_initialize value (LSA_SET_NULL) until the producer emits the
first batch in cdc_make_loginfo. The split keeps intent (start at LSA
X) separate from the commit that the oldest in-flight log info is at X:
if this function also moved start_lsa, a backward reseed would jump the
watermark behind log info that still exists at the new position.
4.3 cdc_find_lsa — translating a timestamp to an LSA
Section titled “4.3 cdc_find_lsa — translating a timestamp to an LSA”cdc_find_lsa (thread_p, *extraction_time, *start_lsa) takes a
wall-clock time_t (in/out) and returns the closest LSA at or after the
request, overwriting *extraction_time on adjustment. It probes the
active volume first, then walks archives newest-to-oldest:
// cdc_find_lsa -- src/transaction/log_manager.cint begin = log_Gl.hdr.last_deleted_arv_num; /* <- one before oldest kept archive */int end = log_Gl.hdr.nxarv_num - 1; /* <- newest archive number */error = cdc_get_start_point_from_file (thread_p, -1, &ret_lsa, &active_start_time); /* probe active */if (error == ER_FAILED || error == ER_LOG_READ) goto end; /* (1) hard read failure */else if (active_start_time != 0 && active_start_time <= *extraction_time) { /* (2) lands in active -> fine-scan; (2b) LSA_NOT_FOUND -> prev_lsa, time=now, ADJUSTED */ }else { /* (3/4/5) older than active start -> scan archives, else adjust to oldest */ }Every branch resolves to one of three outcomes — exact hit
(NO_ERROR), adjusted (ER_CDC_ADJUSTED_LSA + notification), or hard
failure (ER_FAILED / ER_LOG_READ):
| # | Condition | Action | Returns |
|---|---|---|---|
| 1 | active probe gives ER_FAILED/ER_LOG_READ | goto end | that error |
| 2a | request ≥ active start, exact match found | seed ret_lsa | NO_ERROR |
| 2b | request ≥ active start, newer than newest log | seed log_Gl.append.prev_lsa, time = now | ER_CDC_ADJUSTED_LSA |
| 3 | request older than active start, num_arvs > 0 | scan archives newest→oldest (below) | varies |
| 4 | older than active start, num_arvs == 0, active has a time | seed oldest active LSA, adjust time | ER_CDC_ADJUSTED_LSA |
| 5 | num_arvs == 0, active had no time-bearing record | seed log_Gl.append.prev_lsa, time = now | ER_CDC_ADJUSTED_LSA |
The archive scan (branch 3) walks down from end to just above begin,
bracketing to one volume (cdc_get_start_point_from_file, §4.4), then
fine-scanning inside it (cdc_get_lsa_with_start_point, §4.5):
// cdc_find_lsa -- src/transaction/log_manager.c (archive scan)for (int i = end; i > begin; i--) { error = cdc_get_start_point_from_file (thread_p, i, &ret_lsa, &archive_start_time); if (error != NO_ERROR) goto end; /* (3-err) propagate read error */ if (archive_start_time <= *extraction_time) { target_arv_num = i; break; } /* bracket */ }if (target_arv_num == -1) /* (3a) older than every archive */ { LSA_COPY (start_lsa, &ret_lsa); error = ER_CDC_ADJUSTED_LSA; } /* <- oldest LSA we have */else /* (3b) bracketed -> fine-scan */ { error = cdc_get_lsa_with_start_point (thread_p, extraction_time, &ret_lsa); if (error != NO_ERROR) error = ER_CDC_ADJUSTED_LSA; LSA_COPY (start_lsa, &ret_lsa); }Bracketing first (one header read per volume) avoids a forward scan of every archive — the reason for the two-phase split across §4.4 and §4.5.
4.4 cdc_get_start_point_from_file — one volume’s first time
Section titled “4.4 cdc_get_start_point_from_file — one volume’s first time”This helper answers “what is the first timestamped LSA in volume
arv_num?” (arv_num == -1 is the active volume). It reaches the
volume’s first page three ways — active header, already-mounted archive
header, or mounting the archive and reading its LOG_ARV_HEADER:
// cdc_get_start_point_from_file -- src/transaction/log_manager.c (page selection)if (arv_num == -1) { process_lsa.pageid = log_Gl.hdr.fpageid; process_lsa.offset = 0; } /* active */else if (log_Gl.archive.vdes != NULL_VOLDES && log_Gl.archive.hdr.arv_num == arv_num) { process_lsa.pageid = log_Gl.archive.hdr.fpageid; ... } /* already mounted */else if (fileio_is_volume_exist (arv_name) == true) { vdes = fileio_mount (...); if (vdes != NULL_VOLDES) /* <- mount succeeded */ { if (fileio_read (...) == NULL) { ...; return ER_LOG_READ; } if (difftime64 (arv_hdr->db_creation, log_Gl.hdr.db_creation) != 0) { ...; return ER_LOG_DOESNT_CORRESPOND_TO_DATABASE; } /* <- wrong DB's archive */ process_lsa.pageid = arv_hdr->fpageid; ... } }The db_creation check rejects an archive belonging to a different
database. If the archive is missing on disk, or fileio_mount returns
NULL_VOLDES, process_lsa stays LSA_INITIALIZER and the next guard
fires. The scan then walks forw_lsa for the first time-bearing record:
// cdc_get_start_point_from_file -- src/transaction/log_manager.c (scan for first time)if (LSA_ISNULL (&process_lsa)) /* file missing / mount failed */ { assert (!LSA_ISNULL (&process_lsa)); /* <- debug build aborts here */ er_set (..., ER_CDC_LSA_NOT_FOUND, ...); return ER_CDC_LSA_NOT_FOUND; }if ((error_code = logpb_fetch_page (...)) != NO_ERROR) return error_code;while (!LSA_ISNULL (&process_lsa)) /* forward-walk forw_lsa */ { log_type = log_rec_header->type; /* COMMIT/ABORT/HA_DUMMY carry at_time */ LOG_READ_ADD_ALIGN (thread_p, sizeof (*log_rec_header), &process_lsa, log_pgptr); /* on a matched type: LSA_COPY (ret_lsa, &cur_log_lsa); *time = at_time; return NO_ERROR; */ /* page boundary + LSA_ISNULL (&forw_lsa) -> ER_CDC_LSA_NOT_FOUND, else refetch page */ LSA_COPY (&process_lsa, &forw_lsa); }Note the assert (!LSA_ISNULL (&process_lsa)) as the first statement of
the null-LSA block: the mount-failure / file-missing case is a
should-not-happen path that aborts in a debug build; the
ER_CDC_LSA_NOT_FOUND return is only the release-build fallback, not a
designed-for outcome. Only three record types carry a usable at_time —
LOG_COMMIT, LOG_ABORT (LOG_REC_DONETIME), and
LOG_DUMMY_HA_SERVER_STATE (LOG_REC_HA_SERVER_STATE). The loop finds
one (returns NO_ERROR) or runs out of log (LSA_ISNULL (&forw_lsa) at
a page boundary -> ER_CDC_LSA_NOT_FOUND); LOG_READ_ADD_ALIGN is the
same alignment helper log_reader wraps (cubrid-cdc.md §“Log walker”).
4.5 cdc_get_lsa_with_start_point — fine scan inside a volume
Section titled “4.5 cdc_get_lsa_with_start_point — fine scan inside a volume”Where §4.4 finds the first time in a volume to bracket, this helper
scans forward from a known start LSA to the first record whose time ≥
the request (*time and *start_lsa both in/out). It reuses §4.4’s
per-record machinery, differing only in the >= *time match compare:
// cdc_get_lsa_with_start_point -- src/transaction/log_manager.cif (logpb_fetch_page (...) != NO_ERROR) return ER_FAILED; /* page fetch failed */while (!LSA_ISNULL (&process_lsa)) { if (at_time >= *time) /* first record at/after request */ { *time = at_time; LSA_COPY (start_lsa, ¤t_lsa); return NO_ERROR; } LSA_COPY (&process_lsa, &forw_lsa); } /* else advance / refetch page */return ER_CDC_LSA_NOT_FOUND; /* ran off the end of the log */Three branches — ER_FAILED (fetch), NO_ERROR (match), and
ER_CDC_LSA_NOT_FOUND (off the end, also reached at a page boundary when
forw_lsa is null). The >= is why CDC start time is “at or after”; in
branch 2b/3b a LSA_NOT_FOUND here is downgraded to ER_CDC_ADJUSTED_LSA
and the caller substitutes the latest LSA.
4.6 The validation chain — cdc_validate_lsa and cdc_check_lsa_range
Section titled “4.6 The validation chain — cdc_validate_lsa and cdc_check_lsa_range”For the explicit-LSA path, the seed must be proven readable.
cdc_validate_lsa runs four gates, then a record-boundary walk:
// cdc_validate_lsa -- src/transaction/log_manager.cif (LSA_ISNULL (lsa)) { ...; return ER_CDC_INVALID_LOG_LSA; } /* (1) null LSA */if (lsa->pageid >= LOGPAGEID_MAX) { ...; return ER_CDC_INVALID_LOG_LSA; } /* (2) absurd pageid */if (cdc_check_lsa_range (thread_p, lsa) != NO_ERROR) /* (3) outside kept range */ return ER_CDC_INVALID_LOG_LSA;if (logpb_fetch_page (thread_p, lsa, LOG_CS_SAFE_READER, log_page_p) != NO_ERROR) /* (4) */ return ER_FAILED;/* (5) walk record headers on the page; LSA_EQ a record start -> NO_ERROR, else INVALID_LOG_LSA */Gate (3), cdc_check_lsa_range, computes the readable window’s bounds
and checks containment, reading the oldest kept archive’s header with the
same mount/read/db_creation pattern as §4.4 (same ER_LOG_READ /
ER_LOG_DOESNT_CORRESPOND_TO_DATABASE error arms):
// cdc_check_lsa_range -- src/transaction/log_manager.cint begin = log_Gl.hdr.last_deleted_arv_num + 1; /* <- oldest STILL-kept archive */LOG_LSA nxio_lsa = log_Gl.append.get_nxio_lsa (); /* <- appended tail (exclusive upper) */if (num_arvs == 0) { first_lsa.pageid = log_Gl.hdr.fpageid; } /* nothing archived yet */else if (fileio_is_volume_exist (arv_name) == true) { /* mount + read header (else ER_LOG_READ / ER_LOG_DOESNT_CORRESPOND_TO_DATABASE) */ first_lsa.pageid = arv_hdr->fpageid; } /* oldest kept archive's first page */else { first_lsa.pageid = log_Gl.hdr.fpageid; } /* archive file gone */if (LSA_GE (lsa, &first_lsa) && LSA_LT (lsa, &nxio_lsa)) /* <- [first_lsa, nxio_lsa) */ return NO_ERROR;return ER_CDC_INVALID_LOG_LSA;Key invariant: the readable window is the half-open interval
[first_lsa, nxio_lsa) (lower bound = first page of the oldest
still-kept archive per the block above; upper bound = the exclusive tail
the appender writes next). A seed below first_lsa means its archive
was already removed (§4.7); a seed at or past nxio_lsa points at
unwritten log. Either is rejected with ER_CDC_INVALID_LOG_LSA — the
concrete reason a removed archive invalidates a stale resume LSA.
Gate (5) confirms the LSA names a real record boundary — the loop
walks headers on the fetched page until LSA_EQ (&process_lsa, lsa)
(NO_ERROR) or leaves the page (ER_CDC_INVALID_LOG_LSA). Figure 4-2
collects all five branches: four reject as ER_CDC_INVALID_LOG_LSA,
page-fetch failure as ER_FAILED.
flowchart TB
A["cdc_validate_lsa(lsa)"] --> B{"LSA null?"}
B -->|yes| E1["ER_CDC_INVALID_LOG_LSA"]
B -->|no| C{"pageid >= LOGPAGEID_MAX?"}
C -->|yes| E1
C -->|no| D["cdc_check_lsa_range"]
D -->|"not in [first_lsa, nxio_lsa)"| E1
D -->|in range| F{"logpb_fetch_page ok?"}
F -->|no| E2["ER_FAILED"]
F -->|yes| G["walk record headers on page"]
G -->|"LSA_EQ a record start"| OK["NO_ERROR"]
G -->|"left page, no match"| E1
Figure 4-2 — cdc_validate_lsa gate order. cdc_check_lsa_range is the gate that fails when an archive holding the seed LSA has been removed.
On the client, cubrid_log_find_start_lsa decodes the reply; its first
arm catches both NO_ERROR and ER_CDC_ADJUSTED_LSA and returns
CUBRID_LOG_SUCCESS, leaving the CUBRID_LOG_SUCCESS_WITH_ADJUSTED_LSA
arm unreachable as written (Cross-check note, Ch. 9).
4.7 cdc_min_log_pageid_to_keep — why the watermark is the cursor
Section titled “4.7 cdc_min_log_pageid_to_keep — why the watermark is the cursor”The archive remover (cubrid-cdc.md §“Archive retention”) calls this to learn the smallest pageid any CDC consumer still needs:
// cdc_min_log_pageid_to_keep -- src/transaction/log_manager.creturn cdc_Gl.consumer.start_lsa.pageid;Per the §4.2 invariant, consumer.start_lsa advances only via
cdc_make_loginfo. Before any batch it is LSA_SET_NULL, whose pageid
NULL_PAGEID (-1) the remover reads as “no constraint.” Once a batch
exists, start_lsa.pageid is the smallest page any unsent log info sits
on, so the remover keeps every archive from that page forward. A backward
seed lets start_lsa follow the producer backward — which is why such a
seed must pass cdc_validate_lsa (§4.6) first.
Cross-check / version drift: the companion (cubrid-cdc.md §“Archive
retention”) states this returns MAX_LOG_PAGEID with no consumer; the
current source does not — it returns consumer.start_lsa.pageid
unconditionally and leans on the NULL-pageid sentinel for that case. The
companion’s wording is the intended contract, not the literal code.
4.8 How next_extraction_lsa reaches the producer loop
Section titled “4.8 How next_extraction_lsa reaches the producer loop”After cdc_set_extraction_lsa writes producer.next_extraction_lsa, the
handlers call cdc_reinitialize_queue (discard already-produced log info
predating the new seed) and cdc_wakeup_producer (flip the producer out
of WAIT); the time path additionally calls cdc_pause_producer first
(see the §4.1 excerpt) so the seed never races a running tick. On its
next tick the producer reads next_extraction_lsa as its walk origin
(Ch. 5) — the single hand-off between the consumer’s “start here” intent
and the producer’s forward walk.
4.9 Chapter summary — key takeaways
Section titled “4.9 Chapter summary — key takeaways”- Both seed sources converge on
cdc_set_extraction_lsa, which writesproducer.next_extraction_lsa(underproducer.lock) andconsumer.next_lsa, but deliberately notconsumer.start_lsa. cdc_find_lsais a two-phase time→LSA lookup — bracket to a volume (cdc_get_start_point_from_file), then fine-scan inside (cdc_get_lsa_with_start_point) — and every branch returns exact, adjusted (ER_CDC_ADJUSTED_LSA), or hard error.- Only
LOG_COMMIT,LOG_ABORT,LOG_DUMMY_HA_SERVER_STATEcarry a usableat_time; the scan uses>=(at-or-after). The file-missing arm incdc_get_start_point_from_fileis assert-guarded. cdc_validate_lsaruns four gates — null, absurd pageid,cdc_check_lsa_range, page fetch — plus a record-boundary walk; any failure rejects the seed (mostlyER_CDC_INVALID_LOG_LSA).- The readable window is the half-open interval
[first_lsa, nxio_lsa); a seed belowfirst_lsais the “archive was removed” case. cdc_min_log_pageid_to_keepreturnsconsumer.start_lsa.pageid(advanced only viacdc_make_loginfo), so a backward reseed must passcdc_validate_lsafirst.- Version drift: the companion’s “
MAX_LOG_PAGEIDwhen no consumer” is intent; the code relies on the NULL-pageid sentinel, and the client’sSUCCESS_WITH_ADJUSTED_LSAarm is currently unreachable.
Chapter 5: The Producer Daemon Outer Loop
Section titled “Chapter 5: The Producer Daemon Outer Loop”This chapter dissects cdc_loginfo_producer_execute (in log_manager.c),
the body the cdc-loginfo-producer daemon runs. The high-level companion
(cubrid-cdc.md, “Producer/consumer split” and “Backpressure”) establishes
why CDC uses a separate producer thread feeding a bounded queue; here we
trace how one invocation drives the forward log walk and decides — branch by
branch — to keep walking, pause, sleep, or die. Chapter 3 covered the
request/state wiring, Chapter 4 the seeding of next_extraction_lsa,
Chapter 6 the cdc_log_extract call. The daemon has a 10 ms looper, but this
function loops internally and returns only on shutdown — so the period matters
only on the first dispatch.
5.1 Entry, the three state fields, and the loop condition
Section titled “5.1 Entry, the three state fields, and the loop condition”Three globals govern the loop, all in cdc_Gl.producer (the CDC_PRODUCER
struct, tabulated in Chapter 1). Two are volatile because other threads
(the session/API side, Chapter 8) write them and this thread polls without a
lock:
| Field | Role in this loop | Why it exists |
|---|---|---|
state (volatile CDC_PRODUCER_STATE) | This thread publishes its liveness: RUN, WAIT, DEAD. Others spin-wait on it. | Lets cdc_pause_producer/cdc_kill_producer confirm the daemon parked or exited. |
request (volatile CDC_PRODUCER_REQUEST) | This thread reads commands: TO_WAIT, TO_BE_DEAD, NONE. | The control channel from the API side. |
produced_queue_size (int) | Running byte total of payloads pushed but not yet acknowledged. | The backpressure accumulator driving the queue-full guard (5.3). |
The prologue tags the thread as a CDC daemon and declares it RUN:
// cdc_loginfo_producer_execute -- src/transaction/log_manager.cTHREAD_ENTRY *thread_p = &thread_ref;thread_p->is_cdc_daemon = true; /* <- log subsystem knows reads come from CDC */cdc_Gl.producer.state = CDC_PRODUCER_STATE_RUN;while (cdc_Gl.producer.request != CDC_REQUEST_PRODUCER_TO_BE_DEAD) { // ... loop body ... }cdc_Gl.producer.state = CDC_PRODUCER_STATE_DEAD;The predicate checks only TO_BE_DEAD. TO_WAIT does not exit — it is
an inner branch (5.2) that parks and continues. The only way out is a
controller setting request = TO_BE_DEAD.
Invariant — state must reach DEAD exactly once on exit. The loop
sets state = RUN on entry and state = DEAD right after the loop ends;
cdc_kill_producer sets request = TO_BE_DEAD then busy-waits while (state != CDC_PRODUCER_STATE_DEAD). Any return that skipped state = DEAD
(e.g. the dead error: label, 5.7) would spin that controller forever.
Figure 5-1 shows one iteration’s branch structure.
5.2 Branch A — the control pause on wait_cond
Section titled “5.2 Branch A — the control pause on wait_cond”The loop’s first statement reads the control channel:
// cdc_loginfo_producer_execute -- src/transaction/log_manager.cif (cdc_Gl.producer.request == CDC_REQUEST_PRODUCER_TO_WAIT) { cdc_Gl.producer.state = CDC_PRODUCER_STATE_WAIT; /* <- publish parked */
pthread_mutex_lock (&cdc_Gl.producer.lock); pthread_cond_wait (&cdc_Gl.producer.wait_cond, &cdc_Gl.producer.lock); pthread_mutex_unlock (&cdc_Gl.producer.lock);
cdc_Gl.producer.state = CDC_PRODUCER_STATE_RUN; /* <- republish live */ continue; /* <- re-test request */ }This is the requested pause (distinct from the queue-full pause in 5.3). It
publishes WAIT before blocking so cdc_pause_producer’s while (state != CDC_PRODUCER_STATE_WAIT) busy-wait observes the park. On wakeup it
continues rather than assuming “resume work”, so a wakeup that was really a
shutdown exits cleanly via the re-tested while. The lock/wait_cond pair
is initialized in cdc_loginfo_producer_daemon_init (Chapter 3).
Note that this code does not close the classic lost-wakeup window the
textbook way. The producer tests request outside the mutex, then takes the
lock and cond_waits; a controller that sets request and signals in the gap
between those two steps would have its signal dropped. The signalers
(cdc_wakeup_producer, cdc_kill_producer) call pthread_cond_signal
without holding cdc_Gl.producer.lock, so the lock does not serialize the
request-store against the wait. The design tolerates the race differently:
cdc_kill_producer re-signals in a while (state != ...DEAD) { signal; sleep(1); }
loop, and cdc_pause_producer does not signal at all — it sets request and
spins on state, relying on the producer to re-read request at the next loop
top. A single dropped signal therefore costs at most one looper period, not a
permanent hang.
5.3 Branch B — the queue-full backpressure pause
Section titled “5.3 Branch B — the queue-full backpressure pause”If not asked to pause, the loop checks the bounded queue via two OR’d triggers:
// cdc_loginfo_producer_execute -- src/transaction/log_manager.cif (cdc_Gl.producer.produced_queue_size >= MAX_CDC_LOGINFO_QUEUE_SIZE || cdc_Gl.loginfo_queue->is_full ()) { cdc_Gl.producer.state = CDC_PRODUCER_STATE_WAIT; cdc_pause_consumer (); /* <- ask consumer to drain & stop */ // ... lock / cond_wait / unlock as in Branch A, then state = RUN ... cdc_Gl.producer.produced_queue_size -= cdc_Gl.consumer.consumed_queue_size; cdc_Gl.consumer.consumed_queue_size = 0; /* <- reconcile counters */
cdc_wakeup_consumer (); continue; }The triggers measure different things:
produced_queue_size >= MAX_CDC_LOGINFO_QUEUE_SIZE— a byte budget (32 MB, inlog_impl.h) accumulatingtmp->lengthper produce (5.6); a few huge row images trip it first.cdc_Gl.loginfo_queue->is_full ()— a slot count on thelockfree::circular_queue<CDC_LOGINFO_ENTRY *>ring; many tiny entries trip it first.
Unlike Branch A, this branch also pauses (cdc_pause_consumer sets
consumer.request = CDC_REQUEST_CONSUMER_TO_WAIT) and later re-wakes the
consumer, whose drain makes room.
Invariant — produced_queue_size is reconciled at a single serialized
point. The producer only adds tmp->length, the consumer only adds to its
own consumed_queue_size, and this is the lone subtraction — run only after
the consumer has paused and reported a stable figure. The pause_consumer → cond_wait → reconcile → wakeup_consumer ordering serializes it; a concurrent
subtraction would desync the counters, wedging the producer or overflowing the
queue.
5.4 Branch C — the tail-of-log sleep
Section titled “5.4 Branch C — the tail-of-log sleep”Past both guards, the loop compares the appender’s next-output LSA to the walk:
// cdc_loginfo_producer_execute -- src/transaction/log_manager.cnxio_lsa = log_Gl.append.get_nxio_lsa ();
pthread_mutex_lock (&cdc_Gl.producer.lock);
if (LSA_ISNULL (&cdc_Gl.producer.next_extraction_lsa) || LSA_GE (&cdc_Gl.producer.next_extraction_lsa, &nxio_lsa)) { /* LOG_HA_DUMMY_SERVER_STATUS is appended every 1 second and flushed. * So it is expected to be woken up by looper within period of looper */ pthread_mutex_unlock (&cdc_Gl.producer.lock); sleep (1); continue; }Note the source comment misspells the record as LOG_HA_DUMMY_SERVER_STATUS;
the real enum is LOG_DUMMY_HA_SERVER_STATE. get_nxio_lsa returns the
appender’s next-i/o LSA — the high-water mark of durably reachable log. The
branch fires when next_extraction_lsa is NULL (never seeded or reset —
Chapter 4) or >= nxio_lsa (the walk caught the tail; no fully-written record
beyond the cursor). It then releases the producer lock, sleep(1)s, and
continues. The one-second sleep is tied to the LOG_DUMMY_HA_SERVER_STATE
heartbeat, flushed roughly every second on an active server, so after sleeping
nxio_lsa has advanced by at least the heartbeat — the walk is never starved
even on an idle database.
Invariant — the producer lock guards next_extraction_lsa against torn
reads. The API side can overwrite this LOG_LSA, so it is read here and in
5.5 under the lock. The explicit unlock on the sleep path keeps that read
atomic; omitting it would deadlock every API caller.
5.5 The process_lsa ↔ next_extraction_lsa handoff
Section titled “5.5 The process_lsa ↔ next_extraction_lsa handoff”Still under the lock, it resets the scratch entry and syncs cursors:
// cdc_loginfo_producer_execute -- src/transaction/log_manager.c// ... reset scratch log_info_entry: length=0, next_lsa=NULL, log_info=NULL ...if (LSA_ISNULL (&process_lsa)) { LSA_COPY (&process_lsa, &cdc_Gl.producer.next_extraction_lsa); /* first iter or after API reset */ }else { LSA_COPY (&cdc_Gl.producer.next_extraction_lsa, &process_lsa); /* steady state */ }
assert (!LSA_ISNULL (&process_lsa));LSA_COPY (&cur_log_rec_lsa, &process_lsa);
pthread_mutex_unlock (&cdc_Gl.producer.lock);process_lsa is the thread-private walk cursor cdc_log_extract advances
(Chapter 6); next_extraction_lsa is the shared, API-visible cursor. A
NULL private cursor (first iteration or after a reset) pulls from the
authoritative shared seed; otherwise the private walk pushes into the shared
cursor for API readers. cur_log_rec_lsa snapshots the record’s own LSA (later
last_loginfo_queue_lsa, 5.6) before extraction mutates process_lsa. The
lock is dropped before the expensive cdc_log_extract.
5.6 The extract call, error classification, and the produce
Section titled “5.6 The extract call, error classification, and the produce”With the lock dropped, it extracts one record:
// cdc_loginfo_producer_execute -- src/transaction/log_manager.cerror = cdc_log_extract (thread_p, &process_lsa, &log_info_entry);if (!(error == NO_ERROR || error == ER_CDC_LOGINFO_ENTRY_GENERATED)) { cdc_log ("... cdc_log_extract() error(%d) ...", error, LSA_AS_ARGS (&cur_log_rec_lsa)); if (!CDC_IS_IGNORE_LOGINFO_ERROR (error)) { continue; /* <- non-ignorable hard error: retry from same cursor */ } /* else: ignorable -- fall through, treat like "nothing produced" */ }cdc_log_extract (Chapter 6) returns one of three outcome classes:
-
ER_CDC_LOGINFO_ENTRY_GENERATED— “a payload was built intolog_info_entry”. The!(... || error == ...GENERATED)test excludes it from the error branch; the only class that produces. -
NO_ERROR— the record advanced the walk but produced no payload; it falls past both branches. -
Anything else — an error, logged, then classified by
CDC_IS_IGNORE_LOGINFO_ERROR:// CDC_IS_IGNORE_LOGINFO_ERROR -- src/transaction/log_manager.c#define CDC_IS_IGNORE_LOGINFO_ERROR(ERROR) \(((ERROR) == ER_CDC_IGNORE_LOG_INFO) \|| ((ERROR) == ER_CDC_IGNORE_LOG_INFO_INTERNAL) \|| ((ERROR) == ER_CDC_IGNORE_TRANSACTION))A non-ignorable error
continues without advancing the cursor — a retry-from-here that does not kill the daemon. An ignorable error (a filtered transaction or internal-only record CDC skips) falls through theif;cdc_log_extractalready advancedprocess_lsapast it, so the fall-through just means “no produce”.
The produce path runs only for class 1:
// cdc_loginfo_producer_execute -- src/transaction/log_manager.cif (error == ER_CDC_LOGINFO_ENTRY_GENERATED) { CDC_LOGINFO_ENTRY *tmp = (CDC_LOGINFO_ENTRY *) malloc (sizeof (CDC_LOGINFO_ENTRY)); if (tmp == NULL) { // ... er_set ER_OUT_OF_VIRTUAL_MEMORY ... error = ER_OUT_OF_VIRTUAL_MEMORY; continue; /* <- OOM: drop and retry */ }
tmp->length = log_info_entry.length; tmp->log_info = log_info_entry.log_info; /* <- transfer payload ownership */ LSA_COPY (&tmp->next_lsa, &process_lsa); /* <- now-advanced cursor */
if (cdc_Gl.is_queue_reinitialized) { free_and_init (tmp->log_info); free_and_init (tmp); cdc_Gl.is_queue_reinitialized = false; continue; /* <- mid-flight reset: discard */ }
cdc_Gl.loginfo_queue->produce (tmp); cdc_Gl.producer.produced_queue_size += tmp->length; LSA_COPY (&cdc_Gl.last_loginfo_queue_lsa, &cur_log_rec_lsa); }Walking its branches:
mallocthe heap entry. The reused stacklog_info_entryis copied onto a heapCDC_LOGINFO_ENTRY;tmp->log_infotakes ownership of the extracted buffer (a pointer re-home),tmp->next_lsarecords the advanced cursor.mallocfailure. SetsER_OUT_OF_VIRTUAL_MEMORYandcontinues;log_info_entry.log_infois leaked — a known rough edge (OOM is fatal anyway).is_queue_reinitializedmid-flight drop. If a controller reset the queue (Chapter 8) during extraction, the entry belongs to the old generation, so the branch frees both, clears the flag, andcontinues — the one place the flag is consumed, exactly once per reset.- The produce.
produce(tmp)enqueues the pointer;produced_queue_size += tmp->lengthfeeds the byte-budget guard (5.3) andlast_loginfo_queue_lsarecords the enqueued record’s own LSA (notnext_lsa).
Invariant — after produce(tmp) succeeds, tmp belongs to the consumer.
Every pre-produce abort frees tmp itself; no path frees it after produce.
The handoff is a raw pointer through the lock-free queue, so a stray free here
would corrupt the consumer.
flowchart TD
A["loop top"] --> B{"request==TO_WAIT?"}
B -- yes --> A
B -- no --> C{"queue full?"}
C -- yes --> A
C -- no --> E{"tail: NULL or<br/>>= nxio_lsa?"}
E -- yes --> A
E -- no --> G["cdc_log_extract"]
G --> H{"error class"}
H -- "hard, non-ignorable" --> A
H -- "ignorable / NO_ERROR" --> A
H -- "GENERATED" --> K{"tmp NULL?"}
K -- yes --> A
K -- no --> L{"is_queue_reinitialized?"}
L -- yes --> A
L -- no --> M["produce; bookkeeping"]
M --> A
Figure 5-1. Branch skeleton of one loop iteration; each decision and its
continue/produce outcome is traced in 5.2-5.6. The only exit (not shown) is
request == TO_BE_DEAD failing the while predicate.
5.7 Loop exit and the two trailing labels
Section titled “5.7 Loop exit and the two trailing labels”When request becomes TO_BE_DEAD the while fails and control falls out:
// cdc_loginfo_producer_execute -- src/transaction/log_manager.ccdc_Gl.producer.state = CDC_PRODUCER_STATE_DEAD; /* releases cdc_kill_producer */end: thread_p->is_cdc_daemon = false; return;error: /* <- bypasses the state = DEAD above */ thread_p->is_cdc_daemon = false; return;The post-loop state = DEAD releases any controller spinning in
cdc_kill_producer; the function then clears is_cdc_daemon and returns. The
end: and error: labels are unreachable — no goto targets either
(error handling uses continue). For a modifier: a future goto error would
bypass state = DEAD, violating the 5.1 liveness invariant and wedging any
killing controller.
5.8 Chapter summary — key takeaways
Section titled “5.8 Chapter summary — key takeaways”- One function, one long-lived loop. It owns the thread until
request == TO_BE_DEAD;TO_WAITis an inner park-and-continue branch, never an exit, andstatemust reachDEADonce. - Two pauses share
wait_cond. Branch A (TO_WAIT) is resolved by a controller; Branch B (queue full) also pauses the consumer, then on wakeup reconcilesproduced_queue_size -= consumed_queue_size— the lone subtraction point for the byte budget. - The queue-full guard has two OR’d triggers. A 32 MB byte budget
(
MAX_CDC_LOGINFO_QUEUE_SIZE) and a slot-count limit (loginfo_queue->is_full()); large payloads trip the former, tiny ones the latter. - The tail guard sleeps 1 s, synced to the HA heartbeat. When
next_extraction_lsais NULL or>= nxio_lsa, the loop sleeps a second, by which time theLOG_DUMMY_HA_SERVER_STATEheartbeat has advanced the tail — forward progress even with zero user writes. - Two cursors under the producer lock. A NULL private
process_lsapulls from the sharednext_extraction_lsa; otherwise it pushes into the shared cursor. The lock guards both against torn reads, dropped before extract. ER_CDC_LOGINFO_ENTRY_GENERATEDis the produce contract. Only it triggers a heapCDC_LOGINFO_ENTRY, aproduce, and theproduced_queue_size/last_loginfo_queue_lsabookkeeping;NO_ERRORand ignorable errors only advance the walk, hard errorscontinuewithout dying. Afterproducethe entry is the consumer’s (pre-produce aborts freetmpthemselves); the deadend:/error:labels are unreachable.
Chapter 6: Per Record Dispatch and Reconstructing One Change
Section titled “Chapter 6: Per Record Dispatch and Reconstructing One Change”The producer loop (Chapter 5) calls cdc_log_extract per record. This
chapter answers: for a single log record at the cursor, how does CDC
decide what kind of event it is and build the in-memory
CDC_LOGINFO_ENTRY? Payload materialization (cdc_get_recdes /
cdc_make_dml_loginfo) is Chapter 7. For the conceptual model of
supplemental logging and the four event classes, see companion
cubrid-cdc.md §“LOG_SUPPLEMENTAL_INFO” and §“cdc_make_loginfo”.
6.1 The two record headers this code reads
Section titled “6.1 The two record headers this code reads”cdc_log_extract reads one fixed header for the record type, then — for
supplemental records — a second naming the supplement sub-type.
LOG_RECORD_HEADER (log_record.hpp) is the universal prefix of every
record, read via LOG_GET_LOG_RECORD_HEADER (a cast at
log_page_p->area + lsa->offset).
| Field | Role | Why it exists |
|---|---|---|
prev_tranlsa | LSA of the previous record of the same transaction | Backward undo chain; unused by CDC |
back_lsa | Backward log address (previous physical record) | Physical chaining; unused by CDC |
forw_lsa | Forward log address (next physical record) | Copied into next_log_rec_lsa; LSA_ISNULL is the “no more log” sentinel |
trid | Transaction identifier of this record | Key into tran_user / tran_ignore |
type | LOG_RECTYPE discriminant (commit, abort, supplemental, …) | What the outer switch dispatches on |
LOG_REC_SUPPLEMENT (log_record.hpp) follows the header only for
LOG_SUPPLEMENTAL_INFO.
| Field | Role | Why it exists |
|---|---|---|
rec_type | SUPPLEMENT_REC_TYPE discriminant (TRAN_USER, DDL, INSERT, UPDATE, DELETE, TRIGGER_*) | What the inner switch dispatches on |
length | Byte length of the supplement payload | Sizes buffers; cdc_log_extract recomputes supplement_length from the recovered RECDES rather than trusting this |
Invariant — the supplement payload is laid out by rec_type. For
INSERT/UPDATE/DELETE the payload is CLASS OID | UNDO LSA | REDO LSA
(DELETE omits REDO, INSERT omits UNDO). cdc_log_extract reads
classoid from offset 0 and the LSAs from fixed offsets, correct only
because the producer (Chapter 2) emitted them in this order; if they
diverge, CDC reads a garbage OID and silently mis-filters.
graph LR
subgraph page["log page at process_lsa"]
H["LOG_RECORD_HEADER<br/>type, trid, forw_lsa"]
S["LOG_REC_SUPPLEMENT<br/>rec_type, length"]
P["payload<br/>OID | undo_lsa | redo_lsa"]
end
H -->|"only if type==SUPPLEMENTAL_INFO"| S
S -->|"recovered via cdc_get_undo_record"| P
Figure 6-1. The header chain for a supplemental record.
6.2 Page fetch and the temp_logbuf double buffer
Section titled “6.2 Page fetch and the temp_logbuf double buffer”After copying process_lsa into cur_log_rec_lsa, the function fetches
the page via CDC_GET_TEMP_LOGPAGE, then seeds tmpbuf_index = process_lsa->pageid % 2. The producer owns a two-slot cache
temp_logbuf[2] (CDC_TEMP_LOGBUF, log_impl.h); each slot holds a
log_page_p into a backing buffer surviving across calls.
CDC_GET_TEMP_LOGPAGE is a lookup with fetch-on-miss:
// CDC_GET_TEMP_LOGPAGE -- src/transaction/log_impl.hif (cdc_Gl.producer.temp_logbuf[(process_lsa)->pageid % 2].log_page_p->hdr.logical_pageid != (process_lsa)->pageid) { /* <- miss: wrong page in slot */ if (logpb_fetch_page (...) != NO_ERROR) { goto error; } /* <- why the error: label exists */ memcpy (cdc_Gl.producer.temp_logbuf[...].log_page_p, (log_page_p), IO_MAX_PAGE_SIZE); }else (log_page_p) = cdc_Gl.producer.temp_logbuf[(process_lsa)->pageid % 2].log_page_p; /* <- hit */A record can straddle a page boundary; when the cursor crosses the edge
(LOG_READ_ADVANCE_WHEN_DOESNT_FIT), log_page_p points at page N+1
while N may still be needed. Parity indexing keeps N in slot N%2
and N+1 in (N+1)%2. The other two macros mirror CDC_GET:
CDC_CHECK_TEMP_LOGPAGE fires when process_lsa->pageid % 2 != *tmpbuf_index, flipping *tmpbuf_index and memcpy-ing the crossed-into
page into that slot; CDC_UPDATE_TEMP_LOGPAGE is the inverse, forcing
a re-fetch (logpb_fetch_page else goto error, then memcpy) when the
cached page already is the requested one, used on the end-of-log
early-out and after the supplement copy to reflect disk.
Invariant — tmpbuf_index always equals process_lsa->pageid % 2.
Seeded after the initial CDC_GET_TEMP_LOGPAGE, maintained by every
post-advance CDC_CHECK_TEMP_LOGPAGE. A branch advancing without a
matching CHECK would let a later memcpy corrupt the still-needed
page.
6.3 The end-of-log early-out
Section titled “6.3 The end-of-log early-out”Immediately after reading the header, before the switch:
// cdc_log_extract -- src/transaction/log_manager.cif (log_rec_header->type == LOG_END_OF_LOG || LSA_ISNULL (&log_rec_header->forw_lsa)) { CDC_UPDATE_TEMP_LOGPAGE (thread_p, process_lsa, log_page_p); /* <- refresh slot from disk */ error = ER_CDC_NULL_EXTRACTION_LSA; goto error; } /* <- rewinds cursor, see 6.8 */log_type = log_rec_header->type; trid = log_rec_header->trid;LSA_COPY (&next_log_rec_lsa, &log_rec_header->forw_lsa); /* <- cursor lands here on success */// ... LOG_READ_ADD_ALIGN past the header ...LOG_END_OF_LOG or a null forw_lsa both mean CDC reached the tail; it
returns ER_CDC_NULL_EXTRACTION_LSA (-1293) and via error: (6.8)
rewinds to cur_log_rec_lsa, so the next iteration retries the same
LSA once more log arrives (the “wait at the tail” of companion §“CDC
producer”). Otherwise it captures log_type, trid, next_log_rec_lsa
and steps past the header.
6.4 The outer switch on LOG_RECTYPE
Section titled “6.4 The outer switch on LOG_RECTYPE”flowchart TD
A["read LOG_RECORD_HEADER"] --> B{"end-of-log or forw_lsa null"}
B -->|yes| EOL["ER_CDC_NULL_EXTRACTION_LSA -> error"]
B -->|no| SW{"switch log_type"}
SW -->|COMMIT / ABORT| DCL["6.5 DCL arm"]
SW -->|DUMMY_HA_SERVER_STATE| TMR["6.6 Timer arm"]
SW -->|SUPPLEMENTAL_INFO| SUP["6.7 Supplement arm"]
SW -->|default| END["fall through to end"]
Figure 6-2. Only three record types produce events; every other
LOG_RECTYPE hits default: break (cursor still advances to
next_log_rec_lsa).
6.5 LOG_COMMIT / LOG_ABORT — emitting a DCL event
Section titled “6.5 LOG_COMMIT / LOG_ABORT — emitting a DCL event”Both share one arm: advance past LOG_REC_DONETIME, re-sync, four gates.
// cdc_log_extract -- src/transaction/log_manager.c (LOG_COMMIT / LOG_ABORT)// ... advance past donetime, CDC_CHECK_TEMP_LOGPAGE ...if (cdc_Gl.producer.tran_ignore.count (trid) != 0) /* gate 1: poisoned trid */ { cdc_Gl.producer.tran_ignore.erase (trid); break; } /* <- forget it; nothing allocated */donetime = (LOG_REC_DONETIME *) (log_page_p->area + process_lsa->offset);if (cdc_Gl.producer.tran_user.count (trid) == 0) goto end; /* gate 2: no user -> harmless skip */else tran_user = cdc_Gl.producer.tran_user.at (trid);if (!cdc_is_filtered_user (tran_user)) break; /* gate 3: user not extracted */if ((error = cdc_make_dcl_loginfo (donetime->at_time, trid, tran_user, log_type, log_info_entry)) != ER_CDC_LOGINFO_ENTRY_GENERATED) goto error; /* gate 4 */free_and_init (tran_user); cdc_Gl.producer.tran_user.erase (trid); break; /* <- transaction done */Gate 1 erases the ignore marker set by 6.7’s abort fallback. Gate 4
treats the sentinel as success (6.13); any other value goes to
error:. On success the cached user is freed and erased before
break, so end: never sees it.
6.6 LOG_DUMMY_HA_SERVER_STATE — emitting a Timer event
Section titled “6.6 LOG_DUMMY_HA_SERVER_STATE — emitting a Timer event”// cdc_log_extract -- src/transaction/log_manager.c (LOG_DUMMY_HA_SERVER_STATE)// ... advance past ha_dummy, CDC_CHECK_TEMP_LOGPAGE, cast ha_dummy ...if ((error = cdc_make_timer_loginfo (ha_dummy->at_time, trid, NULL, log_info_entry)) == ER_OUT_OF_VIRTUAL_MEMORY) goto error;break;The HA dummy’s at_time heartbeat becomes a CDC_TIMER event with a
NULL user. Only ER_OUT_OF_VIRTUAL_MEMORY diverts to error:; the
success return ER_CDC_LOGINFO_ENTRY_GENERATED is assigned to error
but not acted on, so control falls through break to end:, keeping
the built entry.
6.7 LOG_SUPPLEMENTAL_INFO — the heart of reconstruction
Section titled “6.7 LOG_SUPPLEMENTAL_INFO — the heart of reconstruction”Produces DML and DDL events. Five stages.
Stage 1 — advance, re-sync, ignore-check.
// cdc_log_extract -- src/transaction/log_manager.c (LOG_SUPPLEMENTAL_INFO)// ... advance past supplement, CDC_CHECK_TEMP_LOGPAGE ...if (cdc_Gl.producer.tran_ignore.count (trid) != 0) goto end; /* <- trid already unrecoverable */Stage 2 — read the supplement header, recover the payload.
supplement = (LOG_REC_SUPPLEMENT *) (log_page_p->area + process_lsa->offset);rec_type = supplement->rec_type; /* <- the inner switch discriminant */// ... LOG_READ_ADD_ALIGN past supplement, CDC_CHECK_TEMP_LOGPAGE ...if (cdc_get_undo_record (thread_p, log_page_p, cur_log_rec_lsa, &supp_recdes) != S_SUCCESS) { error = ER_FAILED; goto error; }supplement_length = sizeof (supp_recdes.type) + supp_recdes.length; /* <- recomputed, not trusted */// ... malloc supplement_data (NULL -> ER_OUT_OF_VIRTUAL_MEMORY, goto error); memcpy type+data;// free_and_init (supp_recdes.data) ...CDC_UPDATE_TEMP_LOGPAGE (thread_p, process_lsa, log_page_p); /* <- force slot back to disk */cdc_get_undo_record (Chapter 7) recovers the raw record into
supp_recdes; the header length is overwritten because the recovered
RECDES is authoritative.
Stage 3 — resolve the transaction user (skipped for TRAN_USER).
if (rec_type != LOG_SUPPLEMENT_TRAN_USER) { if (cdc_Gl.producer.tran_user.count (trid) == 0) { if ((error = cdc_find_user (thread_p, cur_log_rec_lsa, trid, &tran_user)) == NO_ERROR) cdc_Gl.producer.tran_user.insert (std::make_pair (trid, tran_user)); /* <- cache it */ else if (error == ER_CDC_IGNORE_TRANSACTION) { cdc_Gl.producer.tran_ignore.insert (std::make_pair (trid, 1)); goto end; } /* <- poison */ else goto end; } /* <- cdc_find_user failed otherwise: skip record */ else tran_user = cdc_Gl.producer.tran_user.at (trid); /* <- already cached */ if (!cdc_is_filtered_user (tran_user)) goto end; } /* <- user not in extraction set */TRAN_USER establishes the user, so it cannot require a pre-existing
one. For every other type a cache miss calls cdc_find_user (6.11); its
ER_CDC_IGNORE_TRANSACTION (the scan hit a LOG_ABORT) inserts a
tran_ignore poison that drops every later record of the trid at Stage
1 until commit/abort erases the marker (6.5 gate 1). The user filter
then drops unrequested users.
Stage 4 — the inner switch over rec_type (Figure 6-3). Each arm
maps to a CDC_DML_TYPE (CDC_INSERT=0, CDC_UPDATE, CDC_DELETE, CDC_TRIGGER_INSERT, CDC_TRIGGER_UPDATE, CDC_TRIGGER_DELETE).
flowchart TD
R{"switch rec_type"}
R -->|TRAN_USER| TU["cache user from payload\nor break if already cached"]
R -->|INSERT / TRIGGER_INSERT| INS["class gate -> redo recdes\n-> CDC_INSERT / CDC_TRIGGER_INSERT"]
R -->|UPDATE / TRIGGER_UPDATE| UPD["class gate -> undo+redo\nREC_ASSIGN_ADDRESS -> re-type TRIGGER_INSERT"]
R -->|DELETE / TRIGGER_DELETE| DEL["class gate -> undo recdes\n-> CDC_DELETE / CDC_TRIGGER_DELETE"]
R -->|DDL| DDL["cdc_make_ddl_loginfo"]
R -->|default| D["break"]
Figure 6-3. The inner supplement dispatch.
Every DML arm opens with the class gate (Stage 5) and goes to
error: on any value but ER_CDC_LOGINFO_ENTRY_GENERATED:
- TRAN_USER — no class gate. If already cached,
break(logged at both begin and end). Else mallocsupplement_length + 1, copy, NUL-terminate, insert intotran_user. The only arm writing the user map from the payload directly. - INSERT / TRIGGER_INSERT — gate, read
redo_lsaat offsetsizeof(OID), fetch redo RECDES, map toCDC_INSERT/CDC_TRIGGER_INSERT. - UPDATE / TRIGGER_UPDATE — gate, fetch undo+redo. Special case:
undo_recdes.type == REC_ASSIGN_ADDRESSis an empty OID-reservation slot, re-typedCDC_TRIGGER_INSERT(with anassertthatrec_typewasTRIGGER_UPDATE); elseCDC_UPDATE/CDC_TRIGGER_UPDATE. ToleratesER_CDC_IGNORE_LOG_INFO/_INTERNALbygoto end. - DELETE / TRIGGER_DELETE — gate, fetch only the undo RECDES (the
deleted pre-image), map to
CDC_DELETE/CDC_TRIGGER_DELETE. - DDL —
cdc_make_ddl_loginfo;ER_CDC_IGNORE_LOG_INFO->end, other non-sentinel ->error:. - default —
break.
Stage 5 — the class gate (quoted once; all three DML arms are identical):
// cdc_log_extract -- src/transaction/log_manager.cmemcpy (&classoid, supplement_data, sizeof (OID));if (!cdc_is_filtered_class (classoid) || oid_is_system_class (&classoid)) { error = ER_CDC_IGNORE_LOG_INFO; goto end; }Two reasons to drop: class not in the extraction set
(!cdc_is_filtered_class), or a system-catalog class
(oid_is_system_class — _db_class, _db_attribute, …). Both
goto end, freeing supplement_data.
6.8 The shared end: and error: exits
Section titled “6.8 The shared end: and error: exits”Both free the same three NULL-guarded heap owners — supplement_data,
undo_recdes.data, redo_recdes.data — differing only in the cursor:
end: ...free... LSA_COPY (process_lsa, &next_log_rec_lsa); return error; /* <- advance */error: ...free... LSA_COPY (process_lsa, &cur_log_rec_lsa); return error; /* <- rewind */Invariant — end: commits forward progress, error: rewinds. A
skip-but-not-fail (goto end) leaves process_lsa at
next_log_rec_lsa; a genuine failure (goto error) restores
cur_log_rec_lsa so the loop re-attempts the same record. This makes
ER_CDC_NULL_EXTRACTION_LSA (6.3) a safe tail-retry; mis-routing a
transient failure to end: would skip a record forever.
6.9 cdc_make_dcl_loginfo — packing a commit/abort entry
Section titled “6.9 cdc_make_dcl_loginfo — packing a commit/abort entry”// cdc_make_dcl_loginfo -- src/transaction/log_manager.cswitch (log_type) { case LOG_COMMIT: dcl_type = CDC_COMMIT; break; case LOG_ABORT: dcl_type = CDC_ABORT; break; default: assert (false); return ER_FAILED; } /* <- only commit/abort here */length = OR_INT_SIZE + OR_INT_SIZE + or_packed_string_length (user, NULL) + OR_INT_SIZE + OR_INT_SIZE + OR_BIGINT_SIZE;loginfo_buf = (char *) malloc (length * 2 + MAX_ALIGNMENT);if (loginfo_buf == NULL) { error_code = ER_OUT_OF_VIRTUAL_MEMORY; goto error; }ptr = start_ptr = PTR_ALIGN (loginfo_buf, MAX_ALIGNMENT);ptr = or_pack_int (ptr, dcl_entry->length); /* placeholder length, backpatched below */// ... or_pack_int trid; or_pack_string user; or_pack_int dataitem_type(CDC_DCL);// or_pack_int dcl_type; or_pack_int64 at_time ...dcl_entry->length = ptr - start_ptr;or_pack_int (start_ptr, dcl_entry->length); /* <- backpatch the leading length */dcl_entry->log_info = (char *) malloc (dcl_entry->length); /* OOM -> error */memcpy (dcl_entry->log_info, start_ptr, dcl_entry->length);return ER_CDC_LOGINFO_ENTRY_GENERATED; /* <- success-as-error sentinel */Packs into scratch loginfo_buf (freed before return), then copies the
exact-length result into dcl_entry->log_info. The leading length is
written twice (placeholder, then backpatched). Either malloc failure
returns ER_OUT_OF_VIRTUAL_MEMORY via error:.
6.10 cdc_make_timer_loginfo — packing a heartbeat entry
Section titled “6.10 cdc_make_timer_loginfo — packing a heartbeat entry”Structurally identical to 6.9, two deltas: dataitem_type = CDC_TIMER,
and no dcl_type field, so the pack sequence and length sizing each
drop one or_pack_int / OR_INT_SIZE. user is always NULL, which
or_packed_string_length / or_pack_string treat as an empty string.
Same backpatched length, same ER_CDC_LOGINFO_ENTRY_GENERATED return,
same OOM error:.
6.11 cdc_find_user — forward scan for a transaction’s user
Section titled “6.11 cdc_find_user — forward scan for a transaction’s user”When the begin-of-transaction TRAN_USER record was truncated away, the
user is recovered from the TRAN_USER logged just before commit.
cdc_find_user walks forward from process_lsa, bounded by nxio_lsa
(a committed transaction’s TRAN_USER is on disk before the next LSA to
flush):
// cdc_find_user -- src/transaction/log_manager.cLOG_LSA nxio_lsa = log_Gl.append.get_nxio_lsa ();if (logpb_fetch_page (...) != NO_ERROR) { logpb_fatal_error (...); return ER_FAILED; }while (!LSA_ISNULL (&process_lsa) && LSA_LT (&process_lsa, &nxio_lsa)) { log_rec_hdr = LOG_GET_LOG_RECORD_HEADER (log_page_p, &process_lsa); LSA_COPY (&forw_lsa, &log_rec_hdr->forw_lsa); // ... LOG_READ_ADD_ALIGN past header ... if (log_rec_hdr->type == LOG_SUPPLEMENTAL_INFO && log_rec_hdr->trid == trid) { // ... advance past supplement; cast supplement ... if (supplement->rec_type == LOG_SUPPLEMENT_TRAN_USER) { *user = (char *) malloc (supplement->length + 1); /* OOM -> ER_OUT_OF_VIRTUAL_MEMORY */ // ... NULL check; ADD_ALIGN; memcpy payload; NUL-terminate ... return NO_ERROR; } } /* <- found */ else if (log_rec_hdr->type == LOG_ABORT && log_rec_hdr->trid == trid) { er_set (..., ER_CDC_IGNORE_TRANSACTION, 1, trid); return ER_CDC_IGNORE_TRANSACTION; } assert (!(log_rec_hdr->type == LOG_COMMIT && log_rec_hdr->trid == trid)); /* user precedes commit */ // ... re-fetch page if forw_lsa crosses (fatal-error + ER_FAILED on failure) ... LSA_COPY (&process_lsa, &forw_lsa); }return ER_FAILED; /* <- scan exhausted */Five exits: TRAN_USER found (NO_ERROR); the matching malloc
OOM (ER_OUT_OF_VIRTUAL_MEMORY, shown inline above); LOG_ABORT
for this trid (ER_CDC_IGNORE_TRANSACTION, the signal cdc_log_extract
turns into a tran_ignore poison); fetch failure (ER_FAILED,
either the initial fetch or the in-loop page-cross re-fetch); loop
exhausted (ER_FAILED). The assert encodes the invariant that a
committed transaction always logs its user before its commit record;
if it fired, producer-side supplemental logging (Chapter 2) is broken.
6.12 cdc_is_filtered_class and cdc_is_filtered_user
Section titled “6.12 cdc_is_filtered_class and cdc_is_filtered_user”Both are linear membership tests with an empty-set-means-all convention:
// cdc_is_filtered_class -- src/transaction/log_manager.cif (cdc_Gl.producer.num_extraction_class == 0) { return true; } /* <- no filter = accept all */for (i = 0; i < cdc_Gl.producer.num_extraction_class; i++) if (cdc_Gl.producer.extraction_classoids[i] == b_classoid) return true;return false;cdc_is_filtered_class compares the OID bit-reinterpreted as a
uint64_t against extraction_classoids[]; cdc_is_filtered_user has
the same shape over extraction_user[] via strcmp. The name reads
backwards: true means pass the filter; an empty list means “extract
everything”.
6.13 The ER_CDC_LOGINFO_ENTRY_GENERATED success-as-error convention
Section titled “6.13 The ER_CDC_LOGINFO_ENTRY_GENERATED success-as-error convention”The “success” return of the cdc_make_* builders is
ER_CDC_LOGINFO_ENTRY_GENERATED (-1294), a negative value in the
error-code space. One int thus distinguishes three outcomes:
NO_ERROR (nothing produced, keep scanning), a real negative error
(abort), and the sentinel (an entry was built). ER_CDC_IGNORE_LOG_INFO
(-1295) and ER_CDC_IGNORE_LOG_INFO_INTERNAL (-1288) mean
“intentionally produced nothing” and route to end:. Returning
NO_ERROR on success would make cdc_log_extract rewind a successfully
built entry, so any builder edit must preserve the contract.
6.14 Chapter summary — key takeaways
Section titled “6.14 Chapter summary — key takeaways”- One record, one switch.
cdc_log_extractdispatches onLOG_RECORD_HEADER.type; onlyLOG_COMMIT/LOG_ABORT(DCL),LOG_DUMMY_HA_SERVER_STATE(Timer), andLOG_SUPPLEMENTAL_INFO(DML/DDL) produce events. temp_logbuf[2]is parity-indexed so a boundary-straddling record never overwrites the page it still needs;tmpbuf_indextracksprocess_lsa->pageid % 2.- User resolution is lazy and self-healing — read
tran_user, fall back tocdc_find_user; an aborted transaction becomes atran_ignorepoison until commit/abort erases it. - Two filter gates —
cdc_is_filtered_userand the class gate (cdc_is_filtered_classplusoid_is_system_class); an empty extraction list means “accept all”. end:advances,error:rewinds — genuine failures restorecur_log_rec_lsaso the same record is retried.- Builders signal success as a negative error code,
ER_CDC_LOGINFO_ENTRY_GENERATED;ER_CDC_IGNORE_LOG_INFO*means “intentionally produced nothing” and routes toend:. - Payload materialization is Chapter 7 — this arm stops at the
cdc_get_recdes/cdc_make_dml_loginfocalls.
Chapter 7: Materializing Row Images and Packing the Event Payload
Section titled “Chapter 7: Materializing Row Images and Packing the Event Payload”Chapter 6 ended with a dispatched DML supplemental record holding only
LSAs: undo_lsa (before) and redo_lsa (after). Given only those
LSAs, how does the producer fetch the real row images and serialize them
into the byte blob the client decodes? Two halves: the indirection
chase (cdc_get_recdes + friends) from LSA to RECDES, and packing
(cdc_make_dml_loginfo + friends) from two RECDES to
CDC_LOGINFO_ENTRY.log_info. The wire format is decoded client-side in
Chapter 8; the DML/DDL/error taxonomy lives in cubrid-cdc.md.
7.1 The three structs this chapter touches
Section titled “7.1 The three structs this chapter touches”flowchart LR RL["redo_lsa / undo_lsa\n(LOG_LSA)"] --> RD["RECDES\n(row image)"] OV["OVF_PAGE_LIST\n(per-page fragment, reverse-linked)"] --> RD ZIP["LOG_ZIP\n(per-call decompress scratch)"] --> RD RD --> ENT["CDC_LOGINFO_ENTRY\n.log_info byte blob"]
Figure 7-1 — Data flow: an LSA resolves to a RECDES (multi-page rows
via an OVF_PAGE_LIST chain, zipped data via LOG_ZIP); the RECDES
pair packs into log_info.
RECDES — the materialized record descriptor (storage_common.h):
// struct recdes -- src/storage/storage_common.hstruct recdes{ int area_size; /* allocated capacity of data; negative == "peeked" */ int length; /* used length, excluding length/type fields */ INT16 type; /* REC_HOME, REC_NEWHOME, ... */ char *data; /* the record payload */};| Field | Role / why |
|---|---|
area_size | Capacity of data; grown on S_DOESNT_FIT, always positive on CDC’s malloc path. |
length | Used byte count; returns negative on S_DOESNT_FIT to size the retry realloc. |
type | Record type tag, set from the first INT16; needed to decode the OR record. |
data | Row bytes — the consumed payload; CDC owns and frees it. |
OVF_PAGE_LIST — one fragment of an overflow (multi-page) row
(log_impl.h):
// struct ovf_page_list -- src/transaction/log_impl.htypedef struct ovf_page_list{ char *rec_type; /* unused on the CDC path (memset to 0) */ char *data; /* fragment incl. OVERFLOW_*_PART header */ int length; /* fragment length */ struct ovf_page_list *next;/* next fragment toward the HEAD (reverse) */} OVF_PAGE_LIST;| Field | Role / why |
|---|---|
rec_type | Heap record-type slot; zeroed, never set on the CDC path. |
data | Fragment incl. its OVERFLOW_*_PART header, stripped by offset at assembly. |
length | Fragment byte length; sizes recdes->data and the per-fragment copy. |
next | Link toward the head; prepend order yields forward bytes despite the backward walk. |
LOG_ZIP — decompression scratch (log_compress.h):
// struct log_zip -- src/transaction/log_compress.hstruct log_zip{ LOG_ZIP_SIZE_T data_length = 0; /* decompressed length after log_unzip */ LOG_ZIP_SIZE_T buf_size = 0; /* capacity of log_data */ char *log_data = nullptr; /* the (de)compressed bytes */};| Field | Role / why |
|---|---|
data_length | Decompressed length from log_unzip — the real image size. |
buf_size | Capacity of log_data, sized once to IO_MAX_PAGE_SIZE; oversize is an LZ4 failure. |
log_data | The decompressed bytes — thread-cached in cdc_get_recdes, per-call in cdc_get_ovfdata_from_log. |
Invariant: a LOG_ZIP buffer is never freed while a RECDES.data
still aliases it. Both fetch paths memcpy out of
redo_zip_ptr->log_data into malloc’d output before log_zip_free;
violating this hands the client a dangling pointer once the next record
reuses the cache.
7.2 cdc_check_log_page and cdc_log_read_advance_and_preserve_if_needed
Section titled “7.2 cdc_check_log_page and cdc_log_read_advance_and_preserve_if_needed”cdc_check_log_page guarantees the buffer holds the LSA’s page: if
log_page_p->hdr.logical_pageid != lsa->pageid it calls
logpb_fetch_page (..., LOG_CS_SAFE_READER, ...) and returns ER_FAILED
on a fetch miss, else NO_ERROR — a no-op when the page already matches.
cdc_log_read_advance_and_preserve_if_needed handles a header straddling
a page boundary. It preserves page N before advancing to N+1
because the caller still needs the header bytes on N:
// cdc_log_read_advance_and_preserve_if_needed -- src/transaction/log_manager.cif (lsa->offset + size >= (int) LOGAREA_SIZE) /* <- header would overflow the page */ { memcpy (preserved, log_page_p, IO_MAX_PAGE_SIZE); /* <- keep page N */ lsa->pageid++; if (logpb_fetch_page (thread_p, lsa, LOG_CS_FORCE_USE, log_page_p) != NO_ERROR) { logpb_fatal_error (...); return ER_FAILED; } /* <- fetch failure is FATAL */ lsa->offset = 0; }This is why cdc_get_recdes re-points log_page_p to
preserved_log_page_p after each call when the pageid changed — the
record header is now in the preserved buffer.
7.3 cdc_get_undo_record — the before image with grow-on-fit
Section titled “7.3 cdc_get_undo_record — the before image with grow-on-fit”// cdc_get_undo_record -- src/transaction/log_manager.cundo_recdes->data = (char *) malloc (ONE_K); /* <- optimistic 1 KB */scan_code = log_get_undo_record (thread_p, log_page_p, lsa, undo_recdes);if (scan_code == S_DOESNT_FIT) /* <- 1 KB too small */ { undo_recdes->data = (char *) realloc (undo_recdes->data, (size_t) (-undo_recdes->length)); undo_recdes->area_size = (size_t) (-undo_recdes->length); /* length came back negative */ if (cdc_check_log_page (thread_p, log_page_p, &lsa) != NO_ERROR) return S_ERROR; scan_code = log_get_undo_record (...); /* <- retry exactly once */ }return scan_code; /* S_ERROR / S_END from a non-DOESNT_FIT failure propagate unchanged */Branches: (1) first call → S_SUCCESS; (2) S_DOESNT_FIT → realloc to
exact -length, re-validate, retry once, second failure propagates; (3)
other non-success → propagate. No third retry — log_get_undo_record
reports the exact size, so one realloc suffices. Its LOG_ZIP decompress
happens inside log_get_undo_record (shared with vacuum), so it is
implicit here.
7.4 cdc_get_recdes — the indirection chase, branch by branch
Section titled “7.4 cdc_get_recdes — the indirection chase, branch by branch”cdc_get_recdes runs an undo block (if undo_lsa != NULL) then a
redo block (if redo_lsa != NULL), each a switch on log record
type. is_flashback (false on the normal CDC path) only changes the
page source: false may reuse cdc_Gl.producer.temp_logbuf, true always
re-fetches. On is_flashback == false, a missing page or broken chain
is a hard error — no silent skip.
flowchart TB
A["enter"] --> B{undo_lsa?}
B -->|yes| C["page: temp_logbuf if !is_flashback,\nelse logpb_fetch_page -> fail goto error"]
C --> D["switch undo type: SUPPL/MVCC_UNDOREDO -> cdc_get_undo_record;\nUNDOREDO DEL/UPD -> undo_record or OVF; UNDO/REDO_DATA OVF -> overflow"]
B -->|no| R{redo_lsa?}
D --> R
R -->|no| Z["free scratch, return NO_ERROR"]
R -->|yes| F["switch redo type: MVCC_UNDOREDO INSERT/UPDATE_NOTIFY\n-> build image, maybe log_diff; UNDOREDO INSERT or OVF -> image/overflow"]
F --> Z
C -.fetch fail.-> ERR["error: free redo/undo/recdes, return error_code"]
Figure 7-2 — cdc_get_recdes. Every goto error converges on one
error: label freeing redo_data, undo_data, tmp_undo_recdes.data,
and both output buffers. Branches are in the diagram, not restated.
The redo case worth quoting is LOG_MVCC_*UNDOREDO_DATA with
RVHF_MVCC_INSERT: the record carries no MVCC header, so CDC rebuilds
the RECDES by hand (a RVHF_UPDATE_NOTIFY_VACUUM DIFF redo instead
patches the undo image with log_diff):
// cdc_get_recdes (redo, RVHF_MVCC_INSERT) -- src/transaction/log_manager.credo_recdes->type = *(INT16 *) redo_data;redo_recdes->length = redo_length - sizeof (INT16);tmp_ptr = (char *) redo_data + sizeof (redo_recdes->type);redo_recdes->length += OR_HEADER_SIZE (tmp_ptr); /* <- re-add OR header room */redo_recdes->data = (char *) malloc (redo_recdes->length + MAX_ALIGNMENT);// ... memcpy rep-id, CHN, then body ...Both redo branches share the zip shape (ZIP_CHECK →
log_append_get_zip_redo thread-cached LOG_ZIP → log_unzip); any
allocation or unzip failure jumps to error:.
7.5 cdc_get_ovfdata_from_log / cdc_get_overflow_recdes — rows that span pages
Section titled “7.5 cdc_get_ovfdata_from_log / cdc_get_overflow_recdes — rows that span pages”A row larger than a heap page is an overflow chain linked by
prev_tranlsa. cdc_get_overflow_recdes walks it backward, prepends
each fragment (yielding forward order), then concatenates minus the
per-page headers. The head pivot decides where to start: when the
current record is an RVOVF_PAGE_UPDATE matching the direction (LOG_UNDO_DATA
is_redo, orLOG_REDO_DATA+!is_redo), or anRVOVF_CHANGE_LINK/RVOVF_NEWPAGE_LINK, the walk seedscurrent_lsafromprev_tranlsa(fetching the page on a pageid change, error on fetch fail) and rewrites the link rcvindexes toRVOVF_PAGE_UPDATEsince the data lives in PAGE_UPDATE records.
The walk loop branches three ways per record: (1) Boundary — trid
changes or LOG_DUMMY_OVF_RECORD seen → stop, but on the undo path a
LOG_DUMMY_OVF_RECORD first triggers one extra
cdc_get_ovfdata_from_log against prev_lsa (the last fragment lives
one record back); (2) Match — a redo record while is_redo (or undo
while !is_redo) → fetch and prepend it; (3) otherwise fall through to
page-advance. After the walk, recdes->data is sized to the summed
length and drained head-first; each fragment is copied from
area_offset = offsetof (OVERFLOW_FIRST_PART, data) for the first node
and offsetof (OVERFLOW_REST_PART, data) for the rest, stripping the
per-page header.
Invariant: every OVF_PAGE_LIST node and its data are freed exactly
once. The success path frees each node as it drains; the end: label
drains the remainder on error; the two goto end sites free the
current node before jumping, so it is never double-freed. If this
slipped, a mid-chain malloc failure would leak.
cdc_get_ovfdata_from_log itself: skip the header, read LOG_REC_REDO/
LOG_REC_UNDO per is_redo, and if the rcvindex does not match,
goto end with no data (a non-error skip). Otherwise copy length
bytes, ZIP_CHECK/log_unzip through a per-call log_zip_allocd
LOG_ZIP (freed at end:, distinct from cdc_get_recdes’s thread-cached
zip), then memcpy into a fresh *outdata.
7.6 cdc_make_dml_loginfo — from two RECDES to one byte blob
Section titled “7.6 cdc_make_dml_loginfo — from two RECDES to one byte blob”With undo_recdes (before) and redo_recdes (after), packing produces
the CDC_LOGINFO_ENTRY:
// struct cdc_loginfo_entry -- src/transaction/log_impl.htypedef struct cdc_loginfo_entry{ LOG_LSA next_lsa; /* set by the dispatch caller, not here */ int length; /* byte length of log_info */ char *log_info; /* the packed wire payload */} CDC_LOGINFO_ENTRY;flowchart TB
A["enter"] --> P{root_class_oid and heap_attrinfo_start ok?}
P -->|no| PE["flashback? schema-changed err\nelse cdc_make_error_loginfo; goto exit"]
P -->|yes| U["undo+redo: schema check, read dbvalues\n-> old_values / new_values by def_order"]
U --> PK{all_in_cond==0 and not INSERT and not flashback?}
PK -->|yes| FPK["cdc_find_primary_key -> has_pk; has_pk<0 -> goto exit"]
PK -->|no| SW
FPK --> SW["switch dml_type: INSERT all changed;\nUPDATE changed=diff cond=PK/all; DELETE no changed cond=PK/all"]
SW --> FIN["pack length, malloc log_info, memcpy;\nexit: free, return ER_CDC_LOGINFO_ENTRY_GENERATED"]
Figure 7-3 — cdc_make_dml_loginfo. Every early failure routes to one
exit: label freeing loginfo_buf, the index/value arrays, and the
attrinfo.
Setup. A partition class OID is rewritten to its root via
partition_find_root_class_oid; on failure (dropped class) the function
emits an error loginfo (§7.9) and exits — except on flashback, where
FLASHBACK_ERROR_HANDLING raises ER_FLASHBACK_SCHEMA_CHANGED first.
heap_attrinfo_start loads the latest class representation.
Reading values by def_order. For each non-NULL RECDES, CDC calls
cdc_check_if_schema_changed (§7.10) then heap_attrinfo_read_dbvalues,
and copies each value into old_values[heap_value->read_attrepr->def_order]
(definition order, not storage order), accumulating
cdc_get_attribute_size (§7.8) into record_length; new_values is
filled the same way from redo_recdes. Primary-key lookup runs only
when all_in_cond == 0, not INSERT, and not flashback; has_pk < 0
(function-index PK) is a hard goto exit.
7.7 The changed-column vs cond-column split
Section titled “7.7 The changed-column vs cond-column split”The heart of the DML payload, mapping onto the client DML struct
(Chapter 8). Each branch packs, in order: dml_type, classoid,
num_change_col + change indices + values, then num_cond_col + cond
indices + values.
- INSERT / TRIGGER_INSERT: all columns changed
(
num_change_col = attr_info.num_values, values fromnew_values); no cond columns. - UPDATE / TRIGGER_UPDATE: changed set = per-column diff (below);
cond columns = PK (
has_pk == 1, frompk_attr_index) or, failing a PK, allold_values. Flashback forces the changed set to all columns. - DELETE / TRIGGER_DELETE:
num_change_col = 0; cond columns = PK (or all) fromold_values.
The UPDATE changed set is computed by cdc_compare_undoredo_dbvalue:
// cdc_make_dml_loginfo (UPDATE changed set) -- src/transaction/log_manager.cfor (i = 0; i < attr_info.num_values; i++) if (cdc_compare_undoredo_dbvalue (&new_values[i], &old_values[i]) > 0) /* <- only differing cols */ changed_col_idx[cnt++] = i;if (cnt == 0) { error_code = ER_CDC_IGNORE_LOG_INFO_INTERNAL; goto exit; } /* <- trigger-savepoint phantom */cdc_compare_undoredo_dbvalue, branch by branch. It asserts both
operands are non-NULL pointers, then has exactly two material branches:
// cdc_compare_undoredo_dbvalue -- src/transaction/log_manager.cassert (new_value != NULL && old_value != NULL);if (DB_IS_NULL (new_value) && DB_IS_NULL (old_value)) return 0; /* <- both SQL NULL == SAME, not a change */else return db_value_compare (new_value, old_value) == 0 ? 0 : 1; /* <- 1 == different */The both-NULL branch is load-bearing: db_value_compare does not
treat two NULLs as equal, so without the explicit return 0 a column
NULL both before and after would be wrongly flagged as changed. The
branch makes the diff skip it, so > 0 selects only genuinely-differing
columns. An empty diff (cnt == 0) is a trigger-savepoint phantom —
ignored via ER_CDC_IGNORE_LOG_INFO_INTERNAL, not an error.
Invariant: a cond column always uses old_values, a changed column
always uses new_values. Cond columns are the client’s WHERE key,
changed columns the SET payload; crossing them would make the client
apply the change against the post-image and fail to match the row.
Back-patch and copy-out. dml_entry->length = ptr - start_ptr then
or_pack_int (start_ptr, dml_entry->length) rewrites the dummy length
prefix in place; the blob is memcpy’d into a freshly malloc’d
dml_entry->log_info. The function returns ER_CDC_LOGINFO_ENTRY_GENERATED
(a success sentinel); exit: frees scratch, both index and value
arrays, and ends the attrinfo.
7.8 cdc_get_attribute_size and cdc_put_value_to_loginfo
Section titled “7.8 cdc_get_attribute_size and cdc_put_value_to_loginfo”cdc_get_attribute_size returns the serialized byte budget for a
DB_VALUE (it never emits bytes), switching on DB_VALUE_TYPE: fixed
types → C width; NUMERIC → DB_NUMERIC_BUF_SIZE; BIT/VARBIT → X'..'
text length; CHAR/VARCHAR → db_get_string_size; DATE/TIME → the
*_PRECISION constant (serialized as strings); MONETARY → double +
currency symbol; ENUMERATION → string form or a short; BLOB/CLOB → ELO
locator length; unsupported composites (OBJECT, SET, JSON) → 0;
default asserts.
cdc_put_value_to_loginfo emits a func_type tag then the value — the
client’s decode discriminator (Chapter 8):
func_type | Covers | Packed as |
|---|---|---|
| 0 | INTEGER | or_pack_int |
| 1 | BIGINT | or_pack_int64 |
| 2 | FLOAT | or_pack_float |
| 3 | DOUBLE | or_pack_double |
| 4 | SHORT, and ENUMERATION stored as a short (db_get_enum_string == NULL) | or_pack_short |
| 7 | NULL, NUMERIC, BIT/VARBIT, CHAR/VARCHAR, all DATE/TIME, MONETARY, enum-as-string, BLOB/CLOB locator, all unsupported composites | or_pack_int(7) + or_pack_string(text or NULL) |
func_type 5 and 6 are reserved (never emitted); DATE/TIME types differ
only in the db_to_char format string (e.g. DB_TYPE_DATE →
"YYYY-MM-DD").
// cdc_put_value_to_loginfo (NULL guard) -- src/transaction/log_manager.cif (DB_IS_NULL (new_value)) { func_type = 7; ptr = or_pack_int (ptr, func_type); ptr = or_pack_string (ptr, NULL); /* <- NULL packs as func_type 7 + NULL string */ *data_ptr = ptr; return NO_ERROR; /* <- NULL is normal (e.g. ALTER added a column) */ }A NULL value is not an error — it is how a newly-added column
(post-ALTER) appears. default asserts; hard failures (LOB extraction,
ELO_LO) return ER_FAILED, which the caller turns into a goto exit.
7.9 cdc_make_error_loginfo — the degenerate DML payload
Section titled “7.9 cdc_make_error_loginfo — the degenerate DML payload”When the schema cannot be resolved (dropped class, schema change),
cdc_make_dml_loginfo emits an error DML entry: same CDC_DML
framing and real trid/user/dml_type/classoid, but
num_change_col = 0 and num_cond_col = 0. The client sees a
well-formed DML event with no column data and can react (skip, log,
abort) rather than choke on a truncated record. It back-patches the
length, copies into dml_entry->log_info, and returns
ER_CDC_LOGINFO_ENTRY_GENERATED.
7.10 cdc_find_primary_key and cdc_check_if_schema_changed
Section titled “7.10 cdc_find_primary_key and cdc_check_if_schema_changed”cdc_find_primary_key maps a repr_id to the PK column def_orders:
heap_classrepr_get loads the class rep, the scan finds a
BTREE_PRIMARY_KEY, and on a hit fills pk_attr[]:
// cdc_find_primary_key -- src/transaction/log_manager.cif (index->type == BTREE_PRIMARY_KEY) { has_pk = 1; if (index->func_index_info != NULL) return ER_FAILED; /* <- func-index PK unsupported */ num_idx_att = index->n_atts; pk_attr = (int *) malloc (sizeof (int) * num_idx_att); for (int j = 0; j < num_idx_att; j++) { pk_attr[j] = index->atts[j]->def_order; *num_attr += 1; } /* <- PK col def_orders */ *pk_attr_id = pk_attr; break; }Returns 1 (PK found — caller uses the def_order array as cond
columns), 0 (no PK — caller falls back to all columns, §7.7), or
negative on a function-index PK (ER_FAILED) / alloc failure
(ER_OUT_OF_VIRTUAL_MEMORY). The in-source header comment is doubly
stale: “if PK exists, return 0 with PK column id” (real code returns
1) and “if PK does not exist, return -1” (real code returns has_pk,
i.e. 0 — negatives are reserved for the func-index and OOM paths). The
returned def_order indices index directly into the value arrays.
cdc_check_if_schema_changed is a one-liner with outsized importance:
// cdc_check_if_schema_changed -- src/transaction/log_manager.creturn or_rep_id (recdes) != attr_info->last_classrepr->id;It compares the record’s representation id against the latest. CDC
cannot interpret a record written under a since-changed schema — the
old rep lacks the def_order metadata CDC needs and clients pre-fetch
schemas over JDBC — so a mismatch routes to cdc_make_error_loginfo (or
ER_FLASHBACK_SCHEMA_CHANGED on flashback), not a best-effort decode.
7.11 cdc_make_ddl_loginfo — inline statement text
Section titled “7.11 cdc_make_ddl_loginfo — inline statement text”DDL is far simpler: the supplemental record already carries the full statement text, so no row materialization is needed — CDC unpacks the supplement and repacks the DDL wire shape:
// cdc_make_ddl_loginfo -- src/transaction/log_manager.cptr = or_unpack_int (ptr, &ddl_type);ptr = or_unpack_int (ptr, &object_type);ptr = or_unpack_oid (ptr, &classoid);if (!OID_ISNULL (&classoid)) if (oid_is_system_class (&classoid) || !cdc_is_filtered_class (classoid)) { error_code = ER_CDC_IGNORE_LOG_INFO; goto error; } /* <- system/unfiltered class: skip */ptr = or_unpack_oid (ptr, &oid); /* then statement_length + statement */A system class or a class outside the CDC filter yields
ER_CDC_IGNORE_LOG_INFO — a skip, not a failure. Otherwise the output
packs length / trid / user / CDC_DDL / ddl_type / object_type
/ objectOID / classOID / statement_length / statement, back-patches the
length, copies into ddl_entry->log_info, and returns
ER_CDC_LOGINFO_ENTRY_GENERATED; the error: label frees loginfo_buf.
7.12 Chapter summary — key takeaways
Section titled “7.12 Chapter summary — key takeaways”cdc_get_recdesresolves the LSAs intoRECDESimages — an undo block then a redo block, each a type-switch that fetches viacdc_get_undo_record, rebuilds MVCC-insert images by hand, or applieslog_difffor DIFF records.- Page boundaries are explicit:
cdc_log_read_advance_and_preserve_if_neededpreserves the page before crossing;cdc_get_recdesre-pointslog_page_pto it when an advance changed the pageid. - Overflow rows reassemble backward then concatenate forward.
cdc_get_overflow_recdeswalksprev_tranlsa, prepends eachOVF_PAGE_LISTfragment, strips theOVERFLOW_*_PARTheaders, and frees each node once;cdc_get_ovfdata_from_logdoes the per-callLOG_ZIPdecompress. cdc_make_dml_loginfore-orders values intodef_orderand splits columns into a changed set (new_values) and a cond set (old_values) — PK if one exists, else all — the SET/WHERE split.cdc_compare_undoredo_dbvaluetreats two SQL NULLs as the same (return 0), so the diff skips before-and-after-NULL columns; an empty diff is ignored (ER_CDC_IGNORE_LOG_INFO_INTERNAL). Serialization usesfunc_type0–4 plus a catch-all 7 for NULL/string forms (5–6 unused).- Schema drift is fatal to interpretation, not the stream: a
cdc_check_if_schema_changedmismatch (or dropped class) routes tocdc_make_error_loginfo, a column-less but well-formedCDC_DMLentry. - DDL skips materialization —
cdc_make_ddl_loginforepacks the inline statement text, skipping system/unfiltered classes with the non-errorER_CDC_IGNORE_LOG_INFO.
Chapter 8: Client API Lifecycle and Wire Decoding
Section titled “Chapter 8: Client API Lifecycle and Wire Decoding”This chapter switches from the server (Ch. 5–7) to the external
consumer — the cubrid_log_* C ABI in src/api/cubrid_log.c. How does
one session move through its stages, pull a batch over the network, and
decode the byte blob into typed CUBRID_LOG_ITEMs? For pull-style design
framing see cubrid-cdc.md §“Public API surface”. Everything here is
single-threaded and global-state driven: one implicit session in
file-scope g_* variables, no handle — hence the one-session-per-process
rigidity and why the stage machine matters.
8.1 The two enums and the item struct
Section titled “8.1 The two enums and the item struct”// CUBRID_LOG_STAGE -- src/api/cubrid_log.ctypedef enum { CUBRID_LOG_STAGE_CONFIGURATION, CUBRID_LOG_STAGE_PREPARATION, CUBRID_LOG_STAGE_EXTRACTION, CUBRID_LOG_STAGE_FINALIZATION } CUBRID_LOG_STAGE;CUBRID_LOG_STAGE g_stage = CUBRID_LOG_STAGE_CONFIGURATION; /* the whole session state */| Value | Role / why it exists |
|---|---|
CONFIGURATION | Initial + post-finalize; the seven setters legal only here, so filters/timeouts freeze before the server reads them once at START_SESSION. |
PREPARATION | After connect_server; find_lsa + first extract legal. Separates “connected, no cursor” from “extracting”. |
EXTRACTION | After first extract; extract + clear_log_item legal. Marks a live batch buffer to be cleared before the next pull. |
FINALIZATION | Declared but never assigned; finalize goes to CONFIGURATION. Vestigial — CONFIGURATION doubles as terminal state. |
Invariant: every public entry point guards on g_stage before
touching any other global — its first statement is a g_stage != <required> check returning INVALID_FUNC_CALL_STAGE (-27). Without it
a consumer could re-run set_max_log_item mid-stream and corrupt the
negotiated session, or extract before connecting and dereference a null
g_conn_entry. Later sections only note deviations from this rule.
// DATA_ITEM_TYPE -- src/api/cubrid_log.ctypedef enum { DATA_ITEM_TYPE_DDL = 0, DATA_ITEM_TYPE_DML, DATA_ITEM_TYPE_DCL, DATA_ITEM_TYPE_TIMER } DATA_ITEM_TYPE;| Value | Role / why it exists |
|---|---|
DDL (0) | Schema change; selects ddl. Carries SQL text inline so replay needs no catalog lookup. |
DML (1) | Row change; selects dml. The only arm with per-column arrays needing malloc/free. |
DCL (2) | Transaction control; selects dcl. Brackets a transaction’s rows by commit timestamp. |
TIMER (3) | Timestamp heartbeat; selects timer. Advances the cursor across idle gaps. |
The values are wire-significant: the server packs the raw int,
make_log_item casts it to DATA_ITEM_TYPE for dispatch, and
data_item_type_to_string assert(0)s outside 0..3.
// CUBRID_LOG_ITEM -- src/api/cubrid_log.hstruct cubrid_log_item { int transaction_id; char *user; int data_item_type; CUBRID_DATA_ITEM data_item; CUBRID_LOG_ITEM *next; };| Field | Role / why it exists |
|---|---|
transaction_id | Groups items by source trid; commit boundaries arrive as DCL items. |
user | Client user name. or_unpack_string_nocopy pointer into g_log_infos — not owned, invalid once the blob is reused. |
data_item_type | Selects the live union arm. Raw int to match the wire; cast at dispatch. |
data_item | Typed payload (CUBRID_DATA_ITEM union). One struct covers all four kinds; only DML owns heap. |
next | NULL-terminated list link. Iterate without the count (also returned via list_size). |
flowchart LR
subgraph G["file-scope globals"]
BLOB["g_log_infos\n(byte blob, reused)"]
ITEMS["g_log_items[]\n(CUBRID_LOG_ITEM array, reused)"]
end
ITEMS -->|next| ITEMS
ITEMS -->|"user, statement,\ncolumn_data ptrs"| BLOB
ITEMS -->|"data_item union"| U["DDL | DML | DCL | TIMER"]
Figure 8-1 — g_log_items[] is reused across pulls; string/column
pointers alias into g_log_infos. Only the DML arm’s index/data/len
arrays are separately malloced.
8.2 Configuration stage — the seven setters
Section titled “8.2 Configuration stage — the seven setters”All seven share one skeleton: guard g_stage == CONFIGURATION,
range-check, store into a g_* global, return SUCCESS; only the range
check and error code differ.
| Setter | Global | Range / rule | Error |
|---|---|---|---|
set_connection_timeout | g_connection_timeout | 0..360 | INVALID_CONNECTION_TIMEOUT |
set_extraction_timeout | g_extraction_timeout | 0..360 | INVALID_EXTRACTION_TIMEOUT |
set_max_log_item | g_max_log_item | 1..1024 | INVALID_MAX_LOG_ITEM |
set_all_in_cond | g_all_in_cond | 0..1 | INVALID_RETRIEVE_ALL |
set_extraction_table | g_extraction_table[] | array/size both-null or both-set | INVALID_CLASSOID_ARR_SIZE |
set_extraction_user | g_extraction_user[] | array/size both-null or both-set | INVALID_USER_ARR_SIZE |
set_tracelog | g_trace_log_* | path writable, level 0..1, filesize 8..512 MB | INVALID_PATH/LEVEL/FILESIZE |
// cubrid_log_set_extraction_table -- src/api/cubrid_log.c if ((classoid_arr == NULL && arr_size != 0) || (classoid_arr != NULL && arr_size == 0)) return CUBRID_LOG_INVALID_CLASSOID_ARR_SIZE; /* <- XOR null/size mismatch */ g_extraction_table = (uint64_t *) malloc (sizeof (uint64_t) * arr_size); if (g_extraction_table == NULL) return CUBRID_LOG_FAILED_MALLOC; memcpy (g_extraction_table, classoid_arr, arr_size * sizeof (uint64_t)); /* OIDs are POD */set_extraction_user instead strdups each string but does not check
the strdup return — OOM leaves a NULL slot that later crashes in
or_pack_string. set_tracelog mkdirs the path via
check_tracelog_path (→ NO_FILE_PERMISSION) and treats filesize == -1
as the 10 MB default.
Invariant: setters never touch the network. The server learns the
config once, later, in send_configurations; the stage guard blocks
re-running a setter after connect, so there is no client-side mid-stream
filter race (cubrid-cdc.md Open Question 4).
8.3 Preparation stage — connecting
Section titled “8.3 Preparation stage — connecting”cubrid_log_connect_server is the gate CONFIGURATION → PREPARATION,
orchestrating three helpers and advancing g_stage only if all succeed.
Note g_dbname is strncpyed (capped at CUBRID_LOG_MAX_DBNAME_LEN)
before the stage guard, so a stage-misuse call still mutates it.
flowchart TB
A["connect_server"] --> B{"dbname != NULL?"}
B -->|no| ERR["INVALID_DBNAME"]
B -->|yes| C["strncpy into g_dbname"]
C --> D{"stage == CONFIGURATION?"}
D -->|no| ERR2["INVALID_FUNC_CALL_STAGE"]
D -->|yes| E{"host/port/user/password valid?"}
E -->|no| ERR3["INVALID_HOST/PORT/USER/PASSWORD"]
E -->|yes| F["db_login"]
F -->|fail| ERR4["FAILED_LOGIN"]
F -->|ok| G["er_init"] --> H["connect_server_internal"]
H -->|fail| ERR5["FAILED_CONNECT"]
H -->|ok| I["send_configurations"]
I -->|fail| ERR6["FAILED_CONNECT / UNAVAILABLE_CDC_SERVER"]
I -->|ok| J["g_stage = PREPARATION; SUCCESS"]
Figure 8-2 — cubrid_log_connect_server branch map.
cubrid_log_db_login does a full db_restart purely to validate
credentials, with a DBA gate and db_shutdown on every exit:
// cubrid_log_db_login -- src/api/cubrid_log.c if (au_login (username, password, true) != NO_ERROR) goto error; if (db_restart ("cubrid_log_api", 0, dbname_at_hostname) != NO_ERROR) return CUBRID_LOG_FAILED_LOGIN; /* <- early return, NOT goto error */ if ((user = au_find_user (username)) == NULL) goto error; if (!au_is_dba_group_member (user)) goto error; /* <- CDC requires DBA */ db_shutdown (); return CUBRID_LOG_SUCCESS;error: db_shutdown (); return CUBRID_LOG_FAILED_LOGIN; /* every failure -> -33 */The db_restart failure returns directly (no restart to tear down); the
three post-restart failures goto error to db_shutdown first.
cubrid_log_connect_server_internal makes the CSS connection and reads a
one-int “reason”:
css_make_conn→css_connect_to_log_server→css_receive_data(timeoutg_connection_timeouts); any failure →FAILED_CONNECT. Reply must be exactlysizeof(int);reason = ntohl(*recv_data).- Non-WINDOWS: require
reason == SERVER_CONNECTED, elseFAILED_CONNECT. - WINDOWS: require
reason == SERVER_CONNECTED_NEW, then a second handshake — receive a fresh port id,css_close_conn,css_server_connect_part_two(master-to-server port handoff).
The cubrid_log_error label frees recv_data, runs
css_queue_find_and_remove_header_entry_ptr (g_conn_entry, rid),
css_free_conns, and (Windows) css_windows_shutdowns — the
find-and-remove-then-free discipline that recurs in every networked helper.
cubrid_log_send_configurations opens the session with one pre-sized
buffer and forks on the reply code:
// cubrid_log_send_configurations -- src/api/cubrid_log.c request_size = OR_INT_SIZE * 5; /* 5 fixed ints + 2 variable arrays */ for (...) request_size += or_packed_string_length (g_extraction_user[i], NULL); request_size += (OR_BIGINT_SIZE * g_extraction_table_count); ...or_pack_int (max_log_item, extraction_timeout, all_in_cond, user_count)... for(...) or_pack_string (g_extraction_user[i]); ...or_pack_int (table_count); for(...) or_pack_int64 (g_extraction_table[i]); ...NET_SERVER_CDC_START_SESSION... or_unpack_int (recv_data, &reply_code); if (reply_code == ER_CDC_NOT_AVAILABLE) CUBRID_LOG_ERROR_HANDLING (CUBRID_LOG_UNAVAILABLE_CDC_SERVER, "...supplemental_log..."); else if (reply_code != NO_ERROR) CUBRID_LOG_ERROR_HANDLING (CUBRID_LOG_FAILED_CONNECT, ...);ER_CDC_NOT_AVAILABLE → UNAVAILABLE_CDC_SERVER (-34) with the
supplemental_log hint (server is not CDC-configured); any other
non-NO_ERROR → FAILED_CONNECT.
8.4 Preparation stage — seeding the start LSA
Section titled “8.4 Preparation stage — seeding the start LSA”cubrid_log_find_lsa (full seeding semantics in Ch. 4) guards
stage/timestamp/out-param, then queries the server only if no cursor
exists yet:
// cubrid_log_find_lsa -- src/api/cubrid_log.c ...guards: g_stage != PREPARATION; timestamp NULL or < 0; lsa NULL... if (LSA_ISNULL (&g_next_lsa)) /* <- query server only if no cursor yet */ if (cubrid_log_find_start_lsa (timestamp, &g_next_lsa) != SUCCESS) { ...LSA_NOT_FOUND... } memcpy (lsa, &g_next_lsa, sizeof (uint64_t)); /* <- return the cursor */Invariant: find_lsa is idempotent once a cursor exists — later calls
echo g_next_lsa, so a consumer cannot re-seek backwards after extracting.
cubrid_log_find_start_lsa packs the timestamp as int64, sends
NET_SERVER_CDC_FIND_LSA, unpacks reply_code | lsa | timestamp, and has
a latent bug: the first if matches NO_ERROR || ER_CDC_ADJUSTED_LSA
and returns plain SUCCESS, so the later else if (... ER_CDC_ADJUSTED_LSA)
returning SUCCESS_WITH_ADJUSTED_LSA (2) is dead. Any other code →
LSA_NOT_FOUND.
8.5 Extraction stage — the two-request pull
Section titled “8.5 Extraction stage — the two-request pull”cubrid_log_extract is the loop body, legal in both PREPARATION
and EXTRACTION (first call in PREPARATION) and always ending in
EXTRACTION. The consumer’s *lsa overrides g_next_lsa on entry, so
the application owns the cursor (persists next_lsa, replays on reconnect
— cubrid-cdc.md §“A modern CDC pull, end to end”); on return *lsa
carries the next cursor.
// cubrid_log_extract -- src/api/cubrid_log.c ...guards: stage not PREPARATION/EXTRACTION; lsa/log_item_list/list_size NULL... memcpy (&g_next_lsa, lsa, sizeof (LOG_LSA)); /* <- caller's cursor wins */ rc = cubrid_log_extract_internal (&g_next_lsa, &num_infos, &total_length); ... rc = cubrid_log_make_log_item_list (num_infos, total_length, log_item_list, list_size); ... memcpy (lsa, &g_next_lsa, sizeof (uint64_t)); /* <- hand advanced cursor back */ g_stage = CUBRID_LOG_STAGE_EXTRACTION;flowchart TB
A["pack g_next_lsa"] --> B["send GET_LOGINFO_METADATA"]
B --> C["recv reply_code | next_lsa | num_infos | total_length"]
C --> D{"reply_code?"}
D -->|ER_CDC_EXTRACTION_TIMEOUT| E["return EXTRACTION_TIMEOUT"]
D -->|ER_CDC_INVALID_LOG_LSA| F["return INVALID_LSA"]
D -->|NO_ERROR / other| G["unpack next_lsa, num_infos, total_length"]
G --> H{"g_log_infos_size < total_length?"}
H -->|yes| I["realloc to total_length + MAX_ALIGNMENT"]
H -->|no| J["reuse blob"]
I --> K{"total_length > 0?"}
J --> K
K -->|yes| L["send GET_LOGINFO\nrecv blob into g_log_infos"]
K -->|no| M["skip 2nd request"]
L --> N["return SUCCESS"]
M --> N
Figure 8-3 — cubrid_log_extract_internal. Two branch findings the
figure cannot show: (a) the reply_code fork has no else arm — any
non-NO_ERROR past the two ER_CDC_* cases falls through and is unpacked
as a successful reply (fragile); (b) the rc == SUCCESS_WITH_NO_LOGITEM
check guards an unreachable goto because rc is still NO_ERROR there,
so that path is dead — an empty batch is signalled by num_infos == 0.
Invariant: the metadata round-trip sizes the buffer before the data
round-trip fills it — the client grows g_log_infos once (grow-only
realloc, failure → FAILED_MALLOC) and receives the bulk blob unframed,
so the second request fires only at total_length > 0 and the received
size must equal total_length else FAILED_CONNECT; an undersized buffer
would overrun the heap.
8.6 The decode chain — blob to typed items
Section titled “8.6 The decode chain — blob to typed items”// cubrid_log_make_log_item_list -- src/api/cubrid_log.c if (g_log_items_count < num_infos) { ...realloc g_log_items, never shrink... } ptr = PTR_ALIGN (g_log_infos, MAX_ALIGNMENT); for (i = 0; i < num_infos; i++) { cubrid_log_make_log_item (&ptr, &g_log_items[i]); g_log_items[i].next = &g_log_items[i + 1]; /* <- link forward */ ptr = PTR_ALIGN (ptr, MAX_ALIGNMENT); } /* <- realign between records */ g_log_items[num_infos - 1].next = NULL; /* <- terminate (underflows if 0) */Invariant: each record is re-aligned to MAX_ALIGNMENT before the next
is decoded — the server packs on a boundary, so omitting PTR_ALIGN
makes the next or_unpack_* read garbage.
// cubrid_log_make_log_item -- src/api/cubrid_log.c (per-item header, then dispatch) ptr = or_unpack_int (ptr, &log_info_len); /* <- length prefix, skipped */ ptr = or_unpack_int (ptr, &log_item->transaction_id); ptr = or_unpack_string_nocopy (ptr, &log_item->user); /* <- aliases into blob */ ptr = or_unpack_int (ptr, &log_item->data_item_type); rc = cubrid_log_make_data_item (&ptr, (DATA_ITEM_TYPE) log_item->data_item_type, &log_item->data_item);cubrid_log_make_data_item is a four-way switch (default: assert(0)):
make_ddl unpacks ddl_type/object_type/oid/classoid/statement_length
then statement via or_unpack_string_nocopy (SQL aliases the blob);
make_dcl unpacks dcl_type + int64 timestamp; make_timer unpacks
one int64 timestamp; make_dml is the only allocator. make_dml
handles two symmetric blocks — changed columns (new image) and
cond columns (old image). Each: (1) unpack the column count; (2) if
non-zero, malloc three parallel arrays (changed_column_index[] int,
changed_column_data[] char*, changed_column_data_len[] int), each
failure → FAILED_MALLOC; (3) unpack indices; (4) per column, unpack a
pack_func_code then switch to decode in place:
// cubrid_log_make_dml -- src/api/cubrid_log.c (changed-column value switch) ptr = or_unpack_int (ptr, &pack_func_code); switch (pack_func_code) { case 0: data[i] = ptr; or_unpack_int (...); len[i] = OR_INT_SIZE; break; // 1-4,8 analogous: int64/float/double/short/string; case 6 is assert(0), unused case 5: data[i] = ptr; or_unpack_string_nocopy (...); len[i] = strlen (data[i]); break; case 7: data[i] = ptr; or_unpack_string_nocopy (...); /* <- NULL-tolerant */ len[i] = (data[i] == NULL) ? 0 : strlen (data[i]); break; default: assert (0); }pack_func_code is a per-column type tag: 0=int, 1=int64/bigint,
2=float, 3=double, 4=short, 5/7/8=string (7 NULL-tolerant),
6 reserved (unused → assert(0)). The cond block repeats the same
switch (case 1 uses the OR_BIGINT_SIZE spelling of the same width the
changed block writes as OR_INT64_SIZE).
Invariant: the three DML arrays are index-parallel and freed together
— clear_data_item frees all three as a unit and resets the count to 0,
so a double-clear is a no-op and the parallelism cannot drift into a
double-free.
8.7 Cleanup and finalization
Section titled “8.7 Cleanup and finalization”// cubrid_log_clear_log_item -- src/api/cubrid_log.c ...guards: g_stage != EXTRACTION; log_item_list NULL... for (i = 0; i < g_log_items_count; i++) /* if count == 0, no-op */ cubrid_log_clear_data_item ((DATA_ITEM_TYPE) g_log_items[i].data_item_type, &g_log_items[i].data_item); g_log_items_count = 0;clear_data_item frees only DML column arrays; DDL/DCL/TIMER arms are
no-ops (their pointers alias the blob). Note the loop iterates
g_log_items[], not the passed argument (which is only null-checked).
flowchart TB
A["cubrid_log_finalize"] --> B{"stage in PREPARATION/EXTRACTION?"}
B -->|no| ERR["INVALID_FUNC_CALL_STAGE"]
B -->|yes| C["disconnect_server"]
C -->|fail| H1["reset_globals; stage=CONFIGURATION; FAILED_DISCONNECT"]
C -->|ok| D["reset_globals"] --> E["g_stage = CONFIGURATION; SUCCESS"]
Figure 8-4 — cubrid_log_finalize always resets globals and stage, even
on the disconnect-failure path, returning the library to a usable
CONFIGURATION state rather than wedging it.
cubrid_log_disconnect_server sends NET_SERVER_CDC_END_SESSION, reads a
one-int reply, returns FAILED_DISCONNECT (-3) on reply_code != NO_ERROR, and either way css_free_conns g_conn_entry.
cubrid_log_reset_globals restores defaults (timeouts 300, max_log_item
512), frees g_extraction_table and each g_extraction_user[i], resets
the trace log, sets g_next_lsa = LSA_INITIALIZER, and leaks
g_log_infos on purpose (free commented out, pointer merely nulled).
8.8 Return-code vocabulary
Section titled “8.8 Return-code vocabulary”| Code | Meaning | Produced where |
|---|---|---|
SUCCESS (0) | Normal success. | Every entry point. |
SUCCESS_WITH_NO_LOGITEM (1) | Empty batch. | Vocabulary only — dead rc check; empty batch is num_infos == 0. |
SUCCESS_WITH_ADJUSTED_LSA (2) | Start LSA snapped. | Vocabulary only — shadowed by the duplicate if in find_start_lsa. |
INVALID_FUNC_CALL_STAGE (-27) | Wrong stage. | Every guarded entry point. |
EXTRACTION_TIMEOUT (-6) | Nothing within the timeout. | extract_internal (ER_CDC_EXTRACTION_TIMEOUT). |
INVALID_LSA (-5) | Cursor LSA archived away. | extract_internal (ER_CDC_INVALID_LOG_LSA). |
UNAVAILABLE_CDC_SERVER (-34) | Server lacks supplemental_log. | send_configurations (ER_CDC_NOT_AVAILABLE). |
FAILED_LOGIN (-33) | Auth or non-DBA. | db_login. |
FAILED_MALLOC (-28) | Allocation failed. | setters, extract_internal, make_dml, make_log_item_list. |
8.9 Chapter summary — key takeaways
Section titled “8.9 Chapter summary — key takeaways”- The whole session is global state, gated by
g_stage—CONFIGURATION → PREPARATION → EXTRACTION(FINALIZATIONvestigial), enforced per-function viaINVALID_FUNC_CALL_STAGE; no handle. - Setters only write globals; the network is touched once in
send_configurations, so there is no mid-stream filter race. - Connect is a three-step composite:
db_login(throwawaydb_restartenforcing DBA) →connect_server_internal(CSS handshake, Windows two-phase port handoff) →send_configurations;g_stageadvances only if all succeed. - Extraction is two requests:
…_METADATAreturnsnext_lsa | num_infos | total_lengthand sizesg_log_infosby a grow-onlyrealloc;GET_LOGINFOfills it (skipped at length 0). The consumer owns the cursor via the in/outlsa. - Decoding aliases into the blob (
or_unpack_string_nocopy); onlymake_dmlallocates three index-parallel column arrays, whichclear_data_itemfrees as a unit. pack_func_codeis the per-column type tag (0=int, 1=int64, 2=float, 3=double, 4=short, 5/7/8=string with 7 NULL-tolerant, 6 reserved).SUCCESS_WITH_NO_LOGITEMandSUCCESS_WITH_ADJUSTED_LSAare ABI vocabulary but never produced (dead branches), so a modifier who “fixes” them changes observable behavior;finalizealways resets toCONFIGURATIONeven on disconnect failure and leaksg_log_infoson purpose.
Chapter 9: Edge Paths Legacy Applier and the Shared Log Reader
Section titled “Chapter 9: Edge Paths Legacy Applier and the Shared Log Reader”Chapters 2–8 traced the happy path of the modern CDC walker. This chapter collects what lives off that lifecycle, in three clusters:
- 9.1–9.3 the
log_readershared forward WAL walker — and the surprise that CDC does not use it. - 9.4 edge events the dispatcher must survive: dropped aborts, keep-alive records, multi-statement DDL, filter races.
- 9.5–9.8 the legacy
la_*HA daemon (applylogdb) that reads the same WAL but reconstructs from physicalUNDOREDOrecords, plus its slave-side SQL audit log.
For the design-level “why two pipelines exist” framing, see the companion cubrid-cdc.md. This chapter traces the code, not the theory.
9.1 log_reader — the shared forward walker
Section titled “9.1 log_reader — the shared forward walker”log_reader (in log_reader.hpp) is a small RAII wrapper owning one aligned log page and a cursor LSA, with methods to fetch, align, and copy bytes forward through the WAL. It is the canonical sequential-read abstraction, used by log_recovery today.
classDiagram
class log_reader {
-THREAD_ENTRY* m_thread_entry
-log_lsa m_lsa
-LOG_CS_ACCESS_MODE m_cs_access
-log_page* m_page
-char m_area_buffer[IO_MAX_PAGE_SIZE+DOUBLE_ALIGNMENT]
+set_lsa_and_fetch_page(lsa, fetch_mode)
+align() add_align() advance_when_does_not_fit()
+does_fit_in_current_page() copy_from_log() skip()
}
log_reader --> log_page : m_page points into m_area_buffer
Figure 9-1 — log_reader owns its page buffer. m_page is m_area_buffer re-pointed to a MAX_ALIGNMENT boundary: self-contained, no shared buffer, no locking.
| Field | Role | Why it exists |
|---|---|---|
m_thread_entry | Cached THREAD_ENTRY*, lazily filled | Encodes the single-thread assumption. |
m_lsa | Cursor (pageid, offset) | The walk state; advance/align/skip mutate it. |
m_cs_access | LOG_CS_ACCESS_MODE for the fetch | Whether to take the log critical section. |
m_page | Live page, aligned in m_area_buffer | The decoded page the cursor reads. |
m_area_buffer | IO_MAX_PAGE_SIZE + DOUBLE_ALIGNMENT bytes | Slack lets m_page slide to a boundary. |
The constructor wires the alignment:
// log_reader::log_reader -- src/transaction/log_reader.hpplog_reader::log_reader (LOG_CS_ACCESS_MODE cs_access) : m_cs_access (cs_access){ m_page = reinterpret_cast<log_page *> (PTR_ALIGN (m_area_buffer, MAX_ALIGNMENT)); /* <- slide into buffer */}INVARIANT — one walker, one thread. The header states it:
NOTE: not thread safe.m_thread_entryis cached on first use and every mutating methodassert (thread_p == &cubthread::get_entry ()). The contract is each walker owns its own instance. If two threads share onelog_reader, the cachedm_thread_entrybelongs to whoever touched it first,m_lsa/m_pagerace, and the page buffer is silently corrupted. CDC honors this with private per-daemon buffers (9.3); the legacy applier honors it by being single-threaded.
fetch_mode expresses when a page is re-read. FORCE is a LETS-port leftover — recovery only passes NORMAL — kept for future reuse: enum class fetch_mode { NORMAL, FORCE };.
9.2 Fetch, align, and boundary helpers — branch by branch
Section titled “9.2 Fetch, align, and boundary helpers — branch by branch”set_lsa_and_fetch_page decides whether a disk fetch is needed. Branch A: do_fetch_page true (caller forced FORCE, or target pageid differs from the cursor’s page) → one logpb_fetch_page. Branch B: false (the LSA is on the page already in m_page) → only m_lsa moves, no I/O. “Refetch only on pageid change” is the performance point — repeated offset advances inside a page are free.
// log_reader::set_lsa_and_fetch_page -- src/transaction/log_reader.hppconst bool do_fetch_page { fetch_page_mode == fetch_mode::FORCE || m_lsa.pageid != lsa.pageid }; /* <- refetch only on page change */m_lsa = lsa;if (do_fetch_page) { ... return fetch_page (get_thread_entry ()); }return NO_ERROR;fetch_page is the only place I/O happens and is fatal-on-failure — a failed fetch during a forward walk means the WAL is unreadable, so it escalates to logpb_fatal_error; the trailing return ER_FAILED is defensive (the fatal call does not return in practice):
// log_reader::fetch_page -- src/transaction/log_reader.hppif (logpb_fetch_page (thread_p, &m_lsa, m_cs_access, m_page) != NO_ERROR) { logpb_fatal_error (thread_p, true, ARG_FILE_LINE, "log_reader::fetch_page"); return ER_FAILED; }The alignment helpers wrap three free functions sharing the “fetch the next page when the cursor crosses LOGAREA_SIZE” pattern:
// LOG_READ_ALIGN -- src/transaction/log_reader.hpplsa->offset = DB_ALIGN (lsa->offset, DOUBLE_ALIGNMENT); /* <- round up to 8B */while (lsa->offset >= (int) LOGAREA_SIZE) { /* <- spilled past page end? */ lsa->pageid++; if (logpb_fetch_page (...) != NO_ERROR) logpb_fatal_error (...); lsa->offset -= LOGAREA_SIZE; /* <- carry remainder onto new page */ lsa->offset = DB_ALIGN (lsa->offset, DOUBLE_ALIGNMENT);}LOG_READ_ALIGN(align): roundoffsettoDOUBLE_ALIGNMENT; thewhile(notif) handles a multi-page overrun, fetching and subtractingLOGAREA_SIZEper page.LOG_READ_ADD_ALIGN(add_align):offset += addthenLOG_READ_ALIGN. Used byreinterpret_copy_and_add_align<T>()to step past a struct and re-align in one call.LOG_READ_ADVANCE_WHEN_DOESNT_FIT(advance_when_does_not_fit): a singleif— ifoffset + length >= LOGAREA_SIZEthe record straddles a boundary, sopageid++, fetch (fatal on failure),offset = 0to start the whole record on the fresh page; the page tail is skipped.
does_fit_in_current_page(size) is a side-effect-free predicate (m_lsa.offset + size < LOGAREA_SIZE) choosing between an in-place read and copy_from_log. copy_from_log(dest, length) defers to logpb_copy_from_log, which advances the page internally on a straddle. skip(size): branch (1) offset + size < LOGAREA_SIZE → offset += size; branch (2) a while drains size page-by-page, fetching, resetting offset = 0, and re-aligning via align() after each page wrap.
9.3 The CDC drift: CDC does not use log_reader
Section titled “9.3 The CDC drift: CDC does not use log_reader”The load-bearing cross-check for anyone editing CDC: despite log_reader being the general forward walker, the modern CDC producer does not instantiate it. CDC reads pages through three log_impl.h macros maintaining a private two-slot cache on cdc_Gl.producer.temp_logbuf[0..1]:
// CDC_GET_TEMP_LOGPAGE -- src/transaction/log_impl.hif (cdc_Gl.producer.temp_logbuf[(process_lsa)->pageid % 2].log_page_p->hdr.logical_pageid != (process_lsa)->pageid) { if (logpb_fetch_page (..., LOG_CS_FORCE_USE, (log_page_p)) != NO_ERROR) { goto error; } memcpy (cdc_Gl.producer.temp_logbuf[(process_lsa)->pageid % 2].log_page_p, (log_page_p), IO_MAX_PAGE_SIZE);} else { (log_page_p) = cdc_Gl.producer.temp_logbuf[(process_lsa)->pageid % 2].log_page_p; }Indexed by pageid % 2, the cache keeps the current page and its neighbor live at once — exactly what undo/redo dual-LSA reconstruction needs (Chapter 6) and a single-page log_reader cannot offer. Both slots are aligned at startup. CDC_CHECK_TEMP_LOGPAGE swaps the active slot on a parity change; CDC_UPDATE_TEMP_LOGPAGE re-fetches a cached page after a write may have extended it.
Drift callout. If
cubrid-cdc.mddescribes the producer as “using the shared log reader,” that is aspirational: as of this revision the producer usesCDC_*_TEMP_LOGPAGEwith its own two-page buffer. An editor wanting to adoptlog_readermust first add a two-page buffering mode preserving thetemp_logbuf[pageid % 2]parity trick, because dual-LSA reconstruction needs the undo and redo pages simultaneously resident.
9.4 Edge events the dispatcher must survive
Section titled “9.4 Edge events the dispatcher must survive”Beyond the single-change flow, la_log_record_process (and the modern dispatcher it mirrors) must absorb record kinds that produce no downstream change.
Aborts are dropped, not reconstructed. la_apply_repl_log short-circuits on LOG_ABORT: the collected LA_ITEM list is freed and LSA bookkeeping rolls forward. Modern CDC likewise never surfaces an aborted transaction’s rows.
// la_apply_repl_log -- src/transaction/log_applier.cif (rectype == LOG_ABORT) { la_clear_applied_info (apply); return NO_ERROR; } /* <- discard item list */LOG_DUMMY_HA_SERVER_STATE is a keep-alive / role-change signal, carrying no row data. Three outcomes: (1) state changed to not ACTIVE/TO_BE_STANDBY while holding the dbname lock → set is_role_changed, return ER_INTERRUPTED so the outer loop releases the lock; (2) state active, no pending repl lists → record at_time and la_log_commit(true) to flush a heartbeat into _db_ha_apply_info; (3) otherwise fall through.
// la_log_record_process, LOG_DUMMY_HA_SERVER_STATE -- src/transaction/log_applier.celse if (la_is_repl_lists_empty ()) { (void) la_update_ha_apply_info_log_record_time (ha_server_state->at_time); error = la_log_commit (true); /* <- keep-alive commit */}Multi-statement DDL is handled in la_apply_statement_log’s case cascade (9.6): a CUBRID_STMT_CREATE_CLASS item fans out into the statement plus a GRANT ALL PRIVILEGES ... TO <db_user> so the object lands with the right owner. The SQL-logging side wraps DDL carrying an ha_sys_prm in a SET SYSTEM PARAMETERS prologue/epilogue, turning one logical change into three written statements.
Filter reconfiguration races. The filter (LA_REPL_FILTER, 9.7) is read on the hot path by la_need_filter_out but built once at startup by la_create_repl_filter. There is no lock around filter->list / filter->num_filters; the design assumes the filter is immutable for the applier’s lifetime, so changing ha_repl_filter_file requires a restart. Adding live reconfiguration would need synchronization here.
9.5 The legacy la_* HA daemon — outer loop
Section titled “9.5 The legacy la_* HA daemon — outer loop”la_apply_log_file is applylogdb’s entire main loop — the legacy analogue of the modern producer daemon (Chapter 5), but it reads physical UNDOREDO records and replays them as SQL against a live slave.
flowchart TB
init["la_init + sl_init + la_init_cache_pb\nla_find_log_pagesize\nla_get_last_ha_applied_info"] --> filt["build REPL_FILTER if configured"]
filt --> outer{"do: la_applier_need_shutdown?"}
outer -->|"la_apply_pre seeds final_lsa"| inner{"while final_lsa not null"}
inner --> arv["delete old archives\nrefresh final_log_hdr"]
arv --> state{"server state ACTIVE?"}
state -->|"DONE and not active"| relock["unlock dbname, log_commit, RECOVERING, sleep"]
state -->|"page not yet written"| sleep1["usleep, continue"]
state -->|"valid"| getpage["la_get_page_buffer(final_lsa.pageid)"]
getpage -->|"NULL"| retry{"retry < LA_GET_PAGE_RETRY_COUNT?"}
retry -->|"yes"| sleep1
retry -->|"no"| corrupt["ER_LOG_PAGE_CORRUPTED, shutdown"]
getpage -->|"page ok"| recloop["per-record: la_log_record_process"]
recloop --> commit["la_check_time_commit\nla_check_mem_size\nla_change_state"]
commit --> inner
inner --> outer
outer -->|"shutdown"| done["reinit_copylog? la_shutdown, return error"]
Figure 9-2 — la_apply_log_file control flow, branch by branch. Setup: la_init (9.7), optional sl_init, la_check_duplicated (one applier per log path), cache/pagesize/recdes-pool init, then la_get_last_ha_applied_info reads the restart point from _db_ha_apply_info. Filter built only if PRM_ID_HA_REPL_FILTER_TYPE != REPL_FILTER_NONE; replay then starts at required_lsa. Archive GC runs every remove_arv_interval_in_secs, or — if the interval is 0 — only when nxarv_num advanced. Demotion (applier DONE, master no longer ACTIVE/TO_BE_STANDBY): release the dbname lock, la_log_commit(false), go RECOVERING, sleep, continue. Page-existence: eof_lsa.pageid < final_lsa.pageid → short sleep; la_get_page_buffer NULL → distinguish not-synced vs past-append_lsa vs corrupted, bounded retry_count before ER_LOG_PAGE_CORRUPTED; a negative hdr.offset is a torn page, skipped if the next page exists else refetched. Inner record loop: adjust a zero offset, check end-of-log against eof_lsa/append_lsa, verify prev_final == lrec->back_lsa (chain integrity, fatal on mismatch), then la_log_record_process; error fan-out separates server-down (ER_NET_CANT_CONNECT_SERVER), exceed-mem/flush (shutdown), and page-corrupted (invalidate, refetch). Teardown: per-page la_check_time_commit / la_check_mem_size / la_change_state; on reinit_copylog delete _db_ha_apply_info and force an error; always la_shutdown.
9.6 Per-record dispatch — la_apply_repl_log and la_apply_statement_log
Section titled “9.6 Per-record dispatch — la_apply_repl_log and la_apply_statement_log”la_apply_repl_log runs at LOG_COMMIT / LOG_SYSOP_END / LOG_ABORT. After the abort and “nothing to do” short-circuits, it locks the dbname and walks the transaction’s LA_ITEM list. For each item ahead of last_committed_rep_lsa and not filtered: LOG_REPLICATION_DATA switches on item_type (RVREPL_DATA_UPDATE*/INSERT/DELETE → la_apply_{update,insert,delete}_log; else assert_release(false)); LOG_REPLICATION_STATEMENT → la_apply_statement_log. Error branches: flush error → goto end; server-down → ER_NET_CANT_CONNECT_SERVER, goto end; else retryable-and-not-ignorable → LA_SLEEP(10,0) and continue (retry the same item). The retryable set is LA_RETRY_ON_ERROR (lock/page-latch timeouts, deadlock, TDE-cipher-not-loaded) plus the user ha_applylogdb_retry_error_list via la_retry_on_error.
la_apply_statement_log is the DDL/statement path. The la_flush_repl_items(true) at the top is the load-bearing ordering barrier; the is_ddl fallthrough and the DROP_LABEL/default tolerance arm are the two other non-obvious branches:
// la_apply_statement_log -- src/transaction/log_applier.cerror = la_flush_repl_items (true); /* <- drain pending DML first; ordering barrier */if (error != NO_ERROR) return error;switch (item->item_type) { case CUBRID_STMT_CREATE_CLASS: ... case CUBRID_STMT_UPDATE_STATS: is_ddl = true; /* FALLTHROUGH into the execute arm */ case CUBRID_STMT_INSERT: ... case CUBRID_STMT_UPDATE: /* set user, write SQL log, set sysprm, execute, restore */ break; case CUBRID_STMT_DROP_LABEL: default: return NO_ERROR; /* <- silently ignore unknown/obsolete statements */}The barrier ensures queued DML hits the slave before a schema change, else it runs against the wrong schema. TRUNCATE is the one DML-shaped statement that respects the filter mid-switch. DDL bumps schema_counter, DML bumps insert/update/delete counters by the affected-row count res, a final error bumps fail_counter and raises ER_HA_LA_FAILED_TO_APPLY_STATEMENT, and the DROP_LABEL/default arm returns NO_ERROR to tolerate obsolete record types rather than crash.
9.7 Restart bookkeeping and initialization
Section titled “9.7 Restart bookkeeping and initialization”LA_HA_APPLY_INFO is the in-memory image of the _db_ha_apply_info catalog row — the persistent record that lets applylogdb restart where it stopped.
| Field | Role | Why it exists |
|---|---|---|
db_name | Slave DB name | Catalog row identity. |
creation_time | Master creation timestamp | Detect a rebuilt peer. |
copied_log_path | Copied-log volume path | Locate the WAL. |
committed_lsa | Last committed commit-log LSA | Commit-replay restart point. |
committed_rep_lsa | Last committed replication-log LSA | Item-replay restart point. |
append_lsa / eof_lsa | Master append / EOF snapshots | Lag and end-of-record detection. |
final_lsa | Last processed LSA | Scan cursor. |
required_lsa | Start LSA of oldest unfinished tran | Retention bound; drives archive GC. |
log_record_time / log_commit_time / last_access_time / start_time | Timestamps | Heartbeat / lag via applyinfo. |
status | IDLE / BUSY | Actively-replaying flag. |
insert/update/delete/schema/commit/fail_counter (INT64) | Cumulative counters | Metrics across restarts. |
la_init_ha_apply_info is the trivial reset: memset then LSA_SET_NULL each of the six LSAs — a zeroed struct is not a null LSA, so explicit nulling matters.
// la_init_ha_apply_info -- src/transaction/log_applier.cmemset ((void *) ha_apply_info, 0, sizeof (LA_HA_APPLY_INFO));LSA_SET_NULL (&ha_apply_info->committed_lsa); /* ... + 5 more LSAs ... */INVARIANT — the catalog row is the source of truth for restart.
la_get_last_ha_applied_infoloads_db_ha_apply_infointola_Infoat startup;la_log_commitwrites it back transactionally (la_update_ha_last_applied_infothenla_commit_transaction). The invariant is never advance the in-memorycommitted_lsapast what was durably written to the catalog; violating it would silently skip transactions after a crash.
la_log_commit — branch trace (the update_commit_time bool is why callers distinguish keep-alive from demotion):
// la_log_commit -- src/transaction/log_applier.c(void) la_find_required_lsa (&la_Info.required_lsa);LSA_COPY (&la_Info.append_lsa, ...->append_lsa); LSA_COPY (&la_Info.eof_lsa, ...->eof_lsa); /* (1) always snapshot */if (update_commit_time) { la_Info.log_commit_time = time (0); } /* (2) stamp only if true */error = la_flush_repl_items (true);if (error != NO_ERROR) { return error; } /* (3) flush fail -> no catalog write */res = la_update_ha_last_applied_info ();if (res > 0) { error = la_commit_transaction (); } /* (4a) wrote -> commit */else { la_Info.fail_counter++; if (ER_IS_SERVER_DOWN_ERROR (res)) error = ER_NET_CANT_CONNECT_SERVER; /* (4b) propagate */ else { er_set (...); error = NO_ERROR; } /* (4c) swallow; replay redoes */}The arms are marked (1)–(4c) inline: (2) stamps log_commit_time only on the keep-alive caller, so a forced demotion commit does not advance the heartbeat clock; (3) returns before any catalog write; (4c) swallows non-server-down errors to NO_ERROR so replay redoes from the last persisted LSA.
la_init resets the la_Info global (not the catalog): memset, copy the log path, set default page/cache sizes (LA_DEFAULT_*), mark both log volume descriptors NULL_VOLDES, null all eight working LSAs, set last_deleted_archive_num = -1, capture start_vsize/start_time (baseline for la_check_mem_size), init the replication-objects workspace for DB_CLIENT_TYPE_LOG_APPLIER, and set repl_filter.type = REPL_FILTER_NONE. It does not allocate the cache buffer or recdes pool — those happen later in la_apply_log_file, once the log-header page size is known.
REPL_FILTER_TYPE and container LA_REPL_FILTER:
// REPL_FILTER_TYPE -- src/transaction/log_applier.htypedef enum { REPL_FILTER_NONE, REPL_FILTER_INCLUDE_TBL, REPL_FILTER_EXCLUDE_TBL } REPL_FILTER_TYPE;| Value | Meaning | la_need_filter_out result |
|---|---|---|
REPL_FILTER_NONE | No filtering | always false |
REPL_FILTER_INCLUDE_TBL | Whitelist | filter out if the class is not in list |
REPL_FILTER_EXCLUDE_TBL | Blacklist | filter out if the class is in list |
LA_REPL_FILTER fields: list (char** table names), list_size (allocated capacity), num_filters (count in use), type. la_need_filter_out strips [...] brackets from item->class_name, exempts statement replication (except TRUNCATE) and the _db_serial system class, then case-insensitively linear-scans list.
9.8 LA_ITEM and the slave-side SQL audit log
Section titled “9.8 LA_ITEM and the slave-side SQL audit log”LA_ITEM is one queued replication change — the legacy counterpart to the modern per-change event (Chapter 6).
| Field | Role | Why it exists |
|---|---|---|
next / prev | List pointers | Per-transaction chain, replayed in LSA order. |
log_type | LOG_REPLICATION_DATA / ..._STATEMENT | Top-level dispatch key. |
item_type | RVREPL_DATA_* or CUBRID_STMT_* | Second-level dispatch. |
class_name | Target table | Filter match, SQL generation. |
db_user | Owner to impersonate | DDL via AU_SET_USER. |
ha_sys_prm | HA system parameters | Wrap DDL in SET SYSTEM PARAMETERS. |
packed_key_value / packed_key_value_length | Disk image of the PK | Lazy-unpacked into key. |
key | Unpacked PK DB_VALUE | Update/delete WHERE anchor; statement text for statements. |
lsa | LSA of the replication-log record | Ordering, committed_rep_lsa cursor. |
target_lsa | LSA of the target data-log record | Locate the physical UNDOREDO record. |
The slave-side SQL audit log (log_applier_sql_log.c) optionally records every applied statement as replayable SQL for forensics. Its global sl_Info holds just curr_file_id and last_inserted_sql_id; the FILE* handles (log_fp, catalog_fp), sql_log_max_cnt, and path buffers are separate file-static globals.
sl_write_sql — the per-statement writer. Branches: lazy-open; emit a -- metadata header (timestamp, monotonic sql_id, select length, query length); optional verification SELECT; write the query; flush; persist the cursor via sl_write_catalog; roll over SL_LOG_FILE_MAX_SIZE, pruning the oldest.
// sl_write_sql -- src/transaction/log_applier_sql_log.cif (log_fp == NULL) { if ((log_fp = sl_log_open ()) == NULL) return ER_FAILED; }fprintf (log_fp, "-- %s | %u | %zu | %zu\n", time_buf, ++sl_Info.last_inserted_sql_id, ...); /* <- header */fwrite (query.get_buffer (), sizeof (char), query.len (), log_fp); fflush (log_fp);sl_write_catalog ();if (ftell (log_fp) >= SL_LOG_FILE_MAX_SIZE) { log_fp = sl_open_next_file (log_fp); sl_delete_oldest_file_if_needed (); }sl_log_open opens <base>.<curr_file_id>: "r+" if it exists (rolling forward if over size), else create "w"; on failure return NULL (caller → ER_FAILED).
sl_read_catalog restores the rolling-file cursor at startup — three branches: missing catalog → bootstrap via sl_write_catalog; unreadable line → ER_FAILED; malformed line (≠ two fields) → ER_FAILED:
// sl_read_catalog -- src/transaction/log_applier_sql_log.cread_catalog_fp = fopen (sql_catalog_path, "r");if (read_catalog_fp == NULL) return sl_write_catalog (); /* <- no catalog yet: create it */if (fgets (info, LINE_MAX, read_catalog_fp) == NULL) { ...; return ER_FAILED; }if (sscanf (info, CATALOG_FORMAT, &sl_Info.curr_file_id, &sl_Info.last_inserted_sql_id) != 2) { fclose (read_catalog_fp); return ER_FAILED; } /* <- corrupt catalog */The format "%u | %010u" (curr_file_id, last_inserted_sql_id) is the single line that lets the audit log resume numbering across restarts.
Why legacy is schema-fragile, and modern CDC is not. The legacy applier reconstructs each change from the physical
UNDOREDOrecord: it must know the slave’s class layout, attribute ordering, and PK constraints at replay time (notesl_print_pkwalkingclassobj_find_class_primary_key, and the DDL owner-rewriting in 9.6). If the slave schema drifts from the master’s — a column added out of band, a different attribute order — reconstruction produces wrong or rejected SQL. Modern CDC sidesteps this: the producer emits supplemental records that already name columns and carry typed values (Chapters 2, 6, 7), so the consumer never reverse-engineers physical layout. This is the central reason the two pipelines coexist. Seecubrid-cdc.mdfor the design rationale.
9.9 Chapter summary — key takeaways
Section titled “9.9 Chapter summary — key takeaways”log_readeris the project’s forward WAL walker, but CDC keeps its own two-slottemp_logbuf[pageid % 2]cache viaCDC_*_TEMP_LOGPAGEbecause dual-LSA reconstruction needs two pages resident — treat any “CDC uses log_reader” claim as aspirational.set_lsa_and_fetch_pagedoes I/O only on a pageid change (orFORCE), the alignment helpers fetch onLOGAREA_SIZEoverrun (fatal vialogpb_fatal_error), and the class is not thread-safe by contract — each walker owns its instance, asserted against the cachedm_thread_entry.- Aborts free the item list and roll forward; an idle
LOG_DUMMY_HA_SERVER_STATEcommits a heartbeat viala_log_commit(true), while a role change returnsER_INTERRUPTEDto release the lock. la_apply_log_fileis a heavily-defensive page loop with retry-bounded fetches, chain-integrity checks (prev_final == back_lsa), and an error fan-out separating retry, shutdown, and corruption.la_log_commitalways snapshotsappend_lsa/eof_lsa, stampslog_commit_timeonly whenupdate_commit_time(true=keep-alive, false=demotion), returns early on flush error, and on a failed catalog write swallows non-server-down errors so replay redoes from the last durable LSA — the restart-safety invariant.- The legacy applier is schema-fragile because it reconstructs from physical
UNDOREDOrecords and replays SQL (owner/sysprm rewriting, optionalsl_write_sqlaudit trail), whereas modern supplemental-record CDC ships column-named typed values and is immune to slave-side layout drift.
Position hints as of this revision
Section titled “Position hints as of this revision”The following are line numbers as observed on 2026-06-23; symbols are the canonical anchor and line numbers are hints that decay.
| Symbol | File | Line |
|---|---|---|
CUBRID_LOG_STAGE | src/api/cubrid_log.c | 85 |
DATA_ITEM_TYPE | src/api/cubrid_log.c | 93 |
cubrid_log_set_connection_timeout | src/api/cubrid_log.c | 158 |
cubrid_log_set_extraction_timeout | src/api/cubrid_log.c | 181 |
cubrid_log_set_tracelog | src/api/cubrid_log.c | 406 |
cubrid_log_set_max_log_item | src/api/cubrid_log.c | 457 |
cubrid_log_set_all_in_cond | src/api/cubrid_log.c | 480 |
cubrid_log_set_extraction_table | src/api/cubrid_log.c | 504 |
cubrid_log_set_extraction_user | src/api/cubrid_log.c | 535 |
cubrid_log_connect_server_internal | src/api/cubrid_log.c | 566 |
cubrid_log_send_configurations | src/api/cubrid_log.c | 686 |
cubrid_log_db_login | src/api/cubrid_log.c | 797 |
cubrid_log_connect_server | src/api/cubrid_log.c | 851 |
cubrid_log_find_start_lsa | src/api/cubrid_log.c | 929 |
cubrid_log_find_start_lsa | src/api/cubrid_log.c | 980 |
cubrid_log_find_lsa | src/api/cubrid_log.c | 1032 |
cubrid_log_extract_internal | src/api/cubrid_log.c | 1083 |
cubrid_log_make_ddl | src/api/cubrid_log.c | 1243 |
cubrid_log_make_dml | src/api/cubrid_log.c | 1262 |
cubrid_log_make_dcl | src/api/cubrid_log.c | 1492 |
cubrid_log_make_timer | src/api/cubrid_log.c | 1507 |
cubrid_log_make_data_item | src/api/cubrid_log.c | 1521 |
cubrid_log_make_log_item | src/api/cubrid_log.c | 1576 |
cubrid_log_make_log_item_list | src/api/cubrid_log.c | 1608 |
cubrid_log_extract | src/api/cubrid_log.c | 1679 |
cubrid_log_clear_data_item | src/api/cubrid_log.c | 1736 |
cubrid_log_clear_log_item | src/api/cubrid_log.c | 1791 |
cubrid_log_disconnect_server | src/api/cubrid_log.c | 1829 |
cubrid_log_reset_globals | src/api/cubrid_log.c | 1899 |
cubrid_log_finalize | src/api/cubrid_log.c | 1949 |
DDL | src/api/cubrid_log.h | 71 |
ddl | src/api/cubrid_log.h | 72 |
DML | src/api/cubrid_log.h | 82 |
dml | src/api/cubrid_log.h | 83 |
DCL | src/api/cubrid_log.h | 97 |
dcl | src/api/cubrid_log.h | 98 |
TIMER | src/api/cubrid_log.h | 104 |
timer | src/api/cubrid_log.h | 105 |
CUBRID_DATA_ITEM | src/api/cubrid_log.h | 110 |
cubrid_data_item | src/api/cubrid_log.h | 111 |
CUBRID_LOG_ITEM | src/api/cubrid_log.h | 119 |
cubrid_log_item | src/api/cubrid_log.h | 120 |
ER_CDC_IGNORE_LOG_INFO_INTERNAL | src/base/error_code.h | 1654 |
ER_CDC_IGNORE_TRANSACTION | src/base/error_code.h | 1655 |
ER_CDC_NULL_EXTRACTION_LSA | src/base/error_code.h | 1659 |
ER_CDC_LOGINFO_ENTRY_GENERATED | src/base/error_code.h | 1660 |
ER_CDC_IGNORE_LOG_INFO | src/base/error_code.h | 1661 |
scdc_find_lsa | src/communication/network_interface_sr.cpp | 11258 |
scdc_get_loginfo_metadata | src/communication/network_interface_sr.cpp | 11314 |
recdes | src/storage/storage_common.h | 220 |
REGISTER_DAEMON | src/thread/thread_manager.hpp | 498 |
log_Zip_min_size_to_compress | src/transaction/log_append.cpp | 41 |
data_header_length | src/transaction/log_append.cpp | 1290 |
la_repl_filter | src/transaction/log_applier.c | 206 |
la_item | src/transaction/log_applier.c | 237 |
la_ha_apply_info | src/transaction/log_applier.c | 394 |
la_init_ha_apply_info | src/transaction/log_applier.c | 606 |
la_apply_statement_log | src/transaction/log_applier.c | 5496 |
la_apply_repl_log | src/transaction/log_applier.c | 5739 |
la_log_record_process | src/transaction/log_applier.c | 6101 |
la_log_commit | src/transaction/log_applier.c | 6531 |
la_init | src/transaction/log_applier.c | 6917 |
la_need_filter_out | src/transaction/log_applier.c | 7723 |
la_apply_log_file | src/transaction/log_applier.c | 8074 |
LA_RETRY_ON_ERROR | src/transaction/log_applier.h | 34 |
REPL_FILTER_TYPE | src/transaction/log_applier.h | 48 |
sl_write_catalog | src/transaction/log_applier_sql_log.c | 116 |
sl_read_catalog | src/transaction/log_applier_sql_log.c | 142 |
sl_write_sql | src/transaction/log_applier_sql_log.c | 519 |
sl_log_open | src/transaction/log_applier_sql_log.c | 570 |
MAKE_ZIP_LEN | src/transaction/log_compress.h | 33 |
log_zip | src/transaction/log_compress.h | 53 |
CDC_GET_TEMP_LOGPAGE | src/transaction/log_impl.h | 240 |
CDC_CHECK_TEMP_LOGPAGE | src/transaction/log_impl.h | 260 |
CDC_UPDATE_TEMP_LOGPAGE | src/transaction/log_impl.h | 271 |
MAX_CDC_LOGINFO_QUEUE_ENTRY | src/transaction/log_impl.h | 297 |
MAX_CDC_LOGINFO_QUEUE_SIZE | src/transaction/log_impl.h | 298 |
cdc_producer_state | src/transaction/log_impl.h | 787 |
CDC_PRODUCER_STATE | src/transaction/log_impl.h | 787 |
CDC_PRODUCER_STATE | src/transaction/log_impl.h | 792 |
cdc_consumer_request | src/transaction/log_impl.h | 794 |
CDC_CONSUMER_REQUEST | src/transaction/log_impl.h | 799 |
cdc_producer_request | src/transaction/log_impl.h | 801 |
CDC_PRODUCER_REQUEST | src/transaction/log_impl.h | 806 |
cdc_loginfo_entry | src/transaction/log_impl.h | 808 |
CDC_LOGINFO_ENTRY | src/transaction/log_impl.h | 813 |
cdc_temp_logbuf | src/transaction/log_impl.h | 815 |
CDC_TEMP_LOGBUF | src/transaction/log_impl.h | 815 |
CDC_TEMP_LOGBUF | src/transaction/log_impl.h | 819 |
cdc_producer | src/transaction/log_impl.h | 821 |
CDC_PRODUCER | src/transaction/log_impl.h | 821 |
temp_logbuf | src/transaction/log_impl.h | 842 |
CDC_PRODUCER | src/transaction/log_impl.h | 848 |
cdc_consumer | src/transaction/log_impl.h | 850 |
CDC_CONSUMER | src/transaction/log_impl.h | 850 |
CDC_CONSUMER | src/transaction/log_impl.h | 867 |
cdc_global | src/transaction/log_impl.h | 869 |
CDC_GLOBAL | src/transaction/log_impl.h | 869 |
loginfo_queue | src/transaction/log_impl.h | 877 |
CDC_GLOBAL | src/transaction/log_impl.h | 885 |
ovf_page_list | src/transaction/log_impl.h | 888 |
cdc_dataitem_type | src/transaction/log_impl.h | 896 |
CDC_DATAITEM_TYPE | src/transaction/log_impl.h | 902 |
cdc_dcl_type | src/transaction/log_impl.h | 904 |
CDC_DCL_TYPE | src/transaction/log_impl.h | 908 |
cdc_dml_type | src/transaction/log_impl.h | 910 |
CDC_DML_TYPE | src/transaction/log_impl.h | 910 |
CDC_DML_TYPE | src/transaction/log_impl.h | 918 |
CDC_IS_IGNORE_LOGINFO_ERROR | src/transaction/log_manager.c | 167 |
LOG_DUMMY_HA_SERVER_STATE | src/transaction/log_manager.c | 489 |
log_append_repl_info_and_commit_log | src/transaction/log_manager.c | 4647 |
log_append_commit_log | src/transaction/log_manager.c | 4779 |
log_append_supplemental_info | src/transaction/log_manager.c | 4837 |
log_append_supplemental_lsa | src/transaction/log_manager.c | 4892 |
log_append_supplemental_undo_record | src/transaction/log_manager.c | 4947 |
log_append_supplemental_serial | src/transaction/log_manager.c | 4972 |
cdc_log_extract | src/transaction/log_manager.c | 10658 |
cdc_loginfo_producer_execute | src/transaction/log_manager.c | 11064 |
cdc_check_log_page | src/transaction/log_manager.c | 11230 |
cdc_get_undo_record | src/transaction/log_manager.c | 11244 |
cdc_log_read_advance_and_preserve_if_needed | src/transaction/log_manager.c | 11306 |
cdc_get_recdes | src/transaction/log_manager.c | 11330 |
cdc_get_ovfdata_from_log | src/transaction/log_manager.c | 12184 |
cdc_get_overflow_recdes | src/transaction/log_manager.c | 12303 |
cdc_find_primary_key | src/transaction/log_manager.c | 12525 |
cdc_make_error_loginfo | src/transaction/log_manager.c | 12591 |
cdc_check_if_schema_changed | src/transaction/log_manager.c | 12688 |
cdc_get_attribute_size | src/transaction/log_manager.c | 12695 |
cdc_make_dml_loginfo | src/transaction/log_manager.c | 12818 |
cdc_make_ddl_loginfo | src/transaction/log_manager.c | 13277 |
cdc_make_dcl_loginfo | src/transaction/log_manager.c | 13381 |
cdc_make_timer_loginfo | src/transaction/log_manager.c | 13454 |
cdc_find_user | src/transaction/log_manager.c | 13533 |
cdc_compare_undoredo_dbvalue | src/transaction/log_manager.c | 13607 |
cdc_put_value_to_loginfo | src/transaction/log_manager.c | 13625 |
cdc_min_log_pageid_to_keep | src/transaction/log_manager.c | 14021 |
cdc_loginfo_producer_daemon_init | src/transaction/log_manager.c | 14030 |
cdc_daemons_init | src/transaction/log_manager.c | 14051 |
cdc_daemons_destroy | src/transaction/log_manager.c | 14066 |
cdc_pause_producer | src/transaction/log_manager.c | 14083 |
cdc_wakeup_producer | src/transaction/log_manager.c | 14098 |
cdc_kill_producer | src/transaction/log_manager.c | 14108 |
cdc_pause_consumer | src/transaction/log_manager.c | 14123 |
cdc_wakeup_consumer | src/transaction/log_manager.c | 14130 |
cdc_find_lsa | src/transaction/log_manager.c | 14137 |
cdc_check_lsa_range | src/transaction/log_manager.c | 14303 |
cdc_validate_lsa | src/transaction/log_manager.c | 14402 |
cdc_set_extraction_lsa | src/transaction/log_manager.c | 14465 |
cdc_reinitialize_queue | src/transaction/log_manager.c | 14479 |
cdc_get_start_point_from_file | src/transaction/log_manager.c | 14550 |
cdc_get_lsa_with_start_point | src/transaction/log_manager.c | 14726 |
cdc_make_loginfo | src/transaction/log_manager.c | 14835 |
cdc_initialize | src/transaction/log_manager.c | 14957 |
cdc_free_extraction_filter | src/transaction/log_manager.c | 14998 |
cdc_cleanup | src/transaction/log_manager.c | 15022 |
cdc_cleanup_consumer | src/transaction/log_manager.c | 15069 |
cdc_finalize | src/transaction/log_manager.c | 15087 |
cdc_set_configuration | src/transaction/log_manager.c | 15144 |
cdc_is_filtered_class | src/transaction/log_manager.c | 15165 |
cdc_is_filtered_user | src/transaction/log_manager.c | 15188 |
log_reader | src/transaction/log_reader.hpp | 36 |
fetch_mode | src/transaction/log_reader.hpp | 46 |
log_reader::set_lsa_and_fetch_page | src/transaction/log_reader.hpp | 162 |
log_reader::advance_when_does_not_fit | src/transaction/log_reader.hpp | 199 |
log_reader::skip | src/transaction/log_reader.hpp | 225 |
log_reader::fetch_page | src/transaction/log_reader.hpp | 269 |
LOG_READ_ALIGN | src/transaction/log_reader.hpp | 315 |
LOG_READ_ADD_ALIGN | src/transaction/log_reader.hpp | 331 |
LOG_READ_ADVANCE_WHEN_DOESNT_FIT | src/transaction/log_reader.hpp | 338 |
LOG_SUPPLEMENTAL_INFO | src/transaction/log_record.hpp | 136 |
LOG_RECORD_HEADER | src/transaction/log_record.hpp | 146 |
SUPPLEMENT_REC_TYPE | src/transaction/log_record.hpp | 418 |
LOG_REC_SUPPLEMENT | src/transaction/log_record.hpp | 434 |
log_rec_supplement | src/transaction/log_record.hpp | 435 |
LOG_REC_SUPPLEMENT | src/transaction/log_record.hpp | 435 |
LOG_GET_LOG_RECORD_HEADER | src/transaction/log_record.hpp | 441 |
Sources
Section titled “Sources”cubrid-cdc.md— the high-level companion. See alsocubrid-log-manager-detail.md(the log it reads) andcubrid-ha-replication.md(the sibling log consumer).- Raw analyses under
raw/code-analysis/cubrid/storage/cdc/. - Code:
src/api/cubrid_log.c,src/transaction/log_reader.{cpp,hpp},log_applier_sql_log.{c,h},log_manager.c. - Methodology:
knowledge/methodology/code-analysis-detail-doc.md.