Skip to content

CUBRID CDC (Change Data Capture) — Code-Level Deep Dive

Where this document fits: The high-level analysis cubrid-cdc.md covers 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:

ChTitleStatus
1Data Structure Map
2Producer Side Supplemental Record Emission
3Initialization and the Producer Consumer Daemon Pair
4Seeding and Validating the Start LSA
5The Producer Daemon Outer Loop
6Per Record Dispatch and Reconstructing One Change
7Materializing Row Images and Packing the Event Payload
8Client API Lifecycle and Wire Decoding
9Edge Paths Legacy Applier and the Shared Log Reader

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.

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.h
typedef 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.

FieldRole / Why
conncss_conn_entry, the one CDC client; consumer sends through it.
producerProducer-owned log-reader context (1.2).
consumerConsumer-owned request/response context (1.3).
loginfo_queuePointer to SPSC ring of CDC_LOGINFO_ENTRY *; reinitializable alone.
first_loginfo_queue_lsaLSA of oldest buffered entry; “buffered?” test.
last_loginfo_queue_lsaLSA of newest pushed; producer high-water mark.
is_queue_reinitializedQueue-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&lt;CDC_LOGINFO_ENTRY*&gt;"]
    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.h
typedef 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;
FieldRole / Why
next_extraction_lsaRead cursor; next record to decode (Ch 5).
all_in_condConfig: full old-image in “cond” — every column, not just key (Ch 7).
num_extraction_user / extraction_userCount + names; user filter, empty = none.
num_extraction_class / extraction_classoidsCount + class OIDs; table filter, empty = all.
statevolatile CDC_PRODUCER_STATE; publishes WAIT/RUN/DEAD.
requestvolatile CDC_PRODUCER_REQUEST; consumer pauses/kills.
produced_queue_sizeOutstanding queued bytes; bumped on push, periodically reduced by consumed_queue_size; backpressure gauge (1.10).
lockMutex over state/request hand-off.
wait_condCondvar for zero-CPU WAIT sleep.
temp_logbuf[2]Two scratch buffers (1.5); a record may straddle a page.
tran_usermap<TRANID,char*> committing user; filter / user field, cleared on abrupt end.
tran_ignoremap<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.h
typedef 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;
FieldRole / Why
extraction_timeoutWait seconds for fill; on expiry returns ..._EXTRACTION_TIMEOUT.
max_log_itemCap on items per extract.
log_infoHeap buffer of packed wire bytes; staging before send.
log_info_sizeValid bytes; send length, <= log_info_buf_size.
log_info_buf_sizeAllocated capacity; grown on overflow.
num_log_infoCount of packed entries; gated by max_log_item.
consumed_queue_sizeBytes popped since the last reconcile; subtracted from produced_queue_size then reset to 0.
requestvolatile CDC_CONSUMER_REQUEST; RUN/WAIT/NONE.
start_lsaFirst LSA wanted; from client LSA (Ch 4).
next_lsaLSA 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.h
typedef struct cdc_loginfo_entry
{
LOG_LSA next_lsa;
int length;
char *log_info;
} CDC_LOGINFO_ENTRY;
FieldRole / Why
next_lsaResume LSA after this entry; sets CDC_CONSUMER::next_lsa.
lengthByte length of log_info; copy + size accounting.
log_infoHeap 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.h
typedef struct cdc_temp_logbuf
{
LOG_PAGE *log_page_p;
char log_page[IO_MAX_PAGE_SIZE + MAX_ALIGNMENT];
} CDC_TEMP_LOGBUF;
FieldRole / Why
log_page_pLOG_PAGE * view set up via the CDC_GET_TEMP_LOGPAGE macros; aligned handle, no separate alloc.
log_pageInline 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.

Drive the handshake; transitions belong to Chapter 3.

// cdc_producer_state -- src/transaction/log_impl.h
typedef 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.h
typedef 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.h
typedef enum cdc_producer_request
{ CDC_REQUEST_PRODUCER_TO_WAIT, CDC_REQUEST_PRODUCER_TO_BE_DEAD, CDC_REQUEST_PRODUCER_NONE } CDC_PRODUCER_REQUEST;
EnumValues, in order, and meaning
CDC_PRODUCER_STATEWAIT parked on wait_cond / RUN decoding+pushing / DEAD terminated.
CDC_CONSUMER_REQUESTTO_WAIT idle / TO_RUN serve current request / NONE no pending.
CDC_PRODUCER_REQUESTTO_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.

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.h
typedef enum cdc_dataitem_type
{ CDC_DDL = 0, CDC_DML, CDC_DCL, CDC_TIMER } CDC_DATAITEM_TYPE;
// cdc_dcl_type -- src/transaction/log_impl.h
typedef enum cdc_dcl_type
{ CDC_COMMIT = 0, CDC_ABORT } CDC_DCL_TYPE;
// cdc_dml_type -- src/transaction/log_impl.h
typedef enum cdc_dml_type
{ CDC_INSERT = 0, CDC_UPDATE, CDC_DELETE, CDC_TRIGGER_INSERT, CDC_TRIGGER_UPDATE, CDC_TRIGGER_DELETE } CDC_DML_TYPE;
EnumValues and meaning
CDC_DATAITEM_TYPEDDL schema / DML row / DCL transaction boundary / TIMER heartbeat.
CDC_DCL_TYPECOMMIT / ABORT — which boundary.
CDC_DML_TYPEINSERT/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.h
typedef 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.h
typedef union cubrid_data_item CUBRID_DATA_ITEM;
union cubrid_data_item
{
DDL ddl;
DML dml;
DCL dcl;
TIMER timer;
};
FieldRole / Why
transaction_idOwning transaction id.
userCommitting user (heap string); from tran_user.
data_item_typeCDC_DATAITEM_TYPE tag; selects the live arm.
data_itemCUBRID_DATA_ITEM union; one of DDL/DML/DCL/TIMER.
nextNext item or NULL; forms the list.
CUBRID_DATA_ITEMUntagged 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.

The four union arms; server packs, Ch 8 decodes.

// ddl -- src/api/cubrid_log.h
typedef 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.h
typedef 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.h
typedef struct dcl DCL;
struct dcl
{
int dcl_type;
time_t timestamp;
};
// timer -- src/api/cubrid_log.h
typedef struct timer TIMER;
struct timer
{
time_t timestamp;
};

DDL fields:

FieldRole / Why
ddl_typeKind of schema change; DDL sub-discriminant.
object_typeTarget object class.
oidOID of the object.
classoidClass OID context.
statementHeap SQL string; replayable, freed on clear.
statement_lengthByte 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.

FieldRole / Why
dml_typeCDC_DML_TYPE; insert/update/delete + triggers.
classoidClass OID of the table.
num_changed_columnChanged-column count; length of changed triplet.
changed_column_indexint* positions of new-image columns.
changed_column_datachar** new value blobs.
changed_column_data_lenint* new value byte lengths.
num_cond_columnCond-column count; length of cond triplet.
cond_column_indexint* positions of identifying columns.
cond_column_datachar** old/identifying value blobs.
cond_column_data_lenint* cond value byte lengths.

DCL and TIMER fields:

Struct::FieldRole / Why
DCL::dcl_typeCDC_DCL_TYPE commit/abort discriminant.
DCL::timestamptime_t commit/abort time.
TIMER::timestamptime_t heartbeat tick (1.7); advances the client cursor.
// 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 */
MacroValueRole
MAX_CDC_LOGINFO_QUEUE_ENTRY2048Cap on CDC_LOGINFO_ENTRY * slots; ring capacity.
MAX_CDC_LOGINFO_QUEUE_SIZE32 MBByte 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. One server global, one client list. Server state on cdc_Gl (CDC_GLOBAL, log_impl.h); client on the CUBRID_LOG_ITEM list (cubrid_log.h). No shared struct — bytes cross via cdc_Gl.conn.
  2. The queue carries pointers, not values. loginfo_queue is lockfree::circular_queue<CDC_LOGINFO_ENTRY *>: producer allocates and enqueues the pointer, consumer copies out and frees.
  3. state vs request is the core handshake. CDC_PRODUCER::state is what-IS (producer-written), request what-is-WANTED (consumer-written), both volatile; the consumer has only a request.
  4. Two LSA windows stay consistent. cdc_Gl.first/last_loginfo_queue_lsa brackets the live queue; CDC_CONSUMER::start_lsa/next_lsa track progress, next_lsa monotonic except the ADJUSTED_LSA snap.
  5. The output triplet is self-consistent. log_info/log_info_size/log_info_buf_size + num_log_info must hold 0 <= size <= buf_size, count matching bytes, else overflow or runaway decode.
  6. Discriminated unions on both ends. CDC_DATAITEM_TYPE (+ CDC_DCL_TYPE/CDC_DML_TYPE) tags server entries; client data_item_type selects the CUBRID_DATA_ITEM arm — read the tag first.
  7. 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 via produced_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.hpp
typedef 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:

ValueEmitted byBody shape
LOG_SUPPLEMENT_TRAN_USERcommit / DDL sitedb_user string; one per txn (2.7)
LOG_SUPPLEMENT_UNDO_RECORD..._undo_recordrecdes.type + raw bytes the reader can’t re-fetch (2.5)
LOG_SUPPLEMENT_DDL..._serial; network sitepacked ints + OIDs + inline SQL (2.6)
LOG_SUPPLEMENT_INSERT..._lsaOID + REDO LSA, no prior image (2.4)
LOG_SUPPLEMENT_UPDATE..._lsaOID + UNDO + REDO LSA, both images
LOG_SUPPLEMENT_DELETE..._lsaOID + UNDO LSA, no new image
LOG_SUPPLEMENT_TRIGGER_INSERT/UPDATE/DELETEDML, trigger_involved setidentical to DML triple
LOG_SUPPLEMENT_LARGER_REC_TYPEneverrange 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.

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.hpp
typedef struct log_rec_supplement LOG_REC_SUPPLEMENT;
struct log_rec_supplement
{
SUPPLEMENT_REC_TYPE rec_type;
int length;
};
FieldRoleWhy it exists
rec_typeWhich of the eleven payloads followsSole discriminator for the reader’s decode switch (2.1)
lengthByte length of the payload after the headerLets 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.

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.

  1. One WAL type, eleven subtypes. Everything CDC reads is LOG_SUPPLEMENTAL_INFO (52); the discriminator is the inner LOG_REC_SUPPLEMENT.rec_type, with LOG_SUPPLEMENT_LARGER_REC_TYPE last as range sentinel.
  2. log_append_supplemental_info is the single chokepoint. It owns compression (MAKE_ZIP_LEN / length top-bit), RV_NOT_DEFINED allocation (recovery-inert), and two silent-drop early returns.
  3. DML records carry LSAs, not images. log_append_supplemental_lsa packs OID + undo/redo LSAs in 16-24 bytes per the INSERT/UPDATE/DELETE (and TRIGGER_*) switch, cost independent of row width.
  4. Two body encodings. DML uses raw memcpy; DDL/serial uses or_pack for its variable-length SQL; the recdes-inlining undo-record path exists for images an LSA can’t re-fetch.
  5. One user record per transaction. LOG_SUPPLEMENT_TRAN_USER is emitted eagerly at the first DDL site or lazily at commit, gated by has_supplemental_log.
  6. The whole producer side is opt-in. PRM_ID_SUPPLEMENTAL_LOG gates 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. Bootcdc_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. Sessioncdc_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.c
void 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_initialize completes (sets state = CDC_PRODUCER_STATE_DEAD, allocates loginfo_queue, aligns both temp_logbuf slots) strictly before create_daemon. The daemon’s first act is to read state/request and the queue pointer; against a NULL loginfo_queue or unaligned temp_logbuf the first is_full() check or page memcpy would crash. The ordering in cdc_daemons_init is 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 pointersMAX_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_producer does not return until state == DEAD, and destroy_daemon joins the thread, both before cdc_finalize deletes loginfo_queue and frees the tran_user strings. A cdc_finalize running 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.c
cdc_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 only producer.state. Both are volatile. 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 set state directly — 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.c
void 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.

  1. Bring-up is two-phase and ordered. cdc_daemons_init runs cdc_initialize (queue alloc, temp_logbuf alignment, zeroed state) strictly before cdc_loginfo_producer_daemon_init (mutex/condvar + the single daemon), so the daemon never observes uninitialized state.
  2. One daemon, born paused. daemon_init sets request = TO_WAIT before create_daemon, so the body sets state = RUN, immediately sees TO_WAIT, and parks on wait_cond until a consumer seeds an LSA and calls cdc_wakeup_producer.
  3. Filters are session-scoped and ownership-transferring. cdc_set_configuration stores the network-unpacked arrays and defensively re-frees any stale filter first, making reconnect after an abnormal client exit idempotent.
  4. Teardown is nested by lifetime: cdc_free_extraction_filtercdc_cleanup_consumercdc_cleanup (session: pause, drain, keep queue) ⊂ cdc_finalize (shutdown: kill, free tran_user, delete queue). Pick by whether the queue should survive.
  5. cdc_reinitialize_queue re-seeks in place via three branches — empty (no-op), in-range (front trim with byte accounting), out-of-range (delete+new) — setting is_queue_reinitialized; the trim loop frees payloads but not entry structs (flagged TODO).
  6. request and state are single-writer-per-field (producer.request consumer-only, producer.state daemon-only, both volatile), which is why pause/kill can busy-wait on state without the mutex.
  7. Pause vs kill differ by signalling. cdc_pause_producer only sets the request and polls (no-op when state is already WAIT); cdc_kill_producer signals wait_cond inside 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 whose at_time brackets 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.cpp
error_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.cpp
if (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.c
pthread_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.c
int 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):

#ConditionActionReturns
1active probe gives ER_FAILED/ER_LOG_READgoto endthat error
2arequest ≥ active start, exact match foundseed ret_lsaNO_ERROR
2brequest ≥ active start, newer than newest logseed log_Gl.append.prev_lsa, time = nowER_CDC_ADJUSTED_LSA
3request older than active start, num_arvs > 0scan archives newest→oldest (below)varies
4older than active start, num_arvs == 0, active has a timeseed oldest active LSA, adjust timeER_CDC_ADJUSTED_LSA
5num_arvs == 0, active had no time-bearing recordseed log_Gl.append.prev_lsa, time = nowER_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_timeLOG_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.c
if (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, &current_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.c
if (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.c
int 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_LSAthe 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.c
return 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.

  1. Both seed sources converge on cdc_set_extraction_lsa, which writes producer.next_extraction_lsa (under producer.lock) and consumer.next_lsa, but deliberately not consumer.start_lsa.
  2. cdc_find_lsa is 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.
  3. Only LOG_COMMIT, LOG_ABORT, LOG_DUMMY_HA_SERVER_STATE carry a usable at_time; the scan uses >= (at-or-after). The file-missing arm in cdc_get_start_point_from_file is assert-guarded.
  4. cdc_validate_lsa runs four gates — null, absurd pageid, cdc_check_lsa_range, page fetch — plus a record-boundary walk; any failure rejects the seed (mostly ER_CDC_INVALID_LOG_LSA).
  5. The readable window is the half-open interval [first_lsa, nxio_lsa); a seed below first_lsa is the “archive was removed” case.
  6. cdc_min_log_pageid_to_keep returns consumer.start_lsa.pageid (advanced only via cdc_make_loginfo), so a backward reseed must pass cdc_validate_lsa first.
  7. Version drift: the companion’s “MAX_LOG_PAGEID when no consumer” is intent; the code relies on the NULL-pageid sentinel, and the client’s SUCCESS_WITH_ADJUSTED_LSA arm is currently unreachable.

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:

FieldRole in this loopWhy 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.c
THREAD_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.c
if (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.c
if (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, in log_impl.h) accumulating tmp->length per produce (5.6); a few huge row images trip it first.
  • cdc_Gl.loginfo_queue->is_full () — a slot count on the lockfree::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.

Past both guards, the loop compares the appender’s next-output LSA to the walk:

// cdc_loginfo_producer_execute -- src/transaction/log_manager.c
nxio_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_lsanext_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.c
error = 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:

  1. ER_CDC_LOGINFO_ENTRY_GENERATED — “a payload was built into log_info_entry”. The !(... || error == ...GENERATED) test excludes it from the error branch; the only class that produces.

  2. NO_ERROR — the record advanced the walk but produced no payload; it falls past both branches.

  3. 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 the if; cdc_log_extract already advanced process_lsa past 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.c
if (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:

  • malloc the heap entry. The reused stack log_info_entry is copied onto a heap CDC_LOGINFO_ENTRY; tmp->log_info takes ownership of the extracted buffer (a pointer re-home), tmp->next_lsa records the advanced cursor.
  • malloc failure. Sets ER_OUT_OF_VIRTUAL_MEMORY and continues; log_info_entry.log_info is leaked — a known rough edge (OOM is fatal anyway).
  • is_queue_reinitialized mid-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, and continues — the one place the flag is consumed, exactly once per reset.
  • The produce. produce(tmp) enqueues the pointer; produced_queue_size += tmp->length feeds the byte-budget guard (5.3) and last_loginfo_queue_lsa records the enqueued record’s own LSA (not next_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.

When request becomes TO_BE_DEAD the while fails and control falls out:

// cdc_loginfo_producer_execute -- src/transaction/log_manager.c
cdc_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.

  1. One function, one long-lived loop. It owns the thread until request == TO_BE_DEAD; TO_WAIT is an inner park-and-continue branch, never an exit, and state must reach DEAD once.
  2. 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 reconciles produced_queue_size -= consumed_queue_size — the lone subtraction point for the byte budget.
  3. 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.
  4. The tail guard sleeps 1 s, synced to the HA heartbeat. When next_extraction_lsa is NULL or >= nxio_lsa, the loop sleeps a second, by which time the LOG_DUMMY_HA_SERVER_STATE heartbeat has advanced the tail — forward progress even with zero user writes.
  5. Two cursors under the producer lock. A NULL private process_lsa pulls from the shared next_extraction_lsa; otherwise it pushes into the shared cursor. The lock guards both against torn reads, dropped before extract.
  6. ER_CDC_LOGINFO_ENTRY_GENERATED is the produce contract. Only it triggers a heap CDC_LOGINFO_ENTRY, a produce, and the produced_queue_size/last_loginfo_queue_lsa bookkeeping; NO_ERROR and ignorable errors only advance the walk, hard errors continue without dying. After produce the entry is the consumer’s (pre-produce aborts free tmp themselves); the dead end:/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).

FieldRoleWhy it exists
prev_tranlsaLSA of the previous record of the same transactionBackward undo chain; unused by CDC
back_lsaBackward log address (previous physical record)Physical chaining; unused by CDC
forw_lsaForward log address (next physical record)Copied into next_log_rec_lsa; LSA_ISNULL is the “no more log” sentinel
tridTransaction identifier of this recordKey into tran_user / tran_ignore
typeLOG_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.

FieldRoleWhy it exists
rec_typeSUPPLEMENT_REC_TYPE discriminant (TRAN_USER, DDL, INSERT, UPDATE, DELETE, TRIGGER_*)What the inner switch dispatches on
lengthByte length of the supplement payloadSizes 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.h
if (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.

Immediately after reading the header, before the switch:

// cdc_log_extract -- src/transaction/log_manager.c
if (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.

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 malloc supplement_length + 1, copy, NUL-terminate, insert into tran_user. The only arm writing the user map from the payload directly.
  • INSERT / TRIGGER_INSERT — gate, read redo_lsa at offset sizeof(OID), fetch redo RECDES, map to CDC_INSERT / CDC_TRIGGER_INSERT.
  • UPDATE / TRIGGER_UPDATE — gate, fetch undo+redo. Special case: undo_recdes.type == REC_ASSIGN_ADDRESS is an empty OID-reservation slot, re-typed CDC_TRIGGER_INSERT (with an assert that rec_type was TRIGGER_UPDATE); else CDC_UPDATE / CDC_TRIGGER_UPDATE. Tolerates ER_CDC_IGNORE_LOG_INFO / _INTERNAL by goto end.
  • DELETE / TRIGGER_DELETE — gate, fetch only the undo RECDES (the deleted pre-image), map to CDC_DELETE / CDC_TRIGGER_DELETE.
  • DDLcdc_make_ddl_loginfo; ER_CDC_IGNORE_LOG_INFO -> end, other non-sentinel -> error:.
  • defaultbreak.

Stage 5 — the class gate (quoted once; all three DML arms are identical):

// cdc_log_extract -- src/transaction/log_manager.c
memcpy (&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.

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.c
switch (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.c
LOG_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.c
if (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.

  1. One record, one switch. cdc_log_extract dispatches on LOG_RECORD_HEADER.type; only LOG_COMMIT/LOG_ABORT (DCL), LOG_DUMMY_HA_SERVER_STATE (Timer), and LOG_SUPPLEMENTAL_INFO (DML/DDL) produce events.
  2. temp_logbuf[2] is parity-indexed so a boundary-straddling record never overwrites the page it still needs; tmpbuf_index tracks process_lsa->pageid % 2.
  3. User resolution is lazy and self-healing — read tran_user, fall back to cdc_find_user; an aborted transaction becomes a tran_ignore poison until commit/abort erases it.
  4. Two filter gatescdc_is_filtered_user and the class gate (cdc_is_filtered_class plus oid_is_system_class); an empty extraction list means “accept all”.
  5. end: advances, error: rewinds — genuine failures restore cur_log_rec_lsa so the same record is retried.
  6. Builders signal success as a negative error code, ER_CDC_LOGINFO_ENTRY_GENERATED; ER_CDC_IGNORE_LOG_INFO* means “intentionally produced nothing” and routes to end:.
  7. Payload materialization is Chapter 7 — this arm stops at the cdc_get_recdes / cdc_make_dml_loginfo calls.

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.h
struct 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 */
};
FieldRole / why
area_sizeCapacity of data; grown on S_DOESNT_FIT, always positive on CDC’s malloc path.
lengthUsed byte count; returns negative on S_DOESNT_FIT to size the retry realloc.
typeRecord type tag, set from the first INT16; needed to decode the OR record.
dataRow 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.h
typedef 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;
FieldRole / why
rec_typeHeap record-type slot; zeroed, never set on the CDC path.
dataFragment incl. its OVERFLOW_*_PART header, stripped by offset at assembly.
lengthFragment byte length; sizes recdes->data and the per-fragment copy.
nextLink 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.h
struct 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 */
};
FieldRole / why
data_lengthDecompressed length from log_unzip — the real image size.
buf_sizeCapacity of log_data, sized once to IO_MAX_PAGE_SIZE; oversize is an LZ4 failure.
log_dataThe 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.c
if (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.c
undo_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 retrylog_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.c
redo_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_CHECKlog_append_get_zip_redo thread-cached LOG_ZIPlog_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, or LOG_REDO_DATA + !is_redo), or an RVOVF_CHANGE_LINK/RVOVF_NEWPAGE_LINK, the walk seeds current_lsa from prev_tranlsa (fetching the page on a pageid change, error on fetch fail) and rewrites the link rcvindexes to RVOVF_PAGE_UPDATE since the data lives in PAGE_UPDATE records.

The walk loop branches three ways per record: (1) Boundarytrid 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.h
typedef 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 from new_values); no cond columns.
  • UPDATE / TRIGGER_UPDATE: changed set = per-column diff (below); cond columns = PK (has_pk == 1, from pk_attr_index) or, failing a PK, all old_values. Flashback forces the changed set to all columns.
  • DELETE / TRIGGER_DELETE: num_change_col = 0; cond columns = PK (or all) from old_values.

The UPDATE changed set is computed by cdc_compare_undoredo_dbvalue:

// cdc_make_dml_loginfo (UPDATE changed set) -- src/transaction/log_manager.c
for (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.c
assert (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_typeCoversPacked as
0INTEGERor_pack_int
1BIGINTor_pack_int64
2FLOATor_pack_float
3DOUBLEor_pack_double
4SHORT, and ENUMERATION stored as a short (db_get_enum_string == NULL)or_pack_short
7NULL, NUMERIC, BIT/VARBIT, CHAR/VARCHAR, all DATE/TIME, MONETARY, enum-as-string, BLOB/CLOB locator, all unsupported compositesor_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.c
if (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.c
if (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.c
return 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.c
ptr = 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.

  1. cdc_get_recdes resolves the LSAs into RECDES images — an undo block then a redo block, each a type-switch that fetches via cdc_get_undo_record, rebuilds MVCC-insert images by hand, or applies log_diff for DIFF records.
  2. Page boundaries are explicit: cdc_log_read_advance_and_preserve_if_needed preserves the page before crossing; cdc_get_recdes re-points log_page_p to it when an advance changed the pageid.
  3. Overflow rows reassemble backward then concatenate forward. cdc_get_overflow_recdes walks prev_tranlsa, prepends each OVF_PAGE_LIST fragment, strips the OVERFLOW_*_PART headers, and frees each node once; cdc_get_ovfdata_from_log does the per-call LOG_ZIP decompress.
  4. cdc_make_dml_loginfo re-orders values into def_order and splits columns into a changed set (new_values) and a cond set (old_values) — PK if one exists, else all — the SET/WHERE split.
  5. cdc_compare_undoredo_dbvalue treats 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 uses func_type 0–4 plus a catch-all 7 for NULL/string forms (5–6 unused).
  6. Schema drift is fatal to interpretation, not the stream: a cdc_check_if_schema_changed mismatch (or dropped class) routes to cdc_make_error_loginfo, a column-less but well-formed CDC_DML entry.
  7. DDL skips materializationcdc_make_ddl_loginfo repacks the inline statement text, skipping system/unfiltered classes with the non-error ER_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.

// CUBRID_LOG_STAGE -- src/api/cubrid_log.c
typedef 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 */
ValueRole / why it exists
CONFIGURATIONInitial + post-finalize; the seven setters legal only here, so filters/timeouts freeze before the server reads them once at START_SESSION.
PREPARATIONAfter connect_server; find_lsa + first extract legal. Separates “connected, no cursor” from “extracting”.
EXTRACTIONAfter first extract; extract + clear_log_item legal. Marks a live batch buffer to be cleared before the next pull.
FINALIZATIONDeclared 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.c
typedef enum { DATA_ITEM_TYPE_DDL = 0, DATA_ITEM_TYPE_DML, DATA_ITEM_TYPE_DCL, DATA_ITEM_TYPE_TIMER } DATA_ITEM_TYPE;
ValueRole / 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.h
struct cubrid_log_item { int transaction_id; char *user; int data_item_type;
CUBRID_DATA_ITEM data_item; CUBRID_LOG_ITEM *next; };
FieldRole / why it exists
transaction_idGroups items by source trid; commit boundaries arrive as DCL items.
userClient user name. or_unpack_string_nocopy pointer into g_log_infosnot owned, invalid once the blob is reused.
data_item_typeSelects the live union arm. Raw int to match the wire; cast at dispatch.
data_itemTyped payload (CUBRID_DATA_ITEM union). One struct covers all four kinds; only DML owns heap.
nextNULL-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.

SetterGlobalRange / ruleError
set_connection_timeoutg_connection_timeout0..360INVALID_CONNECTION_TIMEOUT
set_extraction_timeoutg_extraction_timeout0..360INVALID_EXTRACTION_TIMEOUT
set_max_log_itemg_max_log_item1..1024INVALID_MAX_LOG_ITEM
set_all_in_condg_all_in_cond0..1INVALID_RETRIEVE_ALL
set_extraction_tableg_extraction_table[]array/size both-null or both-setINVALID_CLASSOID_ARR_SIZE
set_extraction_userg_extraction_user[]array/size both-null or both-setINVALID_USER_ARR_SIZE
set_tracelogg_trace_log_*path writable, level 0..1, filesize 8..512 MBINVALID_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).

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_conncss_connect_to_log_servercss_receive_data (timeout g_connection_timeouts); any failure → FAILED_CONNECT. Reply must be exactly sizeof(int); reason = ntohl(*recv_data).
  • Non-WINDOWS: require reason == SERVER_CONNECTED, else FAILED_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_AVAILABLEUNAVAILABLE_CDC_SERVER (-34) with the supplemental_log hint (server is not CDC-configured); any other non-NO_ERRORFAILED_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 togetherclear_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.

// 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).

CodeMeaningProduced 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.
  1. The whole session is global state, gated by g_stageCONFIGURATION → PREPARATION → EXTRACTION (FINALIZATION vestigial), enforced per-function via INVALID_FUNC_CALL_STAGE; no handle.
  2. Setters only write globals; the network is touched once in send_configurations, so there is no mid-stream filter race.
  3. Connect is a three-step composite: db_login (throwaway db_restart enforcing DBA) → connect_server_internal (CSS handshake, Windows two-phase port handoff) → send_configurations; g_stage advances only if all succeed.
  4. Extraction is two requests: …_METADATA returns next_lsa | num_infos | total_length and sizes g_log_infos by a grow-only realloc; GET_LOGINFO fills it (skipped at length 0). The consumer owns the cursor via the in/out lsa.
  5. Decoding aliases into the blob (or_unpack_string_nocopy); only make_dml allocates three index-parallel column arrays, which clear_data_item frees as a unit.
  6. pack_func_code is 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).
  7. SUCCESS_WITH_NO_LOGITEM and SUCCESS_WITH_ADJUSTED_LSA are ABI vocabulary but never produced (dead branches), so a modifier who “fixes” them changes observable behavior; finalize always resets to CONFIGURATION even on disconnect failure and leaks g_log_infos on 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_reader shared 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 physical UNDOREDO records, 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.

FieldRoleWhy it exists
m_thread_entryCached THREAD_ENTRY*, lazily filledEncodes the single-thread assumption.
m_lsaCursor (pageid, offset)The walk state; advance/align/skip mutate it.
m_cs_accessLOG_CS_ACCESS_MODE for the fetchWhether to take the log critical section.
m_pageLive page, aligned in m_area_bufferThe decoded page the cursor reads.
m_area_bufferIO_MAX_PAGE_SIZE + DOUBLE_ALIGNMENT bytesSlack lets m_page slide to a boundary.

The constructor wires the alignment:

// log_reader::log_reader -- src/transaction/log_reader.hpp
log_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_entry is cached on first use and every mutating method assert (thread_p == &cubthread::get_entry ()). The contract is each walker owns its own instance. If two threads share one log_reader, the cached m_thread_entry belongs to whoever touched it first, m_lsa/m_page race, 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.hpp
const 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.hpp
if (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.hpp
lsa->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): round offset to DOUBLE_ALIGNMENT; the while (not if) handles a multi-page overrun, fetching and subtracting LOGAREA_SIZE per page.
  • LOG_READ_ADD_ALIGN (add_align): offset += add then LOG_READ_ALIGN. Used by reinterpret_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 single if — if offset + length >= LOGAREA_SIZE the record straddles a boundary, so pageid++, fetch (fatal on failure), offset = 0 to 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_SIZEoffset += 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.h
if (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.md describes the producer as “using the shared log reader,” that is aspirational: as of this revision the producer uses CDC_*_TEMP_LOGPAGE with its own two-page buffer. An editor wanting to adopt log_reader must first add a two-page buffering mode preserving the temp_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.c
if (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.c
else 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/DELETEla_apply_{update,insert,delete}_log; else assert_release(false)); LOG_REPLICATION_STATEMENTla_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.c
error = 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.

FieldRoleWhy it exists
db_nameSlave DB nameCatalog row identity.
creation_timeMaster creation timestampDetect a rebuilt peer.
copied_log_pathCopied-log volume pathLocate the WAL.
committed_lsaLast committed commit-log LSACommit-replay restart point.
committed_rep_lsaLast committed replication-log LSAItem-replay restart point.
append_lsa / eof_lsaMaster append / EOF snapshotsLag and end-of-record detection.
final_lsaLast processed LSAScan cursor.
required_lsaStart LSA of oldest unfinished tranRetention bound; drives archive GC.
log_record_time / log_commit_time / last_access_time / start_timeTimestampsHeartbeat / lag via applyinfo.
statusIDLE / BUSYActively-replaying flag.
insert/update/delete/schema/commit/fail_counter (INT64)Cumulative countersMetrics 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.c
memset ((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_info loads _db_ha_apply_info into la_Info at startup; la_log_commit writes it back transactionally (la_update_ha_last_applied_info then la_commit_transaction). The invariant is never advance the in-memory committed_lsa past 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.h
typedef enum { REPL_FILTER_NONE, REPL_FILTER_INCLUDE_TBL, REPL_FILTER_EXCLUDE_TBL } REPL_FILTER_TYPE;
ValueMeaningla_need_filter_out result
REPL_FILTER_NONENo filteringalways false
REPL_FILTER_INCLUDE_TBLWhitelistfilter out if the class is not in list
REPL_FILTER_EXCLUDE_TBLBlacklistfilter 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).

FieldRoleWhy it exists
next / prevList pointersPer-transaction chain, replayed in LSA order.
log_typeLOG_REPLICATION_DATA / ..._STATEMENTTop-level dispatch key.
item_typeRVREPL_DATA_* or CUBRID_STMT_*Second-level dispatch.
class_nameTarget tableFilter match, SQL generation.
db_userOwner to impersonateDDL via AU_SET_USER.
ha_sys_prmHA system parametersWrap DDL in SET SYSTEM PARAMETERS.
packed_key_value / packed_key_value_lengthDisk image of the PKLazy-unpacked into key.
keyUnpacked PK DB_VALUEUpdate/delete WHERE anchor; statement text for statements.
lsaLSA of the replication-log recordOrdering, committed_rep_lsa cursor.
target_lsaLSA of the target data-log recordLocate 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.c
if (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.c
read_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 UNDOREDO record: it must know the slave’s class layout, attribute ordering, and PK constraints at replay time (note sl_print_pk walking classobj_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. See cubrid-cdc.md for the design rationale.

  1. log_reader is the project’s forward WAL walker, but CDC keeps its own two-slot temp_logbuf[pageid % 2] cache via CDC_*_TEMP_LOGPAGE because dual-LSA reconstruction needs two pages resident — treat any “CDC uses log_reader” claim as aspirational.
  2. set_lsa_and_fetch_page does I/O only on a pageid change (or FORCE), the alignment helpers fetch on LOGAREA_SIZE overrun (fatal via logpb_fatal_error), and the class is not thread-safe by contract — each walker owns its instance, asserted against the cached m_thread_entry.
  3. Aborts free the item list and roll forward; an idle LOG_DUMMY_HA_SERVER_STATE commits a heartbeat via la_log_commit(true), while a role change returns ER_INTERRUPTED to release the lock.
  4. la_apply_log_file is 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.
  5. la_log_commit always snapshots append_lsa/eof_lsa, stamps log_commit_time only when update_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.
  6. The legacy applier is schema-fragile because it reconstructs from physical UNDOREDO records and replays SQL (owner/sysprm rewriting, optional sl_write_sql audit trail), whereas modern supplemental-record CDC ships column-named typed values and is immune to slave-side layout drift.

The following are line numbers as observed on 2026-06-23; symbols are the canonical anchor and line numbers are hints that decay.

SymbolFileLine
CUBRID_LOG_STAGEsrc/api/cubrid_log.c85
DATA_ITEM_TYPEsrc/api/cubrid_log.c93
cubrid_log_set_connection_timeoutsrc/api/cubrid_log.c158
cubrid_log_set_extraction_timeoutsrc/api/cubrid_log.c181
cubrid_log_set_tracelogsrc/api/cubrid_log.c406
cubrid_log_set_max_log_itemsrc/api/cubrid_log.c457
cubrid_log_set_all_in_condsrc/api/cubrid_log.c480
cubrid_log_set_extraction_tablesrc/api/cubrid_log.c504
cubrid_log_set_extraction_usersrc/api/cubrid_log.c535
cubrid_log_connect_server_internalsrc/api/cubrid_log.c566
cubrid_log_send_configurationssrc/api/cubrid_log.c686
cubrid_log_db_loginsrc/api/cubrid_log.c797
cubrid_log_connect_serversrc/api/cubrid_log.c851
cubrid_log_find_start_lsasrc/api/cubrid_log.c929
cubrid_log_find_start_lsasrc/api/cubrid_log.c980
cubrid_log_find_lsasrc/api/cubrid_log.c1032
cubrid_log_extract_internalsrc/api/cubrid_log.c1083
cubrid_log_make_ddlsrc/api/cubrid_log.c1243
cubrid_log_make_dmlsrc/api/cubrid_log.c1262
cubrid_log_make_dclsrc/api/cubrid_log.c1492
cubrid_log_make_timersrc/api/cubrid_log.c1507
cubrid_log_make_data_itemsrc/api/cubrid_log.c1521
cubrid_log_make_log_itemsrc/api/cubrid_log.c1576
cubrid_log_make_log_item_listsrc/api/cubrid_log.c1608
cubrid_log_extractsrc/api/cubrid_log.c1679
cubrid_log_clear_data_itemsrc/api/cubrid_log.c1736
cubrid_log_clear_log_itemsrc/api/cubrid_log.c1791
cubrid_log_disconnect_serversrc/api/cubrid_log.c1829
cubrid_log_reset_globalssrc/api/cubrid_log.c1899
cubrid_log_finalizesrc/api/cubrid_log.c1949
DDLsrc/api/cubrid_log.h71
ddlsrc/api/cubrid_log.h72
DMLsrc/api/cubrid_log.h82
dmlsrc/api/cubrid_log.h83
DCLsrc/api/cubrid_log.h97
dclsrc/api/cubrid_log.h98
TIMERsrc/api/cubrid_log.h104
timersrc/api/cubrid_log.h105
CUBRID_DATA_ITEMsrc/api/cubrid_log.h110
cubrid_data_itemsrc/api/cubrid_log.h111
CUBRID_LOG_ITEMsrc/api/cubrid_log.h119
cubrid_log_itemsrc/api/cubrid_log.h120
ER_CDC_IGNORE_LOG_INFO_INTERNALsrc/base/error_code.h1654
ER_CDC_IGNORE_TRANSACTIONsrc/base/error_code.h1655
ER_CDC_NULL_EXTRACTION_LSAsrc/base/error_code.h1659
ER_CDC_LOGINFO_ENTRY_GENERATEDsrc/base/error_code.h1660
ER_CDC_IGNORE_LOG_INFOsrc/base/error_code.h1661
scdc_find_lsasrc/communication/network_interface_sr.cpp11258
scdc_get_loginfo_metadatasrc/communication/network_interface_sr.cpp11314
recdessrc/storage/storage_common.h220
REGISTER_DAEMONsrc/thread/thread_manager.hpp498
log_Zip_min_size_to_compresssrc/transaction/log_append.cpp41
data_header_lengthsrc/transaction/log_append.cpp1290
la_repl_filtersrc/transaction/log_applier.c206
la_itemsrc/transaction/log_applier.c237
la_ha_apply_infosrc/transaction/log_applier.c394
la_init_ha_apply_infosrc/transaction/log_applier.c606
la_apply_statement_logsrc/transaction/log_applier.c5496
la_apply_repl_logsrc/transaction/log_applier.c5739
la_log_record_processsrc/transaction/log_applier.c6101
la_log_commitsrc/transaction/log_applier.c6531
la_initsrc/transaction/log_applier.c6917
la_need_filter_outsrc/transaction/log_applier.c7723
la_apply_log_filesrc/transaction/log_applier.c8074
LA_RETRY_ON_ERRORsrc/transaction/log_applier.h34
REPL_FILTER_TYPEsrc/transaction/log_applier.h48
sl_write_catalogsrc/transaction/log_applier_sql_log.c116
sl_read_catalogsrc/transaction/log_applier_sql_log.c142
sl_write_sqlsrc/transaction/log_applier_sql_log.c519
sl_log_opensrc/transaction/log_applier_sql_log.c570
MAKE_ZIP_LENsrc/transaction/log_compress.h33
log_zipsrc/transaction/log_compress.h53
CDC_GET_TEMP_LOGPAGEsrc/transaction/log_impl.h240
CDC_CHECK_TEMP_LOGPAGEsrc/transaction/log_impl.h260
CDC_UPDATE_TEMP_LOGPAGEsrc/transaction/log_impl.h271
MAX_CDC_LOGINFO_QUEUE_ENTRYsrc/transaction/log_impl.h297
MAX_CDC_LOGINFO_QUEUE_SIZEsrc/transaction/log_impl.h298
cdc_producer_statesrc/transaction/log_impl.h787
CDC_PRODUCER_STATEsrc/transaction/log_impl.h787
CDC_PRODUCER_STATEsrc/transaction/log_impl.h792
cdc_consumer_requestsrc/transaction/log_impl.h794
CDC_CONSUMER_REQUESTsrc/transaction/log_impl.h799
cdc_producer_requestsrc/transaction/log_impl.h801
CDC_PRODUCER_REQUESTsrc/transaction/log_impl.h806
cdc_loginfo_entrysrc/transaction/log_impl.h808
CDC_LOGINFO_ENTRYsrc/transaction/log_impl.h813
cdc_temp_logbufsrc/transaction/log_impl.h815
CDC_TEMP_LOGBUFsrc/transaction/log_impl.h815
CDC_TEMP_LOGBUFsrc/transaction/log_impl.h819
cdc_producersrc/transaction/log_impl.h821
CDC_PRODUCERsrc/transaction/log_impl.h821
temp_logbufsrc/transaction/log_impl.h842
CDC_PRODUCERsrc/transaction/log_impl.h848
cdc_consumersrc/transaction/log_impl.h850
CDC_CONSUMERsrc/transaction/log_impl.h850
CDC_CONSUMERsrc/transaction/log_impl.h867
cdc_globalsrc/transaction/log_impl.h869
CDC_GLOBALsrc/transaction/log_impl.h869
loginfo_queuesrc/transaction/log_impl.h877
CDC_GLOBALsrc/transaction/log_impl.h885
ovf_page_listsrc/transaction/log_impl.h888
cdc_dataitem_typesrc/transaction/log_impl.h896
CDC_DATAITEM_TYPEsrc/transaction/log_impl.h902
cdc_dcl_typesrc/transaction/log_impl.h904
CDC_DCL_TYPEsrc/transaction/log_impl.h908
cdc_dml_typesrc/transaction/log_impl.h910
CDC_DML_TYPEsrc/transaction/log_impl.h910
CDC_DML_TYPEsrc/transaction/log_impl.h918
CDC_IS_IGNORE_LOGINFO_ERRORsrc/transaction/log_manager.c167
LOG_DUMMY_HA_SERVER_STATEsrc/transaction/log_manager.c489
log_append_repl_info_and_commit_logsrc/transaction/log_manager.c4647
log_append_commit_logsrc/transaction/log_manager.c4779
log_append_supplemental_infosrc/transaction/log_manager.c4837
log_append_supplemental_lsasrc/transaction/log_manager.c4892
log_append_supplemental_undo_recordsrc/transaction/log_manager.c4947
log_append_supplemental_serialsrc/transaction/log_manager.c4972
cdc_log_extractsrc/transaction/log_manager.c10658
cdc_loginfo_producer_executesrc/transaction/log_manager.c11064
cdc_check_log_pagesrc/transaction/log_manager.c11230
cdc_get_undo_recordsrc/transaction/log_manager.c11244
cdc_log_read_advance_and_preserve_if_neededsrc/transaction/log_manager.c11306
cdc_get_recdessrc/transaction/log_manager.c11330
cdc_get_ovfdata_from_logsrc/transaction/log_manager.c12184
cdc_get_overflow_recdessrc/transaction/log_manager.c12303
cdc_find_primary_keysrc/transaction/log_manager.c12525
cdc_make_error_loginfosrc/transaction/log_manager.c12591
cdc_check_if_schema_changedsrc/transaction/log_manager.c12688
cdc_get_attribute_sizesrc/transaction/log_manager.c12695
cdc_make_dml_loginfosrc/transaction/log_manager.c12818
cdc_make_ddl_loginfosrc/transaction/log_manager.c13277
cdc_make_dcl_loginfosrc/transaction/log_manager.c13381
cdc_make_timer_loginfosrc/transaction/log_manager.c13454
cdc_find_usersrc/transaction/log_manager.c13533
cdc_compare_undoredo_dbvaluesrc/transaction/log_manager.c13607
cdc_put_value_to_loginfosrc/transaction/log_manager.c13625
cdc_min_log_pageid_to_keepsrc/transaction/log_manager.c14021
cdc_loginfo_producer_daemon_initsrc/transaction/log_manager.c14030
cdc_daemons_initsrc/transaction/log_manager.c14051
cdc_daemons_destroysrc/transaction/log_manager.c14066
cdc_pause_producersrc/transaction/log_manager.c14083
cdc_wakeup_producersrc/transaction/log_manager.c14098
cdc_kill_producersrc/transaction/log_manager.c14108
cdc_pause_consumersrc/transaction/log_manager.c14123
cdc_wakeup_consumersrc/transaction/log_manager.c14130
cdc_find_lsasrc/transaction/log_manager.c14137
cdc_check_lsa_rangesrc/transaction/log_manager.c14303
cdc_validate_lsasrc/transaction/log_manager.c14402
cdc_set_extraction_lsasrc/transaction/log_manager.c14465
cdc_reinitialize_queuesrc/transaction/log_manager.c14479
cdc_get_start_point_from_filesrc/transaction/log_manager.c14550
cdc_get_lsa_with_start_pointsrc/transaction/log_manager.c14726
cdc_make_loginfosrc/transaction/log_manager.c14835
cdc_initializesrc/transaction/log_manager.c14957
cdc_free_extraction_filtersrc/transaction/log_manager.c14998
cdc_cleanupsrc/transaction/log_manager.c15022
cdc_cleanup_consumersrc/transaction/log_manager.c15069
cdc_finalizesrc/transaction/log_manager.c15087
cdc_set_configurationsrc/transaction/log_manager.c15144
cdc_is_filtered_classsrc/transaction/log_manager.c15165
cdc_is_filtered_usersrc/transaction/log_manager.c15188
log_readersrc/transaction/log_reader.hpp36
fetch_modesrc/transaction/log_reader.hpp46
log_reader::set_lsa_and_fetch_pagesrc/transaction/log_reader.hpp162
log_reader::advance_when_does_not_fitsrc/transaction/log_reader.hpp199
log_reader::skipsrc/transaction/log_reader.hpp225
log_reader::fetch_pagesrc/transaction/log_reader.hpp269
LOG_READ_ALIGNsrc/transaction/log_reader.hpp315
LOG_READ_ADD_ALIGNsrc/transaction/log_reader.hpp331
LOG_READ_ADVANCE_WHEN_DOESNT_FITsrc/transaction/log_reader.hpp338
LOG_SUPPLEMENTAL_INFOsrc/transaction/log_record.hpp136
LOG_RECORD_HEADERsrc/transaction/log_record.hpp146
SUPPLEMENT_REC_TYPEsrc/transaction/log_record.hpp418
LOG_REC_SUPPLEMENTsrc/transaction/log_record.hpp434
log_rec_supplementsrc/transaction/log_record.hpp435
LOG_REC_SUPPLEMENTsrc/transaction/log_record.hpp435
LOG_GET_LOG_RECORD_HEADERsrc/transaction/log_record.hpp441
  • cubrid-cdc.md — the high-level companion. See also cubrid-log-manager-detail.md (the log it reads) and cubrid-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.