CUBRID HA Replication — Code-Level Deep Dive
Where this document fits: The high-level analysis
cubrid-ha-replication.mdcovers design intent and theoretical background. This document traces every branch and field at the code level. Each chapter is self-contained, but reading in order follows the full lifecycle of a single replicated change — from master log production, through copylogdb shipping, to applylogdb apply on the replica.
Contents:
Chapter 1: Data Structure Map
Section titled “Chapter 1: Data Structure Map”A replicated change is repackaged five times between a master DML statement and
a re-executed row on the slave. This chapter maps every struct and field, and
the byte-for-byte correspondence between the master staging entry and the slave
apply item. The why of the shape — emission at DML time, flush at commit,
per-transaction buffering on the slave — is argued in the high-level companion
(cubrid-ha-replication.md); here we only catalog the structures.
The five hops, each owned by one struct: (1) DML thread —
LOG_TDES.repl_records[] of LOG_REPL_RECORD; (2) on-disk log —
LOG_REC_REPLICATION + packed repl_data; (3) copylogdb — raw log pages, no
decode; (4) applylogdb decode — LA_ITEM; (5) applylogdb buffer — LA_APPLY per
transaction + the LA_COMMIT queue. Transitions: repl_log_insert (stage) →
log_append_repl_info_internal (flush) → ship → la_make_repl_item (decode) →
la_set_repl_log (buffer). Figure 1-1 maps the field-level edges.
1.1 Master staging — the LOG_TDES replication group
Section titled “1.1 Master staging — the LOG_TDES replication group”LOG_TDES (in log_impl.h) carries a realloc-grown array of staged records,
two cursors that drive it, a flush-mark index, and two LSA back-patch slots:
// log_tdes -- src/transaction/log_impl.hint num_repl_records; /* # of replication records */int cur_repl_record; /* # of replication records */int append_repl_recidx; /* index of append replication records */int fl_mark_repl_recidx; /* index of flush marked replication record at first */struct log_repl *repl_records; /* replication records */LOG_LSA repl_insert_lsa; /* insert or mvcc update target lsa */LOG_LSA repl_update_lsa; /* in-place update target lsa */// ... condensed ...int suppress_replication; /* suppress writing replication logs when flag is set */The two int fields have near-identical comments but different roles — read
them against their users, not the comments. repl_log_insert writes at
repl_records[cur_repl_record] then cur_repl_record++; the commit-time
log_append_repl_info_internal loop runs while (append_repl_recidx < cur_repl_record). So cur_repl_record is the staging write cursor and
append_repl_recidx is the flush read cursor.
| Field | Role | Why it exists |
|---|---|---|
repl_records | Heap array of LOG_REPL_RECORD, grown by repl_log_info_alloc. | Holds the transaction’s staged records, flushed to the log at commit. |
num_repl_records | Allocated capacity (slots). REPL_LOG_IS_FULL ≡ num_repl_records == cur_repl_record + 1. | Lets staging grow vs. reuse the array; bounds the realloc trigger. |
cur_repl_record | Staging write cursor / count of records staged. Next append lands at repl_records[cur_repl_record], then cur_repl_record++. | The high-water mark every staging loop walks 0 .. cur_repl_record-1. |
append_repl_recidx | Flush read cursor. -1 before any flush; set to 0 on first flush or any commit; advances to cur_repl_record. | An incremental flush resumes where it left off; commit rewinds it to 0 to re-scan all records. |
fl_mark_repl_recidx | Index where flush-marking opened (= cur_repl_record at repl_start_flush_mark); -1 if no open mark. | Lets repl_end_flush_mark rewind the suffix [fl_mark_repl_recidx, cur_repl_record) on undo (Ch 4). |
repl_insert_lsa | Target LSA of latest INSERT/MVCC-update heap record. | Back-patches the repl record’s lsa once the heap LSA is known. |
repl_update_lsa | Target LSA of latest in-place UPDATE heap record. | Same back-patch, in-place path (repl_add_update_lsa, Ch 3). |
suppress_replication | Adjacent counter; non-zero makes repl_log_insert return early. | Catalog/internal ops avoid replicating; also clears the two repl_*_lsa slots. |
Invariant — staging vs. flush cursors. During staging,
0 <= fl_mark_repl_recidx <= cur_repl_record < num_repl_records(withfl_mark_repl_recidx == -1when no mark is open), enforced by theREPL_LOG_IS_FULLrealloc check before every append inrepl_log_insert. The flush cursorappend_repl_recidxis independent:-1until the first flush, then advanced bylog_append_repl_info_internalup tocur_repl_recordand never past it (the loop guard is<). If a future edit letcur_repl_recordreachnum_repl_recordswithout reallocating, the next append would write one slot past the array and the flush loop would ship an out-of-bounds record’s garbagerepl_datato the slave.
1.2 The staging entry — LOG_REPL_RECORD
Section titled “1.2 The staging entry — LOG_REPL_RECORD”Each repl_records slot is one LOG_REPL_RECORD (struct log_repl): the
in-memory form of one change before it is logged.
// log_repl -- src/transaction/replication.hstruct log_repl{ LOG_RECTYPE repl_type; /* LOG_REPLICATION_DATA or LOG_REPLICATION_SCHEMA */ LOG_RCVINDEX rcvindex; OID inst_oid; LOG_LSA lsa; char *repl_data; /* the content of the replication log record */ int length; LOG_REPL_FLUSH must_flush; bool tde_encrypted; /* if it contains user data of tde-class */};| Field | Role | Why it exists |
|---|---|---|
repl_type | LOG_REPLICATION_DATA (row) or LOG_REPLICATION_STATEMENT (DDL). | Flush picks the log-record header and apply branch from it. |
rcvindex | Op index: RVREPL_DATA_* / RVREPL_STATEMENT (1.6). | Copied to LOG_REC_REPLICATION.rcvindex, then LA_ITEM.item_type (1.7). |
inst_oid | OID of the affected instance. | Correlates with the heap row; used to coalesce the in-place bracket. |
lsa | LSA the record refers to / target heap LSA. | Filled from the repl_*_lsa back-patch slots per rcvindex. |
repl_data | Packed payload (len + class + pkey, or DDL fields). Layout in 1.9. | Built once in repl_log_insert, shipped verbatim; NULL for STATEMENT. |
length | Byte length of repl_data. | Drives malloc, on-disk length, slave read-back. |
must_flush | A LOG_REPL_FLUSH (1.3). | COMMIT_NEED_FLUSH from staging; flush loop resets to DONT_NEED_FLUSH. |
tde_encrypted | Source class is TDE-protected. | Routes the prior node through TDE encryption (Ch 11). |
Cross-check — stale comment. The
repl_typefield comment namesLOG_REPLICATION_SCHEMA, but no suchLOG_RECTYPEexists. The real value set vialog_typeisLOG_REPLICATION_DATA/LOG_REPLICATION_STATEMENT(1.5). The comment lags a rename; the table above reflects the live enum.
1.3 LOG_REPL_FLUSH — the per-record flush policy
Section titled “1.3 LOG_REPL_FLUSH — the per-record flush policy”// log_repl_flush -- src/transaction/replication.henum log_repl_flush{ LOG_REPL_DONT_NEED_FLUSH = -1, /* no flush */ LOG_REPL_COMMIT_NEED_FLUSH = 0, /* log must be flushed at commit */ LOG_REPL_NEED_FLUSH = 1 /* log must be flushed at commit and rollback */};| Value | Role |
|---|---|
LOG_REPL_DONT_NEED_FLUSH (-1) | Skip on flush. Set by repl_log_abort_after_lsa (savepoint rollback retires records past a savepoint LSA) and by log_append_repl_info_internal after a record is appended (so a later incremental pass does not re-append it). |
LOG_REPL_COMMIT_NEED_FLUSH (0) | Default for staged rows: append only when the transaction commits. |
LOG_REPL_NEED_FLUSH (1) | Append at commit and rollback; set inside a flush-mark for records that must reach the slave regardless (Ch 4). |
The flush gate in log_append_repl_info_internal is exactly:
(is_commit && must_flush != DONT_NEED_FLUSH) || must_flush == NEED_FLUSH.
1.4 REPL_INFO_TYPE, REPL_INFO, REPL_INFO_SBR — the statement-based carrier
Section titled “1.4 REPL_INFO_TYPE, REPL_INFO, REPL_INFO_SBR — the statement-based carrier”Statement-based replication (DDL, schema, catalog changes) does not build a
LOG_REPL_RECORD from a heap change; it travels in a client-supplied carrier
tagged by REPL_INFO_TYPE:
// REPL_INFO_TYPE -- src/transaction/replication.htypedef enum{ REPL_INFO_TYPE_SBR, REPL_INFO_TYPE_RBR_START, REPL_INFO_TYPE_RBR_NORMAL, REPL_INFO_TYPE_RBR_END } REPL_INFO_TYPE;| Value | Role |
|---|---|
REPL_INFO_TYPE_SBR | Statement payload in REPL_INFO_SBR; replayed by re-executing the SQL. |
REPL_INFO_TYPE_RBR_START | First record of an in-place UPDATE bracket; maps RVREPL_DATA_UPDATE → RVREPL_DATA_UPDATE_START in repl_log_insert. |
REPL_INFO_TYPE_RBR_NORMAL | Standalone row change (INSERT/DELETE/MVCC-update); leaves rcvindex unchanged. |
REPL_INFO_TYPE_RBR_END | Closes the bracket; maps RVREPL_DATA_UPDATE → RVREPL_DATA_UPDATE_END. |
REPL_INFO is the generic wrapper; its info field downcasts to a payload such
as REPL_INFO_SBR:
// repl_info -- src/transaction/replication.hstruct repl_info { char *info; int repl_info_type; bool need_replication; };// repl_info_statement -- src/transaction/replication.hstruct repl_info_statement{ int statement_type; char *name; char *stmt_text; char *db_user; char *sys_prm_context; };REPL_INFO fields:
| Field | Role | Why it exists |
|---|---|---|
info | Downcast pointer to the payload (e.g. REPL_INFO_SBR). | One wrapper carries any info kind across the client boundary. |
repl_info_type | REPL_INFO_TYPE selecting how to read info. | Discriminator for interpreting info. |
need_replication | Gate checked before staging. | Lets the client suppress replication for a statement. |
REPL_INFO_SBR fields:
| Field | Role | Why it exists |
|---|---|---|
statement_type | The CUBRID_STMT_* kind. | Packed first, lands in LA_ITEM.item_type for filters (1.7). |
name | Affected object name. | Identifies the schema object the DDL targets. |
stmt_text | The DDL SQL to re-execute. | Slave replays the statement verbatim (into LA_ITEM.key). |
db_user | Owning DB user. | Slave re-executes under the master’s identity. |
sys_prm_context | Session parameter context. | Slave re-executes under the master’s parameters. |
1.5 The on-disk header — LOG_REC_REPLICATION
Section titled “1.5 The on-disk header — LOG_REC_REPLICATION”On flush, the log gets a fixed header followed by the repl_data bytes:
// log_rec_replication -- src/transaction/log_record.hppstruct log_rec_replication{ LOG_LSA lsa; int length; int rcvindex;};| Field | Role | Why it exists |
|---|---|---|
lsa | Target LSA (from LOG_REPL_RECORD.lsa); set NULL for DELETE/STATEMENT. | Slave orders and correlates with the data change (becomes LA_ITEM.target_lsa). |
length | Byte length of the trailing repl_data. | Slave reads exactly this many bytes back (1.9). |
rcvindex | RVREPL_DATA_* / RVREPL_STATEMENT (1.6). | Tells the slave how to decode and which apply branch. |
The parent LOG_RECTYPE is LOG_REPLICATION_DATA = 39 or
LOG_REPLICATION_STATEMENT = 40 (log_record.hpp), copied from repl_type.
1.6 RVREPL_DATA_* — the recovery-index dispatch keys
Section titled “1.6 RVREPL_DATA_* — the recovery-index dispatch keys”// LOG_RCVINDEX (replication subset) -- src/transaction/recovery.hRVREPL_DATA_INSERT = 98, RVREPL_DATA_UPDATE = 99, RVREPL_DATA_DELETE = 100,RVREPL_STATEMENT = 101, RVREPL_DATA_UPDATE_START = 102, RVREPL_DATA_UPDATE_END = 103,| Value | Role |
|---|---|
RVREPL_DATA_INSERT | Row INSERT; slave inserts the reconstructed image. |
RVREPL_DATA_UPDATE | MVCC (out-of-place) UPDATE, single record; slave updates by pkey. |
RVREPL_DATA_DELETE | Row DELETE; slave deletes by pkey (payload key-only). |
RVREPL_STATEMENT | DDL/schema; slave re-executes the SQL. |
RVREPL_DATA_UPDATE_START | Opens the in-place UPDATE bracket (before-image). |
RVREPL_DATA_UPDATE_END | Closes it (after-image); applied atomically (Ch 9). |
These six are the join key: rcvindex is set in LOG_REPL_RECORD, written to
LOG_REC_REPLICATION.rcvindex, then copied into LA_ITEM.item_type on the slave.
1.7 Slave decode — LA_ITEM
Section titled “1.7 Slave decode — LA_ITEM”la_make_repl_item unpacks one LOG_REC_REPLICATION + payload into an LA_ITEM
— the inverse of master packing.
// la_item -- src/transaction/log_applier.cstruct la_item{ LA_ITEM *next; LA_ITEM *prev; int log_type; /* LOG_REPLICATION_DATA or _STATEMENT */ int item_type; /* rcvindex (DATA) or CUBRID_STMT_* (STATEMENT) */ char *class_name; char *db_user; char *ha_sys_prm; int packed_key_value_length; char *packed_key_value; /* disk image of pkey value */ DB_VALUE key; /* it will be unpacked from packed_key_value on demand */ LOG_LSA lsa; /* the LSA of the replication log record */ LOG_LSA target_lsa; /* the LSA of the target log record */};| Field | Role | Why it exists |
|---|---|---|
next / prev | List pointers in an LA_APPLY list (1.8). | Items accumulate until the commit record arrives. |
log_type | LOG_REPLICATION_DATA or _STATEMENT. | Selects the unpack branch and apply path. |
item_type | DATA: RVREPL_DATA_* (from header rcvindex). STATEMENT: CUBRID_STMT_* (unpacked first). | The apply / statement-filter discriminator (Ch 11). |
class_name | Target class name. | Names the table; drives class-level filters. |
db_user | DB user (STATEMENT only). | Re-execute DDL as the original user. |
ha_sys_prm | Session parameters (STATEMENT only). | Re-execute DDL under the same parameters. |
packed_key_value_length | Packed pkey byte length (DATA only). | Drives malloc/memcpy. |
packed_key_value | Disk image of the pkey DB_VALUE (DATA only). | Kept packed; unpacked into key lazily. |
key | Unpacked pkey DB_VALUE (DATA); statement text via db_make_string (STATEMENT). | Slave lookup key, or the SQL to re-execute. |
lsa | LSA of this replication record. | Position bookkeeping / dedup. |
target_lsa | LSA of the data record (repl_log->lsa), set in la_new_repl_item. | Correlates the item with its data change. |
Invariant — lazy key unpack. For a DATA item,
keyis meaningless untilpacked_key_valueis unpacked; apply does this on first use underlog_type == LOG_REPLICATION_DATA && DB_IS_NULL (&item->key). Freeingpacked_key_valuebefore that point leaves apply unpacking a dangling buffer. (For a STATEMENT item the relationship inverts:keyis filled at decode time andpacked_key_valuestaysNULL.)
1.8 Slave buffering — LA_APPLY, LA_COMMIT, LA_INFO
Section titled “1.8 Slave buffering — LA_APPLY, LA_COMMIT, LA_INFO”LA_ITEMs buffer per transaction in an LA_APPLY; commits queue in LA_COMMIT;
the global LA_INFO (la_Info) owns both.
// la_apply -- src/transaction/log_applier.cstruct la_apply{ int tranid; int num_items; bool is_long_trans; LOG_LSA start_lsa; LOG_LSA last_lsa; LA_ITEM *head; LA_ITEM *tail; };// la_commit -- src/transaction/log_applier.cstruct la_commit{ LA_COMMIT *next; LA_COMMIT *prev; int type; int tranid; LOG_LSA log_lsa; time_t log_record_time; };LA_APPLY fields:
| Field | Role | Why it exists |
|---|---|---|
tranid | Master transaction id of this list. | Records fan into per-transaction lists keyed by tranid (la_find_apply_list). |
num_items | Count of buffered LA_ITEMs. | Triggers the long-transaction cutover at LA_MAX_REPL_ITEMS. |
is_long_trans | List exceeded the cap (see invariant). | Switches apply to re-fetch-from-log instead of replaying buffered items. |
start_lsa | LSA of the transaction’s first record. | Re-scan start for a long transaction. |
last_lsa | LSA of the latest record seen. | Updated even while is_long_trans. |
head / tail | Ends of the LA_ITEM list. | Append at tail, replay from head. |
LA_COMMIT fields:
| Field | Role | Why it exists |
|---|---|---|
next / prev | Link the global commit queue. | Commit records apply FIFO across transactions. |
type | Termination kind (LOG_COMMIT). | Distinguishes commit from other terminations. |
tranid | Selects the LA_APPLY to flush. | Ties the commit to its buffer. |
log_lsa | LSA of the commit record. | The durable apply bookmark advances to it (Ch 10). |
log_record_time | Master commit timestamp (eot_time). | Replication-delay reporting. |
Invariant — bounded buffering (one-way latch). A list holds at most
LA_MAX_REPL_ITEMS(1000) items. On reaching itla_set_repl_logcallsla_free_all_repl_items_except_head, setsis_long_trans = true, and thereafter only copieslast_lsa— it never appends another item. The latch is cleared only when the transaction is fully applied (is_long_trans = falsein the apply path). Without it, one large transaction could exhaust slave memory holding decoded items.
LA_INFO is the singleton (la_Info). Its replication-relevant fields:
| Field | Role | Why it exists |
|---|---|---|
repl_lists | Array of LA_APPLY * (capacity repl_cnt, cursor cur_repl). | Per-transaction buffers, searched by tranid; grown by la_init_repl_lists. |
commit_head | Head of the LA_COMMIT queue. | Commits dequeue FIFO from the head. |
commit_tail | Tail of the LA_COMMIT queue. | New commits enqueue at the tail. |
Remaining LA_INFO fields are dissected where used: the LSA bookmark group
(final_lsa, committed_lsa, required_lsa) in Ch 10, fetch/cache (act_log,
cache_pb, log_data) in Ch 7, filter/role (repl_filter, is_role_changed)
in Ch 11.
1.9 Field-level correspondence: repl_data ↔ LA_ITEM
Section titled “1.9 Field-level correspondence: repl_data ↔ LA_ITEM”The most error-prone hop is the payload layout: the master packs repl_data in
repl_log_insert, the slave unpacks it in la_make_repl_item, and they must
agree byte for byte. For a LOG_REPLICATION_DATA record:
// repl_log_insert (pack) -- src/transaction/replication.cptr_to_packed_key_value_size = ptr; ptr += OR_INT_SIZE; /* reserve len */ptr = or_pack_string_with_length (ptr, class_name, strlen);ptr = or_pack_mem_value (ptr, key_dbvalue, &packed_key_len);or_pack_int (ptr_to_packed_key_value_size, packed_key_len); /* back-fill len */
// la_make_repl_item (unpack) -- src/transaction/log_applier.cptr = or_unpack_int (area, &item->packed_key_value_length);ptr = or_unpack_string (ptr, &item->class_name);item->packed_key_value = (char *) malloc (item->packed_key_value_length);ptr = PTR_ALIGN (ptr, MAX_ALIGNMENT); /* 8 bytes alignment. see or_pack_mem_value */memcpy (item->packed_key_value, ptr, item->packed_key_value_length);item->item_type = repl_log->rcvindex;repl_data region (master) | LA_ITEM field (slave) | Notes |
|---|---|---|
leading OR_INT_SIZE int = packed_key_len | packed_key_value_length | Reserved first, back-filled after packing the value. |
or_pack_string_with_length(class_name) | class_name (via or_unpack_string) | Length-prefixed string. |
or_pack_mem_value(key_dbvalue) (8-byte aligned) | packed_key_value (PTR_ALIGN + memcpy), later → key | PTR_ALIGN to MAX_ALIGNMENT must match the master’s or_pack_mem_value padding or the pkey decodes wrong. |
(header) rcvindex | item_type | Copied from repl_log->rcvindex; the apply discriminator. |
Invariant — pack/unpack symmetry. The order (int length, class string, aligned pkey value) is hand-coded in two files with no embedded schema. The master sizes the buffer via
OR_INT_SIZE + or_packed_string_length + OR_VALUE_ALIGNED_SIZE; the slave re-derives the pkey offset withPTR_ALIGN(ptr, MAX_ALIGNMENT). A pack-order or alignment change inrepl_log_insertnot mirrored inla_make_repl_itemcorrupts every replicated row, silently.
A LOG_REPLICATION_STATEMENT record packs instead: item_type (a CUBRID_STMT_*
int), class_name, statement text (or_unpack_string → db_make_string into
key), db_user, ha_sys_prm — the unpacked images of REPL_INFO_SBR’s five
fields (1.4).
1.10 Chapter summary — key takeaways
Section titled “1.10 Chapter summary — key takeaways”- Five structures hold the change across two hosts:
LOG_TDES.repl_records[]→LOG_REC_REPLICATION+ packedrepl_data→ raw copylogdb pages →LA_ITEM→LA_APPLY/LA_COMMIT. - The master array uses two distinct cursors:
cur_repl_recordis the staging write cursor (every staging loop runs0 .. cur_repl_record-1), whileappend_repl_recidxis the flush read cursor (-1until the first flush, advanced towardcur_repl_record). Therepl_*_lsaslots back-patch a record’slsaonce the heap LSA is known. must_flush(LOG_REPL_FLUSH) decides survival: commit-only for rows (COMMIT_NEED_FLUSH), commit-and-rollback inside a flush mark (NEED_FLUSH), retired after savepoint rollback or after being appended (DONT_NEED_FLUSH).rcvindex(RVREPL_DATA_*/RVREPL_STATEMENT) is the join key: set inLOG_REPL_RECORD, written toLOG_REC_REPLICATION, copied intoLA_ITEM.item_type, switched on by every apply branch.- The slave decodes lazily (
LA_ITEM.keyunpacked only at apply time), andis_long_transis a one-way latch: pastLA_MAX_REPL_ITEMS(1000) the list is freed to its head and the transaction replays by re-scanningstart_lsa..last_lsafrom the log. - The
repl_datalayout (int length, class string, 8-byte-aligned pkey) is hand-coded symmetrically inrepl_log_insert/la_make_repl_item; with no self-describing schema, any unmirrored pack-order or alignment change corrupts rows silently.
Chapter 2: Master Side Initialization and the Staging Array
Section titled “Chapter 2: Master Side Initialization and the Staging Array”Before any DML can ship a row image to a replica, the master must stage the per-transaction stream of replication records somewhere — the tdes->repl_records[] array on every transaction descriptor. This chapter traces that memory’s lifecycle (born lazily on the first write, grown in 100-slot chunks, reset across transaction boundaries), how suppress_replication short-circuits it, and the invariant that the master writes nothing replication-related into the WAL until commit (Chapter 5). Assumes the staging-array overview and master/replica roles from the high-level companion (cubrid-ha-replication.md, “Master-side log generation”); we trace how the array is managed, not why row-based replication stages per transaction.
2.1 The staging fields and where they live
Section titled “2.1 The staging fields and where they live”The staging array is described by four integer fields and one pointer on LOG_TDES (log_impl.h), introduced in Chapter 1’s data-structure map. The slot payload is a LOG_REPL_RECORD (struct log_repl, replication.h); here we focus on the descriptor-side cursor semantics that govern allocation and reset.
| Field | Role | Why it exists |
|---|---|---|
repl_records | Pointer to the heap LOG_REPL_RECORD array (or NULL) | The staging buffer; NULL means no write has happened yet on this (fresh or freshly-freed) descriptor |
num_repl_records | Allocated capacity (slot count) | Separates “how big” from “how full”; drives the grow decision |
cur_repl_record | Slots in use — also the write cursor | repl_log_insert writes at repl_records[cur_repl_record], then post-increments |
append_repl_recidx | Cursor for the commit-time emit pass into the WAL | Lets emit resume mid-array; -1 = emit never ran this transaction |
fl_mark_repl_recidx | Index where statement flush marking began, or -1 | Drives the must_flush upgrade logic (Chapter 4); -1 = no flush mark |
Three of these are index cursors — cur_repl_record (fill), append_repl_recidx (emit), fl_mark_repl_recidx (flush mark); repl_records is the buffer pointer and num_repl_records the capacity. The array splits at cur_repl_record: slots [0 .. cur-1] used, [cur .. num-1] free. The gap to num_repl_records is the free space the next insert consumes; when it closes the array is “full” and must grow.
Invariant (fill cursor never exceeds capacity).
0 <= cur_repl_record <= num_repl_recordsholds at all times, enforced structurally:repl_log_insertcallsrepl_log_info_allocbefore writing whenever the array is full, so a slot is always present atrepl_records[cur_repl_record]before the post-increment. Violate it and the write&tdes->repl_records[tdes->cur_repl_record]indexes past the allocation, corrupting the heap. The “full” test (Section 2.3) fires one slot early precisely to keep this safe.
2.2 Birth and reset: the staging fields across transaction boundaries
Section titled “2.2 Birth and reset: the staging fields across transaction boundaries”The fields have three lifecycle events in three functions. The distinction that matters: which zeroes the pointer versus which merely rewinds the cursors — that is what lets the buffer survive across transactions on a reused descriptor.
(a) Descriptor creation — logtb_initialize_tdes (log_tran_table.c). Run once when a LOG_TDES slot is set up — the only place repl_records is nulled and capacity zeroed:
// logtb_initialize_tdes -- src/transaction/log_tran_table.ctdes->num_repl_records = 0;tdes->cur_repl_record = 0;tdes->append_repl_recidx = -1;tdes->fl_mark_repl_recidx = -1;tdes->repl_records = NULL;LSA_SET_NULL (&tdes->repl_insert_lsa);LSA_SET_NULL (&tdes->repl_update_lsa);// ... first_save_entry / suppress_replication = 0 ...(b) Transaction end / reuse — logtb_clear_tdes (log_tran_table.c). Run at every commit or abort to recycle the descriptor. It frees each record’s payload but keeps the array allocation for the next transaction — it does not touch repl_records or num_repl_records:
// logtb_clear_tdes -- src/transaction/log_tran_table.cfor (i = 0; i < tdes->cur_repl_record; i++) { if (tdes->repl_records[i].repl_data) { free_and_init (tdes->repl_records[i].repl_data); } }// ... bind_history cleanup condensed ...tdes->cur_repl_record = 0;tdes->append_repl_recidx = -1;tdes->fl_mark_repl_recidx = -1;LSA_SET_NULL (&tdes->repl_insert_lsa);LSA_SET_NULL (&tdes->repl_update_lsa);// ... later: tdes->suppress_replication = 0 ...Because pointer and capacity persist, a transaction that grew the array to 300 slots leaves them warm: REPL_LOG_IS_NOT_EXISTS (Section 2.3) reads num_repl_records != 0 and the lazy first-allocation is skipped. The freed slots’ repl_data pointers are re-nulled only for newly grown slots by repl_log_info_alloc (Section 2.4); the slots below cur_repl_record that a prior transaction filled retain dangling repl_data pointers until overwritten — but cur_repl_record was rewound to 0 here, so no read sees them before repl_log_insert overwrites the field.
(c) Descriptor teardown — logtb_free_tran_index (log_tran_table.c). Called when the transaction index is released — the only place the array memory returns to the heap. Note it calls logtb_clear_tdes first (freeing payloads), then frees the array shell:
// logtb_free_tran_index -- src/transaction/log_tran_table.clogtb_clear_tdes (thread_p, tdes);if (tdes->repl_records) { free_and_init (tdes->repl_records); }tdes->num_repl_records = 0;Invariant (capacity and pointer move together).
repl_records == NULLiffnum_repl_records == 0.logtb_initialize_tdes,logtb_free_tran_index, andrepl_log_info_alloc(on success) all set both consistently;logtb_clear_tdestouches neither. Null the pointer without zeroing capacity (or vice versa) andREPL_LOG_IS_NOT_EXISTSwould misclassify the buffer, leaking the allocation or dereferencingNULL.
A fourth event — append_repl_recidx reset to 0 — happens not here but at commit-time emit in log_append_repl_info_internal (log_manager.c), where if (tdes->append_repl_recidx == -1 || is_commit) rewinds the emit cursor (Chapter 5). Shown only to complete the lifecycle (Figure 2-1).
stateDiagram-v2 [*] --> Uninit: logtb_initialize_tdes Uninit --> Allocated: first repl_log_insert -> malloc Allocated --> Allocated: array full -> realloc grow Allocated --> Emitted: commit -> log_append_repl_info_internal Emitted --> Recycled: logtb_clear_tdes rewinds cursors keeps buffer Recycled --> Allocated: next txn reuses warm buffer Recycled --> Freed: logtb_free_tran_index frees buffer Uninit --> Freed: logtb_free_tran_index Freed --> [*]
Figure 2-1. Staging-array lifecycle across descriptor reuse. The Recycled -> Allocated edge is why most transactions skip malloc.
2.3 The two macro guards: NOT_EXISTS and FULL
Section titled “2.3 The two macro guards: NOT_EXISTS and FULL”Two macros classify the array’s state at every insert, reading the descriptor through the global trantable by tran_index:
// REPL_LOG_IS_NOT_EXISTS / REPL_LOG_IS_FULL -- src/transaction/replication.c#define REPL_LOG_IS_NOT_EXISTS(tran_index) \ (log_Gl.trantable.all_tdes[(tran_index)]->num_repl_records == 0)#define REPL_LOG_IS_FULL(tran_index) \ (log_Gl.trantable.all_tdes[(tran_index)]->num_repl_records \ == log_Gl.trantable.all_tdes[(tran_index)]->cur_repl_record+1)
static const int REPL_LOG_INFO_ALLOC_SIZE = 100;REPL_LOG_IS_NOT_EXISTS is true when capacity is zero — no buffer ever allocated for this (fresh or freshly-freed) descriptor — and triggers the lazy initial malloc. REPL_LOG_IS_FULL is true when the fill cursor is one short of capacity (num_repl_records == cur_repl_record+1); the +1 is the slot-early test from Section 2.1, guaranteeing a free slot for the insert about to happen. The two are mutually exclusive — a zero-capacity array cannot be “full” — so repl_log_insert tests them with if / else if.
2.4 repl_log_info_alloc — every branch
Section titled “2.4 repl_log_info_alloc — every branch”repl_log_info_alloc is the only function that touches repl_records and num_repl_records together. Its need_realloc parameter selects initial malloc vs grow-by-realloc.
// repl_log_info_alloc -- src/transaction/replication.cstatic intrepl_log_info_alloc (LOG_TDES * tdes, int arr_size, bool need_realloc){ int i = 0, k; int error = NO_ERROR;
if (need_realloc == false) { i = arr_size * DB_SIZEOF (LOG_REPL_RECORD); tdes->repl_records = (LOG_REPL_RECORD *) malloc (i); if (tdes->repl_records == NULL) { error = ER_REPL_ERROR; er_set (ER_WARNING_SEVERITY, ARG_FILE_LINE, ER_REPL_ERROR, 1, "can't allocate memory"); return error; } tdes->num_repl_records = arr_size; k = 0; } else { i = tdes->num_repl_records + arr_size; tdes->repl_records = (LOG_REPL_RECORD *) realloc (tdes->repl_records, i * DB_SIZEOF (LOG_REPL_RECORD)); // ... same NULL check / ER_REPL_ERROR / er_set / return as the malloc path ... k = tdes->num_repl_records; tdes->num_repl_records = i; }
for (i = k; i < tdes->num_repl_records; i++) { tdes->repl_records[i].repl_data = NULL; }
return error;}Four outcomes cover every branch. A (need_realloc == false): malloc; on success capacity = arr_size, k = 0. A1 (malloc fails): set ER_REPL_ERROR and return — num_repl_records stays 0, pointer stays NULL, so NOT_EXISTS still holds. B (need_realloc == true): grow capacity to old + arr_size, k = old capacity. B1 (realloc fails): set ER_REPL_ERROR and return; the old block leaks (see below). Both success paths run the tail loop for (i = k; ...) repl_records[i].repl_data = NULL;. The k variable is the crux: 0 for a fresh malloc initializes everything; the old capacity for a realloc initializes only the grown tail, leaving staged records untouched. Nulling only repl_data suffices because repl_log_insert fills every other field (repl_type, rcvindex, inst_oid, lsa, length, must_flush, tde_encrypted) before it is read, and repl_data’s NULL-ness is what guards the free in logtb_clear_tdes.
Invariant (every slot’s
repl_datais a valid heap pointer orNULL). Nulled here at allocation, set to amalloc’d buffer orNULLinrepl_log_insert, and freed inlogtb_clear_tdesunderif (repl_records[i].repl_data)— an uninitialized grown slot would make that free hit garbage. The tail loop upholds it for grown slots, the per-insert assignment for filled ones. Caveat: branch B1 is the classicp = realloc(p, …)anti-pattern — on failure the original block survives but its only pointer is overwritten withNULL, leaking it. Benign here (OOM precedes abort and process cleanup) but do not preserve it.
2.5 repl_log_insert — the allocation branches and the suppress short-circuit
Section titled “2.5 repl_log_insert — the allocation branches and the suppress short-circuit”repl_log_insert is dissected for emission in Chapter 3; here, only its entry guards — the suppress short-circuit and the allocation branches preceding any record write.
// repl_log_insert -- src/transaction/replication.ctran_index = LOG_FIND_THREAD_TRAN_INDEX (thread_p);tdes = LOG_FIND_TDES (tran_index);if (tdes == NULL) { return ER_FAILED; }
/* If suppress_replication flag is set, do not write replication log. */if (tdes->suppress_replication != 0) { /* clear repl lsa in tdes since no replication log will be written */ LSA_SET_NULL (&tdes->repl_insert_lsa); LSA_SET_NULL (&tdes->repl_update_lsa);
return NO_ERROR; }
if (thread_p->no_logging && tdes->fl_mark_repl_recidx == -1) { return NO_ERROR; }
/* check the replication log array status, if we need to alloc? */if (REPL_LOG_IS_NOT_EXISTS (tran_index) && ((error = repl_log_info_alloc (tdes, REPL_LOG_INFO_ALLOC_SIZE, false)) != NO_ERROR)) { return error; }/* the replication log array is full? re-alloc? */else if (REPL_LOG_IS_FULL (tran_index) && (error = repl_log_info_alloc (tdes, REPL_LOG_INFO_ALLOC_SIZE, true)) != NO_ERROR) { return error; }
repl_rec = (LOG_REPL_RECORD *) (&tdes->repl_records[tdes->cur_repl_record]);// ... record fill (Chapter 3) ...Figure 2-2 traces every branch. The key non-obvious point: the two allocation guards fire at most one repl_log_info_alloc because the macros are mutually exclusive (Section 2.3) and && invokes the allocator only when its guard holds — G3/G4 propagate failure, the non-allocating cases fall through, and a buffer that already has room skips both. The same allocation prologue appears verbatim in repl_log_insert_statement (Chapter 4); only repl_log_insert carries the no_logging early-out at G2.
flowchart TD
A["repl_log_insert entry"] --> B{"tdes == NULL?"}
B -- yes --> R0["return ER_FAILED (G0)"]
B -- no --> C{"suppress_replication != 0?"}
C -- yes --> S["NULL both repl LSAs<br/>return NO_ERROR (G1)"]
C -- no --> D{"no_logging AND no flush mark?"}
D -- yes --> R2["return NO_ERROR (G2)"]
D -- no --> E{"NOT_EXISTS?"}
E -- yes --> F["alloc(malloc)"]
F --> F2{"error?"}
F2 -- yes --> R3["return error (G3)"]
F2 -- no --> J["slot = repl_records[cur_repl_record]"]
E -- no --> G{"IS_FULL?"}
G -- yes --> H["alloc(realloc grow)"]
H --> H2{"error?"}
H2 -- yes --> R4["return error (G4)"]
H2 -- no --> J
G -- no --> J
J --> K["fill record (Chapter 3)"]
Figure 2-2. Entry guards of repl_log_insert; both allocation branches converge on the same safe slot access.
Invariant (suppress nulls the staging LSAs, never the array). When
suppress_replication != 0,repl_log_insertclearsrepl_insert_lsa/repl_update_lsabut leavesrepl_recordsand the cursors alone. Suppression is a per-operation gate a transaction may toggle, so nulling the LSAs stops a later non-suppressed insert in the same transaction from inheriting a stalerepl_insert_lsaand stamping a record with the wrong WAL address (Chapter 3: the insert path copiesrepl_insert_lsaintorepl_rec->lsa). The same flag governsrepl_log_insert_statement(Chapter 4) andrepl_add_update_lsa— the latter returns immediately under suppression without nulling the LSAs, since there is no staged update record to back-patch.
2.6 The commit-only WAL invariant
Section titled “2.6 The commit-only WAL invariant”Invariant (no LOG_REPLICATION_DATA in the WAL until commit). Everything here writes to process-private heap memory (
tdes->repl_records[]), never to the log buffer or disk. Staged records become real replication WAL records only at commit, whenlog_append_repl_info_internalwalks the array fromappend_repl_recidx(Chapter 5); enforcement is structural — no call site appends one outside that path. An aborting transaction simply has its staging payloads freed inlogtb_clear_tdes, so its row images never reached the WAL and can never reach a replica. Replication is thus atomic with commit by construction; an eager DML-side append would leak phantom changes on abort, silently diverging the replica.
2.7 Chapter summary — key takeaways
Section titled “2.7 Chapter summary — key takeaways”- Lazily allocated, warm-reused.
repl_recordsisNULLuntil the first write triggersrepl_log_info_alloc(..., false);logtb_clear_tdesrewinds cursors but keeps the buffer, so later transactions on the descriptor skip themalloc. - Three functions own three lifecycle events.
logtb_initialize_tdesnulls the pointer and zeroes capacity;logtb_clear_tdesfrees payloads and rewinds the cursors while keeping the allocation;logtb_free_tran_indexalone frees the array shell (after callinglogtb_clear_tdes). - Capacity and fill are separate.
num_repl_recordsis capacity,cur_repl_recordthe fill cursor; the “full” macro fires one slot early (num == cur+1) to guarantee a slot for the in-flight insert. repl_log_info_allocgrows by 100, initializes only new slots. Thekvariable bounds therepl_data = NULLtail loop so staged records survive a grow; the realloc-on-OOM path leaks the old block.suppress_replicationshort-circuits before any allocation, nulling the staging LSAs so a later non-suppressed insert cannot inherit a stale LSA.- Nothing reaches the WAL until commit — staging is private heap memory, converted to WAL records only by the commit-time emit pass (Chapter 5), making replication atomic with commit.
Chapter 3: Emitting a Replication Record During DML
Section titled “Chapter 3: Emitting a Replication Record During DML”This chapter answers one question: when a row is inserted, updated, or
deleted on the master, exactly which call path stages a
LOG_REPL_RECORD, and how does the staged record differ per operation
kind? The high-level companion (cubrid-ha-replication.md,
“Row-based replication” section) explains why CUBRID replicates the
primary key rather than the physical row image. Here we trace how the
key is captured, where the back-patch happens for UPDATE, and every
branch the emission code takes.
The staging array (tdes->repl_records[]) and LOG_REPL_RECORD were
mapped in Chapter 1; Chapter 2 covered allocation. We trace the writers
into the array, not the array itself.
3.1 The emission topology — one funnel, three call paths
Section titled “3.1 The emission topology — one funnel, three call paths”All server-side DML arrives at locator_attribute_info_force, which
rebuilds the record image from the attribute-info copyarea, switches on
the flush operation, and dispatches to locator_insert_force,
locator_update_force (carrying the caller-supplied repl_info), or
locator_delete_force — the public wrapper that forwards to
locator_delete_force_internal; an unknown operation raises
ER_LC_BADFORCE_OPERATION.
The key architectural fact: the replication record is staged at the
index layer, not the heap layer. All three force routines call
locator_add_or_remove_index / locator_update_index, and those call
repl_log_insert — the one place the primary-key DB_VALUE is already
materialized for the B-tree op, so capturing it costs nothing extra.
flowchart TB AIF["locator_attribute_info_force"] AIF --> INS["locator_insert_force"] --> HIL["heap_insert_logical"] AIF --> UPD["locator_update_force"] AIF --> DEL["locator_delete_force"] --> DELI["locator_delete_force_internal"] --> HDL["heap_delete_logical"] INS --> ARI["locator_add_or_remove_index\n(is_insert true/false)"] DELI --> ARI UPD --> LUI["locator_update_index"] UPD --> HUL["heap_update_logical"] UPD --> RAU["repl_add_update_lsa\n(back-patch lsa)"] ARI -->|"PRIMARY_KEY, INSERT or DELETE"| RLI["repl_log_insert\n(stages LOG_REPL_RECORD)"] LUI -->|"PRIMARY_KEY, UPDATE"| RLI
Figure 3-1 — The emission funnel. INSERT and DELETE stage inside
locator_add_or_remove_index; UPDATE stages inside locator_update_index
then back-patches the LSA after heap_update_logical via
repl_add_update_lsa.
3.2 The three force routines — emission-relevant branches
Section titled “3.2 The three force routines — emission-relevant branches”The force routines do not call repl_log_insert themselves; they
establish the heap log and the need_replication state that the index
pass depends on. INSERT and DELETE run the heap pass before the index
pass (so the heap LSA is in hand when staging); UPDATE inverts the order —
index pass first, heap pass second — which is what forces the back-patch.
The branches that matter to replication, across all three:
| Routine / arm | Effect on replication |
|---|---|
insert: partition_prune_insert fail to goto error2 | nothing staged |
insert: heap_insert_logical ok | its RVHF_INSERT/RVHF_MVCC_INSERT heap log makes the log-append path stamp repl_insert_lsa; on error no LSA, no index pass |
insert: has_index && add_or_remove_index (... true ...) | is_insert = true index pass stages INSERT record |
delete: isold_object == false | OID_SET_NULL; instance block skipped — nothing staged |
delete: isold_object && OID_IS_ROOTOID | class delete — catalog path, not the instance index pass |
delete: heap_delete_logical ok | appends heap-delete log, advancing tdes->tail_lsa; the MVCC-disabled skip writes no heap log so tail_lsa is unchanged (3.4 fallback to prior_lsa); on error nothing staged |
delete: idx_action_flag == FOR_INSERT_OR_DELETE to add_or_remove_index (... false ...) | is_insert = false index pass stages DELETE record |
delete: else to ..._index_for_moving (... false ...) | partition-move — deletes index entry without staging |
update: locator_update_index (earlier in body) | stages UPDATE record with NULL lsa; sets/clears repl_info.need_replication |
update: heap_update_logical ok | its RVHF_UPDATE-family heap log makes the log-append path stamp repl_update_lsa; on error the NULL-lsa record is never patched |
update: !LOG_CHECK_LOG_APPLIER && log_does_allow_replication () && repl_info.need_replication | guard gating repl_add_update_lsa (thread_p, oid) |
3.3 INSERT and DELETE — staging inside locator_add_or_remove_index
Section titled “3.3 INSERT and DELETE — staging inside locator_add_or_remove_index”Both locator_insert_force and locator_delete_force_internal land in
locator_add_or_remove_index_internal, which loops over every B-tree of
the class, does the physical index op, and — only for the primary
key — stages the replication record:
// locator_add_or_remove_index_internal -- src/transaction/locator_sr.c if (need_replication && index->type == BTREE_PRIMARY_KEY && error_code == NO_ERROR && !LOG_CHECK_LOG_APPLIER (thread_p) && log_does_allow_replication () == true) { error_code = repl_log_insert (thread_p, class_oid, inst_oid, datayn ? LOG_REPLICATION_DATA : LOG_REPLICATION_STATEMENT, is_insert ? RVREPL_DATA_INSERT : RVREPL_DATA_DELETE, /* <- op kind */ key_dbvalue, REPL_INFO_TYPE_RBR_NORMAL); /* <- always NORMAL here */ }Four guards must all hold: need_replication false means the class is
non-replicated (e.g. no PK); a non-BTREE_PRIMARY_KEY index is handled by
another loop iteration; error_code != NO_ERROR means the B-tree op
failed (do not stage a failed change); and the LOG_CHECK_LOG_APPLIER /
log_does_allow_replication () pair short-circuits when we are the applier
replaying or replication is globally off. INSERT and DELETE hard-code
REPL_INFO_TYPE_RBR_NORMAL (single-row ops are never START/END bracketed —
Section 3.5); only rcvindex differs (RVREPL_DATA_INSERT vs
RVREPL_DATA_DELETE).
Invariant — a row replication record is staged exactly once per row, against the primary-key index only. Enforced by the
index->type == BTREE_PRIMARY_KEYguard in the per-index loop. Drop it and a table with N indexes stages N copies; a PK-less table stages nothing (need_replication/PKshort-circuit), so it is silently non-replicated.
3.4 repl_log_insert — the staging core, branch by branch
Section titled “3.4 repl_log_insert — the staging core, branch by branch”repl_log_insert is the single writer into tdes->repl_records[]. Its
signature carries the op kind (rcvindex) and the multi-update
refinement (repl_info). It proceeds through five phases; Figure 3-2
traces every branch.
flowchart TB A["tdes == NULL?"] -->|yes| RET1["return ER_FAILED"] A -->|no| B["suppress_replication?"] B -->|yes| C["null both pending LSAs; return"] B -->|no| D["no_logging AND no flush-mark?"] D -->|yes| E["return NO_ERROR"] D -->|no| F["array missing/full -> repl_log_info_alloc"] F -->|fail| RET2["return error"] F -->|ok| G["P3: rcvindex refine\nP4: payload + tde\nP5: lsa switch; cur_repl_record++"] G --> Q["flush-mark open? upgrade must_flush"]
Figure 3-2 — repl_log_insert control flow, every early return shown.
Phase 1–2 — early returns and array growth. Three short-circuits
never stage: tdes == NULL; suppress_replication != 0 (Chapter 11 —
also nulls both pending LSAs so a later record cannot back-patch onto a
suppressed change); and no_logging && fl_mark_repl_recidx == -1. Then
REPL_LOG_IS_NOT_EXISTS / REPL_LOG_IS_FULL gate repl_log_info_alloc
(Chapter 2); on alloc failure it returns before touching
cur_repl_record, preserving cur_repl_record <= capacity.
Phase 3 — rcvindex refinement. Where the op kind is finalized:
// repl_log_insert -- src/transaction/replication.c repl_rec->rcvindex = rcvindex; if (rcvindex == RVREPL_DATA_UPDATE) /* INSERT/DELETE skip this block */ switch (repl_info) { case REPL_INFO_TYPE_RBR_START: repl_rec->rcvindex = RVREPL_DATA_UPDATE_START; break; case REPL_INFO_TYPE_RBR_END: repl_rec->rcvindex = RVREPL_DATA_UPDATE_END; break; default: break; /* NORMAL -> stays RVREPL_DATA_UPDATE */ }Because the outer if only fires for RVREPL_DATA_UPDATE, Section 3.3’s
REPL_INFO_TYPE_RBR_NORMAL argument is ignored for INSERT/DELETE.
Phase 4 — payload and tde_encrypted. Packed only for
LOG_REPLICATION_DATA (the datayn argument); a
LOG_REPLICATION_STATEMENT record gets repl_data = NULL, length = 0:
// repl_log_insert -- src/transaction/replication.c if (log_type == LOG_REPLICATION_DATA) { // ... condensed: heap_get_class_name / heap_get_class_tde_algorithm -> ER_REPL_ERROR on failure ... repl_rec->tde_encrypted = tde_algo != TDE_ALGORITHM_NONE; /* <- per-class TDE flag */ repl_rec->length = OR_INT_SIZE + or_packed_string_length (class_name, &strlen) + OR_VALUE_ALIGNED_SIZE (key_dbvalue); ptr = (char *) malloc (repl_rec->length); repl_rec->repl_data = ptr; /* NULL -> ER_REPL_ERROR */ ptr_to_packed_key_value_size = ptr; ptr += OR_INT_SIZE; /* reserve 4 bytes, fill last */ ptr = or_pack_string_with_length (ptr, class_name, strlen); /* class name */ ptr = or_pack_mem_value (ptr, key_dbvalue, &packed_key_len); /* primary key DB_VALUE */ or_pack_int (ptr_to_packed_key_value_size, packed_key_len); /* back-fill size header */ } else { repl_rec->repl_data = NULL; repl_rec->length = 0; }tde_encrypted is reset false at the top and only raised here, so
STATEMENT and non-TDE records stay false.
Invariant — the 4-byte size header equals the byte length of the packed key that follows it. Written last, after
or_pack_mem_valuereportspacked_key_len. Fill it early with theOR_VALUE_ALIGNED_SIZEupper bound and the slave’s unpack reads past the key, corrupting the row.
Phase 5 — the asymmetric LSA switch. After
must_flush = LOG_REPL_COMMIT_NEED_FLUSH, the function picks
repl_rec->lsa by op kind:
// repl_log_insert -- src/transaction/replication.c switch (rcvindex) { case RVREPL_DATA_INSERT: /* heap_insert already ran */ if (!LSA_ISNULL (&tdes->repl_insert_lsa)) { LSA_COPY (&repl_rec->lsa, &tdes->repl_insert_lsa); LSA_SET_NULL (&tdes->repl_insert_lsa); LSA_SET_NULL (&tdes->repl_update_lsa); } break; case RVREPL_DATA_UPDATE: LSA_SET_NULL (&repl_rec->lsa); break; /* <- heap update not done yet; back-patch later */ case RVREPL_DATA_DELETE: /* tail_lsa if set, else next prior_lsa */ LSA_COPY (&repl_rec->lsa, LSA_ISNULL (&tdes->tail_lsa) ? &log_Gl.prior_info.prior_lsa : &tdes->tail_lsa); break; default: break; } tdes->cur_repl_record++;INSERT copies repl_insert_lsa and nulls both pending LSAs; UPDATE
stores NULL because the heap update runs after the index pass and
will back-patch via repl_add_update_lsa; DELETE uses its own tail_lsa,
falling back to the upcoming prior_lsa when the MVCC-disabled skip
(3.2) left tail_lsa unchanged. This index-before-heap inversion for
UPDATE — documented in the repl_add_update_lsa header comment — is why
UPDATE alone needs a back-patch.
3.5 The flush-mark tail and multi-update bracketing
Section titled “3.5 The flush-mark tail and multi-update bracketing”After cur_repl_record++, a final block runs only when a flush-mark
bracket is open (fl_mark_repl_recidx != -1; opened by
repl_start_flush_mark, Chapter 4):
// repl_log_insert -- src/transaction/replication.c if (tdes->fl_mark_repl_recidx != -1) { if (strcmp (class_name, CT_SERIAL_NAME) != 0) { for (i = 0; i < tdes->fl_mark_repl_recidx; i++) /* same instance already staged? */ if (recsp[i].must_flush == LOG_REPL_COMMIT_NEED_FLUSH && OID_EQ (&recsp[i].inst_oid, &repl_rec->inst_oid)) break; /* -> do NOT upgrade */ if (i >= tdes->fl_mark_repl_recidx) repl_rec->must_flush = LOG_REPL_NEED_FLUSH; /* <- flush at commit AND rollback */ } else repl_rec->must_flush = LOG_REPL_NEED_FLUSH; /* serial changes always upgrade */ }Default must_flush is LOG_REPL_COMMIT_NEED_FLUSH (commit only). Inside
an open bracket it upgrades to LOG_REPL_NEED_FLUSH (commit and
rollback) — unless an earlier bracket record targets the same inst_oid
(dedup to avoid double-flush). CT_SERIAL_NAME (_db_serial) is exempt:
serial changes survive rollback, so they always upgrade. repl_log_insert
applies the mark; Chapter 4 covers why marks exist.
3.6 UPDATE — staging in locator_update_index, then back-patch
Section titled “3.6 UPDATE — staging in locator_update_index, then back-patch”locator_force_for_multi_update computes the refinement
(REPL_INFO_TYPE_RBR_START for i == first_update_obj,
REPL_INFO_TYPE_RBR_END for i == last_update_obj, else NORMAL).
locator_update_force wraps it into a REPL_INFO
(need_replication = true) and passes it to locator_update_index,
which locates the PK (pk_btid_index), materializes the old key, and
stages with RVREPL_DATA_UPDATE:
// locator_update_index -- src/transaction/locator_sr.c if (pk_btid_index != -1) { // ... condensed: heap_attrvalue_get_key materializes repl_old_key, MIDXKEY domain fixup ... error_code = repl_log_insert (thread_p, class_oid, oid, LOG_REPLICATION_DATA, RVREPL_DATA_UPDATE, repl_old_key, (REPL_INFO_TYPE) repl_info->repl_info_type); /* START/NORMAL/END */ } else { LSA_SET_NULL (&tdes->repl_insert_lsa); /* no PK -> FK action can't steal it */ if (repl_info != NULL) repl_info->need_replication = false; /* <- mark class non-replicated */ }UPDATE replicates the old PK (repl_old_key) so the slave can locate
the row even when the PK changes. With no PK, need_replication is
cleared and the caller skips the back-patch. Back in
locator_update_force, heap_update_logical appends the RVHF_UPDATE-family
heap log; the replication-aware log-append helpers in log_manager.c stamp
tdes->repl_update_lsa from tail_lsa as a side effect. Only then does the
three-condition guard (!LOG_CHECK_LOG_APPLIER && log_does_allow_replication () && repl_info.need_replication) fire
repl_add_update_lsa (thread_p, oid) to fill the lsa left NULL in Phase 5.
3.7 repl_add_update_lsa — the backward-walk back-patch
Section titled “3.7 repl_add_update_lsa — the backward-walk back-patch”repl_add_update_lsa walks the staging array backward from the most
recent record, filling the nearest UPDATE record matching inst_oid:
// repl_add_update_lsa -- src/transaction/replication.c if (tdes->suppress_replication != 0) return NO_ERROR; /* <- suppressed: nothing to patch */ for (i = tdes->cur_repl_record - 1; i >= 0; i--) { /* newest first */ repl_rec = &tdes->repl_records[i]; if (OID_EQ (&repl_rec->inst_oid, inst_oid) && !LSA_ISNULL (&tdes->repl_update_lsa)) { assert (repl_rec->rcvindex == RVREPL_DATA_UPDATE || ... _UPDATE_START || ... _UPDATE_END); LSA_COPY (&repl_rec->lsa, &tdes->repl_update_lsa); /* patch the NULL lsa */ LSA_SET_NULL (&tdes->repl_update_lsa); LSA_SET_NULL (&tdes->repl_insert_lsa); find = true; break; /* patch only the nearest match */ } } if (find == false && prm_get_bool_value (PRM_ID_DEBUG_REPLICATION_DATA)) _er_log_debug (ARG_FILE_LINE, "can't find out the UPDATE LSA");Branches: suppressed returns immediately; match (OID_EQ,
non-null repl_update_lsa) copies the heap-update LSA, nulls both pending
LSAs, and stops at the nearest record (a twice-updated instance gets the
most recent LSA on its most recent record); no match logs only if
PRM_ID_DEBUG_REPLICATION_DATA is set, else returns NO_ERROR silently
(a non-replicated class’s record was never staged). The assert
documents that INSERT/DELETE never reach here with a null LSA — they
resolved eagerly in Phase 5.
Invariant —
tdes->repl_update_lsais consumed exactly once per UPDATE. Set byheap_update_logical, cleared the instantrepl_add_update_lsapatches a record; nulling both pending LSAs on success stops a later FK cascade from back-patching the wrong record. Thepreserved_repl_lsasave/restore around the FK check inlocator_update_indexprotects this when a PK also carries an FK.
3.8 Chapter summary — key takeaways
Section titled “3.8 Chapter summary — key takeaways”- All DML funnels through
locator_attribute_info_forceto the three force routines; the record is staged one layer deeper, at the index pass, where the PKDB_VALUEis already in hand. - The force routines order heap and index passes differently:
INSERT/DELETE write the heap log first (the log-append path stamps the
pending LSA eagerly), UPDATE runs the index pass first and patches the
LSA afterward; every error
gotoskips staging, and the partition-move and MVCC-disabled branches stage nothing. repl_log_insertis the single writer intotdes->repl_records[], short-circuiting ontdes == NULL,suppress_replication, and the no-logging/no-flush-mark case before growing the array.- The op kind is the
rcvindexargument: INSERT/DELETE passRVREPL_DATA_INSERT/_DELETEwith fixedREPL_INFO_TYPE_RBR_NORMAL; UPDATE passesRVREPL_DATA_UPDATEplus a START/NORMAL/END refinement rewritten intoRVREPL_DATA_UPDATE_START/_END. - Only the primary-key index stages a record, once per row; the DATA
payload is class name + packed PK behind a back-filled 4-byte size
header, with
tde_encryptedfromheap_get_class_tde_algorithm. - LSA handling is asymmetric: INSERT copies
repl_insert_lsa, DELETE usestail_lsa(or the nextprior_lsa), UPDATE stores NULL because the heap update has not happened yet. The pending LSAs themselves are written by the replication-aware log-append helpers inlog_manager.c, keyed on theRVHF_INSERT/RVHF_MVCC_INSERTandRVHF_UPDATEheap recovery indexes — not by theheap_*_logicalroutines directly. - UPDATE alone back-patches via
repl_add_update_lsa, which walks the array backward, fills the nearest matching record fromrepl_update_lsa, and nulls both pending LSAs so FK cascades cannot corrupt an earlier record’s target.
Chapter 4: Statement Based Emission and Flush Marks
Section titled “Chapter 4: Statement Based Emission and Flush Marks”Chapter 3 staged a row event into repl_records with must_flush = LOG_REPL_COMMIT_NEED_FLUSH. This chapter covers two deviations from that
path: (1) statement-based records, carrying SQL text instead of a packed
row image, and (2) flush marks, by which a record is forced to survive
even a rollback. The reader question: how are DDL and replicated session
statements staged differently from row events, and how do flush marks force
emission even on rollback? For the design rationale of the SBR path see
cubrid-ha-replication.md (“Statement-based vs row-based” and Open
Question 8); here we trace the code.
4.1 Where statement records come from
Section titled “4.1 Where statement records come from”A statement record is produced explicitly by the SQL executor on finishing a
DDL statement (and a few replicated session statements), not by the
heap/index machinery. The path is do_replicate_statement (in
execute_statement.c), which fills a stack-resident REPL_INFO_SBR repl_stmt
from the parsed statement -> locator_flush_replication_info -> the
server-side dispatcher xrepl_set_info (in locator_sr.c), the gate — only
REPL_INFO_TYPE_SBR reaches repl_log_insert_statement; every other
repl_info_type is rejected:
// xrepl_set_info -- src/transaction/locator_sr.cif (!LOG_CHECK_LOG_APPLIER (thread_p) && log_does_allow_replication () == true) { switch (repl_info->repl_info_type) { case REPL_INFO_TYPE_SBR: error_code = repl_log_insert_statement (thread_p, (REPL_INFO_SBR *) repl_info->info); break; default: /* <- RBR types never arrive here */ error_code = ER_REPL_ERROR; er_set (..., "can't make repl sbr info"); break; } }The row-based types (REPL_INFO_TYPE_RBR_* in the REPL_INFO_TYPE enum) go
through repl_log_insert on the heap/index path (Chapter 3), not here.
REPL_INFO_SBR payload fields
Section titled “REPL_INFO_SBR payload fields”REPL_INFO_SBR (alias of struct repl_info_statement, in replication.h)
describes one statement in flight — not a staged record, but the input
repl_log_insert_statement serializes into repl_rec->repl_data. Every
field:
| Field | Role | Why it exists |
|---|---|---|
statement_type | int — a CUBRID_STMT_* code (e.g. CUBRID_STMT_CREATE_CLASS, CUBRID_STMT_GRANT). | Tells the applier the statement category; do_replicate_statement switches on the PT node type to set it. PT_DROP_VARIABLE and unhandled types return NO_ERROR early — intentionally not replicated. |
name | Target object name (class/serial/index), or the sentinel unknown_name (the literal string "-") when the parser yields no name. | Lets the applier log / route by object; the sentinel keeps it from ever being an unset pointer. |
stmt_text | The SQL text to re-execute on the replica. | This is the payload — the replica re-parses and re-runs it. For CREATE/ALTER SERVER it is replaced by pt_print_bytes output to scrub passwords; with host variables it is rebuilt with ? placeholders substituted (sbr_text). |
db_user | Executing DB user (db_get_user_name ()). | The replica must apply the DDL as the same user so authorization matches; assert_release (repl_stmt.db_user != NULL) guards it. |
sys_prm_context | HA-relevant system parameters (sysprm_print_parameters_for_ha_repl ()), or NULL for non-DDL. | DDL semantics can depend on session parameters; filled only when pt_is_ddl_statement (statement). |
Invariant — SBR carries text, not a row image. A statement record’s
repl_data packs the {statement_type, name, stmt_text, db_user, sys_prm_context} tuple, and its inst_oid is always NULL
(OID_SET_NULL (&repl_rec->inst_oid)); row records carry a packed PK and a
real inst_oid. repl_type (LOG_REPLICATION_STATEMENT vs
LOG_REPLICATION_DATA) is the discriminator the shipper and applier branch
on. A non-NULL inst_oid here would let the conflict-coalescing logic in
repl_log_insert (which keys on inst_oid) mistake it for a row update.
4.2 repl_log_insert_statement — branch by branch
Section titled “4.2 repl_log_insert_statement — branch by branch”flowchart TB
A["LOG_FIND_TDES"] --> B{"tdes == NULL?"}
B -->|yes| RF["return ER_FAILED"]
B -->|no| C{"suppress_replication?"}
C -->|yes| OK0["return NO_ERROR\nno record staged"]
C -->|no| D{"array state"}
D -->|NOT_EXISTS| AL1["alloc(false)"]
D -->|IS_FULL| AL2["alloc(true) realloc"]
D -->|has room| E
AL1 --> E["stamp identity\ntype/rcvindex/must_flush/inst_oid"]
AL2 --> E
E --> G["malloc repl_data"]
G --> H{"malloc OK?"}
H -->|no| ME["ER_REPL_ERROR, return"]
H -->|yes| I["or_pack 5 fields\nlsa = tdes->tail_lsa"]
I --> J{"in flush-mark window?"}
J -->|yes| K["must_flush = NEED_FLUSH"]
J -->|no| L["keep COMMIT_NEED_FLUSH"]
K --> M["cur_repl_record++"]
L --> M
Figure 4-1 — Control flow of repl_log_insert_statement. The two early
returns and the malloc error path all leave cur_repl_record untouched, so
no half-built record is published.
The preamble matches repl_log_insert (Chapter 3): resolve the LOG_TDES,
honour suppress_replication, allocate or grow repl_records. Two
allocation branches matter:
// repl_log_insert_statement -- src/transaction/replication.cif (REPL_LOG_IS_NOT_EXISTS (tran_index) /* <- never allocated */ && ((error = repl_log_info_alloc (tdes, REPL_LOG_INFO_ALLOC_SIZE, false)) != NO_ERROR)) return error;else if (REPL_LOG_IS_FULL (tran_index) /* <- last free slot used */ && (error = repl_log_info_alloc (tdes, REPL_LOG_INFO_ALLOC_SIZE, true)) != NO_ERROR) return error;REPL_LOG_IS_FULL is num_repl_records == cur_repl_record + 1, so the grow
fires one slot before overflow — the write to repl_records[cur_repl_record]
always lands in bounds. The record is then stamped with its identity and LSA:
// repl_log_insert_statement -- src/transaction/replication.crepl_rec->repl_type = LOG_REPLICATION_STATEMENT;repl_rec->tde_encrypted = false; /* <- SBR text is never TDE data */repl_rec->rcvindex = RVREPL_STATEMENT;repl_rec->must_flush = LOG_REPL_COMMIT_NEED_FLUSH; /* <- default: flush at commit only */OID_SET_NULL (&repl_rec->inst_oid); /* <- statements have no instance */// ... condensed: length, malloc repl_data, or_pack the 5 fields ...LSA_COPY (&repl_rec->lsa, &tdes->tail_lsa); /* <- last log this txn wrote */The payload length sums one OR_INT_SIZE (for statement_type) plus four
packed-string lengths; malloc produces repl_data, then or_pack_int /
or_pack_string_with_length serialize the tuple. A malloc failure raises
ER_REPL_ERROR and returns before incrementing cur_repl_record, leaving
the array unchanged. The lsa is what repl_log_abort_after_lsa compares
against a rollback LSA (§4.4) and what copylogdb/applylogdb order by.
Unlike repl_log_insert’s RVREPL_DATA_INSERT/DELETE cases (which use
repl_insert_lsa or prior_info.prior_lsa), the statement path has no
before/after image, so plain tail_lsa — the last log this txn wrote — is
the right anchor.
The flush-mark check at the tail differs from the row check: where the row
path scans repl_records[0 .. fl_mark_repl_recidx) for a prior
COMMIT_NEED_FLUSH record with the same inst_oid and promotes only if
none is found (conflict coalescing), the statement path does no such
scan:
// repl_log_insert_statement -- src/transaction/replication.cif (tdes->fl_mark_repl_recidx != -1 && tdes->cur_repl_record >= tdes->fl_mark_repl_recidx) { /* statement replication does not check log conflicts, so * use repl_start_flush_mark with caution. */ repl_rec->must_flush = LOG_REPL_NEED_FLUSH; /* <- unconditional promotion */ }tdes->cur_repl_record++;The in-source comment is the load-bearing warning: with no inst_oid to
deduplicate on, any statement record at or after the flush-mark index is
forced to NEED_FLUSH.
4.3 The must_flush state machine and the flush-mark bracket
Section titled “4.3 The must_flush state machine and the flush-mark bracket”LOG_REPL_FLUSH (in replication.h) is a three-valued enum deciding, per
staged record, when it may be shipped:
| Value | Numeric | Meaning |
|---|---|---|
LOG_REPL_DONT_NEED_FLUSH | -1 | Never ship — logically discarded (e.g. by repl_log_abort_after_lsa). |
LOG_REPL_COMMIT_NEED_FLUSH | 0 | Ship only if the transaction commits (default for every row and statement record); rollback drops it. |
LOG_REPL_NEED_FLUSH | 1 | Ship on commit and rollback alike — must reach the replica even if the transaction undoes its data. Set only inside a flush-mark bracket. |
The bracket is tdes->fl_mark_repl_recidx, an int index into
repl_records (a LOG_TDES field, Chapter 1), opened by
repl_start_flush_mark and closed by repl_end_flush_mark.
stateDiagram-v2 [*] --> Closed: fl_mark_repl_recidx == -1 Closed --> Open: repl_start_flush_mark\n records cur_repl_record Open --> Open: insert in window \n new records get NEED_FLUSH Open --> Closed: repl_end_flush_mark need_undo=false\n keep records, clear mark Open --> RolledBack: repl_end_flush_mark need_undo=true\n free and truncate to mark RolledBack --> Closed: mark cleared
Figure 4-2 — The flush-mark state machine on one transaction descriptor.
“Open” means the mark holds a valid index, “Closed” means -1.
repl_start_flush_mark
Section titled “repl_start_flush_mark”// repl_start_flush_mark -- src/transaction/replication.ctdes = LOG_FIND_CURRENT_TDES (thread_p);if (tdes == NULL) { er_set (ER_FATAL_ERROR_SEVERITY, ..., ER_LOG_UNKNOWN_TRANINDEX, ...); return; /* <- fatal: no current txn */ }if (tdes->fl_mark_repl_recidx == -1) tdes->fl_mark_repl_recidx = tdes->cur_repl_record; /* <- remember where window opened *//* else already started, return */ /* <- idempotent, not nested */Invariant — the flush mark is not re-entrant. A second
repl_start_flush_mark while a mark is open is a no-op: the if (== -1)
guard fixes the start index at the first call, so the bracket cannot be
nested or its start moved. Stacking marks would bind the inner start to
the outer window and let the inner end close everything early — hence
qexec_execute_selupd_list pairs exactly one start with one end.
repl_end_flush_mark
Section titled “repl_end_flush_mark”// repl_end_flush_mark -- src/transaction/replication.cif (need_undo) { LOG_REPL_RECORD *recsp = tdes->repl_records; for (i = tdes->fl_mark_repl_recidx; i < tdes->cur_repl_record; i++) free_and_init (recsp[i].repl_data); /* <- free payloads staged in window */ tdes->cur_repl_record = tdes->fl_mark_repl_recidx; /* <- truncate array back to mark */ }tdes->fl_mark_repl_recidx = -1; /* <- always close the window */Two branches:
need_undo == false(success). The window’s records are kept (stillLOG_REPL_NEED_FLUSH); only the mark integer resets to-1.need_undo == true(failed). Each record fromfl_mark_repl_recidxup to (not including)cur_repl_recordhas itsrepl_datafreed andcur_repl_recordrewinds to the mark, erasing them; then the mark clears.
Exactly three sites in the tree open and close a bracket, and they all pair one start with one end:
xchksum_insert_repl_log_and_demote_table_lock(inlocator_sr.c) — the canonical statement caller and the one that wraps the DDL path of this chapter. It runslog_sysop_start->repl_start_flush_mark->xrepl_set_info(which reachesrepl_log_insert_statement) ->repl_end_flush_mark (..., false). The bracket promotes the DDL’s statement record toLOG_REPL_NEED_FLUSH, so it reaches the replica even if the wrapping transaction later rolls its data changes back; onerror != NO_ERRORit falls tolog_sysop_abortinstead.qexec_execute_selupd_list(inquery_executor.c) — the canonical row caller, around a batch of increments underlock_is_instant_lock_mode. It callsrepl_end_flush_mark (..., false)on success andrepl_end_flush_mark (..., true)onexit_on_error, so the batch emits atomically.serial_update_serial_object(inserial.c) — only whenlock_mode != X_LOCK; anX_LOCK(uncommitted create/alter) deliberately skips both the sysop and the flush mark, since marking it would mis-order the replication log.
All three gate the start/end pair behind !LOG_CHECK_LOG_APPLIER and
log_does_allow_replication () — the applier never opens a bracket while
replaying.
4.4 repl_log_abort_after_lsa — surviving partial rollback
Section titled “4.4 repl_log_abort_after_lsa — surviving partial rollback”Forcing a record to flush on rollback (§4.3) is only half the story.
Partial rollback — a system-operation abort or savepoint rollback — must
discard records staged after that point while flush-marked records before
it survive. That discard is repl_log_abort_after_lsa:
// repl_log_abort_after_lsa -- src/transaction/replication.crepl_rec_arr = tdes->repl_records;for (i = 0; i < tdes->cur_repl_record; i++) { if (LSA_GT (&repl_rec_arr[i].lsa, start_lsa)) /* <- staged strictly after the rollback point */ repl_rec_arr[i].must_flush = LOG_REPL_DONT_NEED_FLUSH; }return NO_ERROR;It walks every staged record and tombstones any whose lsa is strictly
greater than start_lsa with LOG_REPL_DONT_NEED_FLUSH. It does not
rewind cur_repl_record or free repl_data — records stay in the array but
the shipper skips them. Records at or before start_lsa keep their flush
state (including LOG_REPL_NEED_FLUSH), so flush-marked work before the
rollback point survives.
The sole caller is log_sysop_abort (in log_manager.c), under three
guards that keep it off the applier and non-worker transactions:
// log_sysop_abort -- src/transaction/log_manager.cif (!LOG_CHECK_LOG_APPLIER (thread_p) /* <- not the applier replaying */ && tdes->is_active_worker_transaction () /* <- a real worker txn, not sysop-main */ && log_does_allow_replication () == true) /* <- HA replication enabled */ { repl_log_abort_after_lsa (tdes, LOG_TDES_LAST_SYSOP_PARENT_LSA (tdes)); }This sits in the else arm of log_sysop_abort’s LSA_ISNULL || LSA_LE
guard, so it runs only when the system operation actually logged past its
parent LSA; a no-op sysop skips the tombstone walk.
Invariant — LSA_GT is strict, so the rollback boundary record is
kept. Records whose lsa equals start_lsa are not tombstoned (>, not
>=); the boundary record belongs to the surviving prefix. The two
mechanisms compose by orthogonal criteria — the flush mark protects by
flush state at ship time, while repl_log_abort_after_lsa tombstones by
LSA position regardless of flush state. A flush-marked record before the
rollback point keeps NEED_FLUSH and ships; one after it is dropped. A
reader touching savepoint or sysop logic must preserve this.
4.5 Statement determinism — pointer to the open question
Section titled “4.5 Statement determinism — pointer to the open question”The statement path stages SQL text and re-executes it on the replica —
compact but not deterministic: a statement with NOW(), RAND(), or a
serial-dependent expression can diverge, since it is re-run rather than the
row image shipped. CUBRID mitigates this for serials (the CT_SERIAL_NAME
branch in repl_log_insert, Chapter 3) and by shipping sys_prm_context,
but general re-execution non-determinism is unresolved here — Open
Question 8 in cubrid-ha-replication.md. The detail this chapter adds is
where it surfaces: the shipped field is stmt_text, not a row image.
4.6 Chapter summary — key takeaways
Section titled “4.6 Chapter summary — key takeaways”-
Statement records take a separate entry point —
do_replicate_statement->xrepl_set_info, which dispatches onlyREPL_INFO_TYPE_SBRtorepl_log_insert_statement; everything else isER_REPL_ERROR, and row events never reach this function. -
REPL_INFO_SBRships text, not a row. Its five fields pack intorepl_data; the staged record isLOG_REPLICATION_STATEMENT,rcvindex = RVREPL_STATEMENT, with an always-NULLinst_oid. -
must_flushis a three-valued ship gate —DONT_NEED_FLUSH(-1) never ships,COMMIT_NEED_FLUSH(0) ships on commit only (default),NEED_FLUSH(1) ships on commit and rollback. -
Statement flush-mark promotion is unconditional — unlike
repl_log_insert(coalesces byinst_oid),repl_log_insert_statementforces any in-window record toNEED_FLUSH(“use with caution”). -
The flush-mark bracket is a single, non-reentrant integer.
repl_start_flush_marksetsfl_mark_repl_recidxonly if-1;repl_end_flush_markkeeps (need_undo=false) or frees-and-truncates (need_undo=true) the window, then resets to-1. Three callers pair a start with an end —xchksum_insert_repl_log_and_demote_table_lock(statement/DDL),qexec_execute_selupd_list(row batch), and theserial.cupdate path. -
repl_log_abort_after_lsatombstones by LSA position, not flush mark — onlog_sysop_abortit demotes every record withlsa > start_lsatoDONT_NEED_FLUSH(strict, so the boundary survives), overriding a flush mark for work the partial rollback undid. -
Statement determinism is the open seam —
stmt_textis re-executed on the replica, so non-deterministic functions diverge (Open Question 8).
Chapter 5: Commit Time Flush and Atomic Emission
Section titled “Chapter 5: Commit Time Flush and Atomic Emission”By the end of Chapter 4 the transaction descriptor holds a fully
populated staging array tdes->repl_records[0 .. cur_repl_record-1],
each a LOG_REPL_RECORD with its repl_type, rcvindex, back-patched
lsa, packed repl_data, and a must_flush flag — none yet in the WAL
stream. This chapter answers the commit-time question: how are those
staged records turned into real LOG_REPLICATION_DATA /
LOG_REPLICATION_STATEMENT WAL records, and what guarantees that no peer
transaction’s commit record slips between transaction T’s replication
records and T’s own commit record?
The companion high-level doc (cubrid-ha-replication.md, section
“Commit-time emission — log_append_repl_info_and_commit_log”) states
the atomicity contract; this chapter dissects the five functions that
implement it, branch by branch, and names the invariant the slave’s
restart logic depends on. The prior-list drain that writes these nodes
to the log page buffer is not re-derived here — see
cubrid-log-manager-detail.md, section “Prior-LSA list and the append
pipeline.”
Three end-of-transaction outcomes bound the scope: a committing HA transaction emits its staged records and its commit record under one mutex (§5.2); a plain commit (any guard term false) emits only the commit record and silently drops the staged records (§5.1); and an aborting transaction emits no replication record at all — abort never walks the staging array, so the records are discarded by the descriptor reset at transaction-end cleanup (§5.1, abort note).
5.1 The commit decision in log_commit_local
Section titled “5.1 The commit decision in log_commit_local”log_commit (the public entry) does 2PC bookkeeping, then for a local
or participant transaction calls log_commit_local. The replication
fork lives at the tail of log_commit_local, inside the
“transaction updated data” arm (is_local_tran):
// log_commit_local -- src/transaction/log_manager.cif (is_local_tran) { LOG_LSA commit_lsa; // ... condensed ... if (!LOG_CHECK_LOG_APPLIER (thread_p) && tdes->is_active_worker_transaction () && log_does_allow_replication () == true) log_append_repl_info_and_commit_log (thread_p, tdes, &commit_lsa); /* <- atomic path */ else log_append_commit_log (thread_p, tdes, &commit_lsa); /* <- plain commit, no repl */ if (retain_lock != true) lock_unlock_all (thread_p); /* <- AFTER the commit log is appended; see ordering note */ log_change_tran_as_completed (thread_p, tdes, LOG_COMMIT, &commit_lsa); }Every branch of the guard matters:
| Branch | Condition | Outcome |
|---|---|---|
| Atomic repl path | replication allowed and active worker tran and caller is not the log applier | log_append_repl_info_and_commit_log — repl records + commit under one mutex |
| Plain commit | any guard term false | log_append_commit_log — commit record only, staged repl records never emitted |
| No-update arm | LSA_ISNULL (&tdes->tail_lsa) (function top) | neither is called; state jumps straight to TRAN_UNACTIVE_COMMITTED |
LOG_CHECK_LOG_APPLIER excludes the slave’s own apply connection (it
must not re-emit replication of its own); is_active_worker_transaction () excludes system worker transactions (vacuum, checkpoint) whose
changes are not user-visible DML; log_does_allow_replication () (in
log_comm.c) returns false under SA_MODE/WINDOWS, false for
log-copier/applier clients, and otherwise consults the HA server state.
When any term is false the staged records are discarded with the
descriptor.
The same emitter is reached from a second site. log_complete (non-2PC
completion) appends only a plain commit; log_complete_for_2pc (the 2PC
path) carries the repl+commit emitter under a two-term variant —
!LOG_CHECK_LOG_APPLIER (thread_p) && log_does_allow_replication () == true — that drops the is_active_worker_transaction () term (redundant
there, since a 2PC transaction is by construction an active worker). The
rest of the chapter treats both sites as one.
Ordering note. The repl+commit append happens before
lock_unlock_all (the source block comment explains why): if T1 resumed
waiter T2, T2 committed, and a crash struck after T2’s commit but before
T1’s, an out-of-order unlock log would let recovery abort T1 while T2 is
already committed. Writing the unlock/commit log first closes that
window.
Abort discards by not emitting. The abort counterpart
log_abort_local has no replication fork at all: it calls log_rollback,
then lock_unlock_all, and returns TRAN_UNACTIVE_ABORTED — it never
calls any log_append_repl_info*. The staged tdes->repl_records are
therefore discarded purely by omission: nothing emits them, and at
transaction-end cleanup logtb_clear_tdes resets the staging array
(cur_repl_record = 0, append_repl_recidx = -1,
fl_mark_repl_recidx = -1). There is no abort-time gate walk to skip
records; the gate (§5.4.2) is reached only from the commit and
system-op paths.
5.2 The atomicity idiom — log_append_repl_info_and_commit_log
Section titled “5.2 The atomicity idiom — log_append_repl_info_and_commit_log”// log_append_repl_info_and_commit_log -- src/transaction/log_manager.cstatic voidlog_append_repl_info_and_commit_log (THREAD_ENTRY * thread_p, LOG_TDES * tdes, LOG_LSA * commit_lsa){ if (tdes->has_supplemental_log) /* <- SUPPLEMENTAL_LOG > 0 set a user-name marker */ { log_append_supplemental_info (thread_p, LOG_SUPPLEMENT_TRAN_USER, strlen (tdes->client.get_db_user ()), tdes->client.get_db_user ()); tdes->has_supplemental_log = false; }
log_Gl.prior_info.prior_lsa_mutex.lock (); /* <- single hold spans BOTH appends */ log_append_repl_info_with_lock (thread_p, tdes, true); /* is_commit = true */ log_append_commit_log_with_lock (thread_p, tdes, commit_lsa); /* LOG_COMMIT donetime */ log_Gl.prior_info.prior_lsa_mutex.unlock ();}Invariant 5-A — strict adjacency of T’s repl records and T’s commit. Between transaction T’s last
LOG_REPLICATION_*record and T’sLOG_COMMITrecord, no other transaction’s commit record may appear in the log. Enforced by holdinglog_Gl.prior_info.prior_lsa_mutexacross bothlog_append_repl_info_with_lockandlog_append_commit_log_with_lock— the same mutex every other committer must take to append its commit donetime, so no peer commit can interleave. If violated: the two appends could be split by a peer commit, producing[T repl...][T' commit][T commit]; a slave (applylogdb) that crashed and restarted with its durable bookmark (Chapter 10) pointing between[T repl...]and[T' commit]would read T’s commit out of position, mis-attribute or mis-order the flush, and could silently drop T’s row changes. The doc comment names this exactly: “Atomic write of replication log and commit log is crucial for replication consistencies.”
The non-HA path differs: log_append_commit_log (§5.5) takes no
explicit mutex — log_append_donetime_internal runs with
LOG_PRIOR_LSA_WITHOUT_LOCK and acquires the mutex internally for its
single commit append. Only the HA path holds one lock across two
appends, so only it takes the mutex at the caller.
The has_supplemental_log prefix. When PRM_ID_SUPPLEMENTAL_LOG > 0
and tdes->has_supplemental_log is set, a LOG_SUPPLEMENTAL_INFO record
of sub-type LOG_SUPPLEMENT_TRAN_USER carrying the committing DB user
name is appended first (outside the mutex) and the flag cleared, so it
emits at most once per commit. This feeds CDC / supplemental consumers
the transaction’s user identity; it is unrelated to the HA atomicity
guarantee and deliberately sits before the lock.
5.3 log_append_repl_info vs log_append_repl_info_with_lock
Section titled “5.3 log_append_repl_info vs log_append_repl_info_with_lock”These are one-line dispatchers over a shared internal, differing only in
the with_lock argument forwarded:
// log_append_repl_info / log_append_repl_info_with_lock -- src/transaction/log_manager.cvoid log_append_repl_info (THREAD_ENTRY * thread_p, LOG_TDES * tdes, bool is_commit){ log_append_repl_info_internal (thread_p, tdes, is_commit, LOG_PRIOR_LSA_WITHOUT_LOCK); }
static void log_append_repl_info_with_lock (THREAD_ENTRY * thread_p, LOG_TDES * tdes, bool is_commit){ log_append_repl_info_internal (thread_p, tdes, is_commit, LOG_PRIOR_LSA_WITH_LOCK); }with_lock selects prior_lsa_next_record_with_lock (mutex already held
by the caller) vs prior_lsa_next_record (acquires the mutex per node).
The two callers have distinct purposes:
| Caller | is_commit | with_lock | Purpose |
|---|---|---|---|
log_append_repl_info_and_commit_log → _with_lock | true | WITH_LOCK | Commit-time flush; emits all not-already-flushed records under the held mutex (the atomic path) |
log_sysop_commit_internal → log_append_repl_info | false | WITHOUT_LOCK | System-op commit, under the same three-term guard as §5.1; emits only LOG_REPL_NEED_FLUSH records mid-transaction (DDL flush marks), each taking the mutex itself |
The system-op caller is the flush-mark mechanism from Chapter 4: a DDL
sub-operation replicated at the system-op boundary (not deferred to user
commit) marks its record LOG_REPL_NEED_FLUSH, and
log_sysop_commit_internal flushes it early with is_commit = false.
The is_commit flag distinguishes “flush only the NEED_FLUSH records
now” from “flush everything still pending at commit”; §5.4 shows how the
gate uses it.
5.4 log_append_repl_info_internal — branch by branch
Section titled “5.4 log_append_repl_info_internal — branch by branch”The core: it walks the staging array from a cursor and converts qualifying records into prior-list nodes.
// log_append_repl_info_internal -- src/transaction/log_manager.cstatic voidlog_append_repl_info_internal (THREAD_ENTRY * thread_p, LOG_TDES * tdes, bool is_commit, int with_lock){ LOG_REPL_RECORD *repl_rec; LOG_REC_REPLICATION *log; LOG_PRIOR_NODE *node;
if (tdes->append_repl_recidx == -1 || is_commit) /* first time, OR commit rescans from start */ tdes->append_repl_recidx = 0;
while (tdes->append_repl_recidx < tdes->cur_repl_record) { repl_rec = (LOG_REPL_RECORD *) (&(tdes->repl_records[tdes->append_repl_recidx]));
if ((repl_rec->repl_type == LOG_REPLICATION_DATA || repl_rec->repl_type == LOG_REPLICATION_STATEMENT) && ((is_commit && repl_rec->must_flush != LOG_REPL_DONT_NEED_FLUSH) || repl_rec->must_flush == LOG_REPL_NEED_FLUSH)) /* <- the gate, §5.4.2 */ { node = prior_lsa_alloc_and_copy_data (thread_p, repl_rec->repl_type, RV_NOT_DEFINED, NULL, repl_rec->length, repl_rec->repl_data, 0, NULL); if (node == NULL) { assert (false); continue; } /* <- does NOT advance recidx */
if (repl_rec->tde_encrypted) if (prior_set_tde_encrypted (node, repl_rec->rcvindex) != NO_ERROR) { assert (false); continue; } /* <- same non-advancing continue */
log = (LOG_REC_REPLICATION *) node->data_header; if (repl_rec->rcvindex == RVREPL_DATA_DELETE || repl_rec->rcvindex == RVREPL_STATEMENT) LSA_SET_NULL (&log->lsa); /* <- DELETE/STATEMENT carry no target LSA */ else LSA_COPY (&log->lsa, &repl_rec->lsa); /* <- INSERT/UPDATE copy the heap LSA through */ log->length = repl_rec->length; log->rcvindex = repl_rec->rcvindex;
// ... with_lock ? prior_lsa_next_record_with_lock : prior_lsa_next_record (both arms in §5.3) ...
repl_rec->must_flush = LOG_REPL_DONT_NEED_FLUSH; /* <- §5.4.4 re-emit guard */ } tdes->append_repl_recidx++; }}5.4.1 The cursor reset
Section titled “5.4.1 The cursor reset”append_repl_recidx is the resume cursor, set to 0 at entry in two
cases: first time ever (== -1, freshly initialized descriptor),
or is_commit true — commit rescans the entire array from 0
because mid-transaction flush-mark passes (§5.3) advanced the cursor
past records they inspected, and commit must re-examine every record to
catch the COMMIT_NEED_FLUSH ones those passes skipped. When
is_commit is false and the cursor is already >= 0, a non-commit pass
resumes where it left off, scanning only records appended since the
last pass.
5.4.2 The must_flush gate — every combination
Section titled “5.4.2 The must_flush gate — every combination”The if admits a record when its type is a replication type and the
flush condition holds, against the three LOG_REPL_FLUSH values
(replication.h):
must_flush | value | commit pass (is_commit=true) | flush-mark pass (is_commit=false) |
|---|---|---|---|
LOG_REPL_DONT_NEED_FLUSH | -1 | skip (!= DONT_NEED_FLUSH is false) | skip (!= NEED_FLUSH) |
LOG_REPL_COMMIT_NEED_FLUSH | 0 | emit (!= DONT_NEED_FLUSH true) | skip (!= NEED_FLUSH) |
LOG_REPL_NEED_FLUSH | 1 | emit | emit (== NEED_FLUSH) |
The outer type check (LOG_REPLICATION_DATA || LOG_REPLICATION_STATEMENT)
is belt-and-suspenders — the staging array only ever holds those two
types.
5.4.3 Building the LOG_REC_REPLICATION node — the lsa-null rules
Section titled “5.4.3 Building the LOG_REC_REPLICATION node — the lsa-null rules”prior_lsa_alloc_and_copy_data allocates a prior node whose
data_header is a LOG_REC_REPLICATION (the 3-field {lsa, length, rcvindex} struct covered in Chapter 1) and whose body copies
repl_rec->repl_data (length bytes — the packed
pkey_size | class_name | pkey_dbvalue). length and rcvindex are
filled straight from the staging record; lsa is set per rcvindex:
rcvindex | header lsa | Why |
|---|---|---|
RVREPL_DATA_DELETE | LSA_SET_NULL | slave needs only the primary key to locate-and-delete; no after-image |
RVREPL_STATEMENT | LSA_SET_NULL | the statement text is the payload; there is no heap record to point at |
RVREPL_DATA_INSERT | LSA_COPY from repl_rec->lsa | points at the heap insert log; slave fetches the inserted row image |
RVREPL_DATA_UPDATE | LSA_COPY | back-patched by repl_add_update_lsa (Chapter 3); points at the heap update log |
RVREPL_DATA_UPDATE_START / UPDATE_END | LSA_COPY | same as UPDATE; the START/END pair brackets a multi-row update |
The values copied here were set on the staging side (Chapter 3).
5.4.4 TDE encryption and the post-emit flush-flag flip
Section titled “5.4.4 TDE encryption and the post-emit flush-flag flip”Two side conditions complete the per-record handling:
tde_encrypted. If the staged record came from a TDE-encrypted class (repl_rec->tde_encryptedset at staging — Chapter 3),prior_set_tde_encrypted(log_append.cpp) checkstde_is_loaded (); if the master key is not loaded it raisesER_TDE_CIPHER_IS_NOT_LOADEDand the record skips via theassert(false); continue;arm (which does not advance the cursor — see Invariant 5-B and Figure 5-1), else it setsnode->tde_encrypted = trueso the carrying page is encrypted at rest. Chapter 11 covers the apply side.must_flush = LOG_REPL_DONT_NEED_FLUSHafter a successfulprior_lsa_next_record*— the re-emit guard: once a record is in the prior list, flipping it toDONT_NEED_FLUSHmakes a later commit pass rescan (§5.4.1) skip it, because the gate excludesDONT_NEED_FLUSH. This matters when a mid-transaction flush-mark pass (§5.3) already emitted aNEED_FLUSHrecord: the commit rescan that walks from index 0 must not emit it a second time. Abort does not enter this loop at all (§5.1), so the guard is purely a commit-vs-flush-mark de-duplication, not an abort filter.
Invariant 5-B — each staged record is emitted at most once. Enforced by flipping
must_flushtoDONT_NEED_FLUSHright after the node is handed toprior_lsa_next_record*. If violated: a record emitted by an early flush-mark pass and re-emitted by the commit rescan (which restarts at index 0, §5.4.1) would produce twoLOG_REPLICATION_*records for one logical change, and the slave would apply the same INSERT/UPDATE twice. The guard governs only the commit-vs-flush-mark overlap; the abort path emits nothing (§5.1) and needs no such filter. Note the two error arms (node == NULL,prior_set_tde_encrypted != NO_ERROR)continuewithout advancingappend_repl_recidx; both areassert(false)dead-ends for “impossible” states (allocation failure, missing TDE key), not graceful recovery — a modifier must preserve the success-path cursor advance.
5.4.5 Flow of one commit-time pass
Section titled “5.4.5 Flow of one commit-time pass”Each loop iteration is a 4-decision cascade (the real code above carries the same branches inline):
flowchart TD
C{"recidx < cur_repl_record ?"}
C -- no --> Z["return"]
C -- yes --> D{"REPL type and gate passes ?"}
D -- no --> INC["recidx++"] --> C
D -- yes --> G{"alloc NULL, or tde set fails ?"}
G -- yes --> C2["continue, no recidx++"] --> C
G -- no --> K{"rcvindex DELETE/STATEMENT ?"}
K -- yes --> L["lsa = NULL"] --> N
K -- no --> M["lsa = repl_rec->lsa"] --> N
N["next_record, then must_flush = DONT_NEED_FLUSH"] --> INC
Figure 5-1 — log_append_repl_info_internal loop body: the gate, the
non-advancing error continue, and the lsa-null split.
5.5 The commit donetime — log_append_commit_log_with_lock
Section titled “5.5 The commit donetime — log_append_commit_log_with_lock”The second append under the held mutex is the commit record itself:
// log_append_commit_log_with_lock / log_append_commit_log -- src/transaction/log_manager.cstatic void log_append_commit_log_with_lock (THREAD_ENTRY * thread_p, LOG_TDES * tdes, LOG_LSA * commit_lsa){ log_append_donetime_internal (thread_p, tdes, commit_lsa, LOG_COMMIT, LOG_PRIOR_LSA_WITH_LOCK); }
static void log_append_commit_log (THREAD_ENTRY * thread_p, LOG_TDES * tdes, LOG_LSA * commit_lsa){ if (tdes->has_supplemental_log) /* <- non-HA path emits the user-name marker here (cf. §5.2) */ { /* ... condensed: same log_append_supplemental_info + clear flag as §5.2 ... */ } log_append_donetime_internal (thread_p, tdes, commit_lsa, LOG_COMMIT, LOG_PRIOR_LSA_WITHOUT_LOCK);}log_append_donetime_internal allocates a LOG_COMMIT node, stamps
donetime->at_time = time (NULL), and — because the HA caller passed
WITH_LOCK — calls prior_lsa_next_record_with_lock without re-acquiring
the mutex, which is why the emitter holds one lock across both appends.
The assigned LSA returns into commit_lsa, flowing back through
log_commit_local to log_change_tran_as_completed (drives
logpb_flush_pages, flips to TRAN_UNACTIVE_COMMITTED). The prior list
now holds, in strict order, [repl record 0 .. n] then [LOG_COMMIT];
its drain into the log page buffer is detailed in
cubrid-log-manager.md, section “Prior-LSA list and the append
pipeline.”
5.6 Chapter summary — key takeaways
Section titled “5.6 Chapter summary — key takeaways”log_commit_localforks on three conditions (!LOG_CHECK_LOG_APPLIER,is_active_worker_transaction,log_does_allow_replication); only when all hold does it call the atomic emitter, else a plainlog_append_commit_logwith no repl. The 2PC path (log_complete_for_2pc) uses a two-term variant that drops the redundantis_active_worker_transaction()check.- Atomicity is one mutex held across two appends.
log_append_repl_info_and_commit_logholdslog_Gl.prior_info.prior_lsa_mutexacross bothlog_append_repl_info_with_lockandlog_append_commit_log_with_lock(Invariant 5-A); splitting them would let[T repl...][T' commit][T commit]appear and the slave drop or mis-order T’s changes on restart. - The
must_flushgate isis_commit-aware. At commit every record!= DONT_NEED_FLUSHemits (bothCOMMIT_NEED_FLUSHDML rows and pendingNEED_FLUSH); a flush-mark pass (is_commit=false) emits onlyNEED_FLUSHDDL records. - lsa-null is per
rcvindex. DELETE and STATEMENT null the headerlsa; INSERT and the three UPDATE variants copy the back-patched heap LSA through so the slave fetches the after-image. - Each record emits at most once (Invariant 5-B): after a node is
appended
must_flushflips toDONT_NEED_FLUSH, so the commit rescan (which restarts at index 0) will not re-emit a record an earlier flush-mark pass already wrote. Abort emits nothing —log_abort_localhas no replication fork; staged records are discarded by thelogtb_clear_tdesdescriptor reset, not by the gate. The two errorcontinuearms areassert(false)dead-ends that do not advance the cursor. log_append_repl_info(WITHOUT_LOCK,is_commit=false) andlog_append_repl_info_with_lock(WITH_LOCK,is_commit=true) are the two faces of one internal: the system-op flush-mark caller and the commit-atomic caller.
Chapter 6: Shipping Log Pages with copylogdb
Section titled “Chapter 6: Shipping Log Pages with copylogdb”The slave-side copylogdb daemon pulls raw log pages (not parsed records) and
lays them into a slave-local active-log volume that near-mirrors the master’s.
The companion’s Slave side — copylogdb section
established that a puller exists and why it copies pages; this chapter traces
how — every struct field, every branch of the fetch loop, the physical-page
wraparound math, and the master-side responder that blocks until the page exists.
Two halves live in log_writer.c under opposite compile guards. The client
half (#if defined(CS_MODE)) is the copylogdb process on the slave; the
server half (SERVER_MODE) is xlogwr_get_log_pages in the master
cub_server. They speak the NET_SERVER_LOGWR_GET_LOG_PAGES protocol.
6.1 The two global personalities — LOGWR_GLOBAL vs LOGWR_INFO
Section titled “6.1 The two global personalities — LOGWR_GLOBAL vs LOGWR_INFO”log_writer.h defines two disjoint structure sets selected by the compile guard.
graph TB
subgraph CS["CS_MODE (copylogdb process, the slave puller)"]
G["LOGWR_GLOBAL logwr_Gl<br/>process-wide singleton"]
C["LOGWR_CONTEXT ctx<br/>one per copy session"]
G -.owns active-log fd, buffer.-> C
end
subgraph SV["SERVER_MODE (inside master cub_server)"]
I["LOGWR_INFO log_Gl.writer_info<br/>master-side coordinator"]
E["LOGWR_ENTRY<br/>one per connected slave"]
I -->|writer_list| E
end
C -.NET_SERVER_LOGWR_GET_LOG_PAGES.-> E
Figure 6-1. Client structs (LOGWR_GLOBAL, LOGWR_CONTEXT) live in the slave
puller; server structs (LOGWR_INFO, LOGWR_ENTRY) live in the master. They
never coexist in one binary.
LOGWR_MODE is the only type shared by both halves — it travels on the wire:
| Enumerator | Value | Meaning |
|---|---|---|
LOGWR_MODE_ASYNC | 1 | Master replies as soon as pages are ready; never blocks the master flush. |
LOGWR_MODE_SEMISYNC | 2 | Master flush waits for the slave to fetch (not apply). |
LOGWR_MODE_SYNC | 3 | Master flush waits for the slave’s fetch round-trip. |
Exact wait semantics are an Open Question in the companion;
§6.8 documents the mechanism (thread_suspend_with_other_mutex). The bit
LOGWR_COPY_FROM_FIRST_PHY_PAGE_MASK (0x80000000) is OR’d into the wire mode for
a fresh copylogdb -f — “copy from the oldest physical page” (§6.8).
LOGWR_GLOBAL — the slave puller’s whole world
Section titled “LOGWR_GLOBAL — the slave puller’s whole world”logwr_Gl is one static instance. Its fields, grouped:
| Field(s) | Role | Why it exists |
|---|---|---|
hdr | Slave’s working copy of the master LOG_HEADER. | Drives all physical-page math (fpageid, npages, nxarv_*); refreshed from page 0 of every reply. |
loghdr_pgptr | Pointer into logpg_area at the header slot. | logwr_flush_header_page writes the header without a copy. |
db_name / hostname | DB name with @host split out (hostname points past @). | Names volumes; identifies the master. Split in logwr_initialize. |
log_path / loginf_path / active_name | Slave log dir, loginfo path, active-log path. | Where volumes live; active_name is what logwr_writev_append_pages writes. |
append_vdes | fd of the slave active log (or NULL_VOLDES). | The single mutable output handle. |
logpg_area / logpg_area_size / logpg_fill_size | Receive buffer (NPAGES+1)*LOG_PAGESIZE, its size, bytes received. | Lands one reply (page 0 = header, 1..N = data); logwr_set_hdr_and_flush_info walks [0,fill_size). |
toflush / max_toflush / num_toflush | LOG_PAGE* data pointers, capacity (= NPAGES), valid count. | The flush worklist (§6.5); num_toflush is the loop bound. |
mode | Current LOGWR_MODE. | Saved/restored per request; downgradable to ASYNC mid-catchup. |
action | LOGWR_ACTION_* bitmask. | Carries DELAYED_WRITE / ASYNC_WRITE / HDR_WRITE / ARCHIVING across loop turns. |
last_recv_pageid | Highest logical pageid received. | The cursor. Becomes the next request’s first_pageid (§6.3). |
last_arv_fpageid / last_arv_lpageid / last_arv_num | First/last pageid and number of the archive being assembled. | Drive logwr_archive_active_log’s copy range and name. |
force_flush / last_flush_time | Force a SEMISYNC flush past the timer; last-flush timestamp. | SEMISYNC throttles to ~1/sec; force_flush (shutdown/crash) bypasses it. |
bg_archive_info / bg_archive_name | Bg-archive scratch state and temp path. | Dual-write when LOG_BACKGROUND_ARCHIVING is on; temp is renamed into the real archive on roll. |
ori_nxarv_pageid | nxarv_pageid snapshot before applying this reply. | Detects the archive boundary so request mode is not downgraded (§6.3). |
start_pageid | -2 normal; >= NULL_PAGEID for -f. | Distinguishes one-shot full copy from the daemon. |
reinit_copylog | Master DB rebuilt under us. | Triggers logwr_reinit_copylog. |
(last_chkpt_pageid is carried for diagnostics, unread by the core loop.)
Invariant — last_recv_pageid is monotonic and bounds the next request.
logwr_set_hdr_and_flush_info sets it to the last received page (more to come)
or hdr.eof_lsa.pageid (caught up); the next request derives first_pageid_torecv
from it (§6.3). logwr_get_log_pages asserts
last_recv_pageid <= hdr.eof_lsa.pageid — a page beyond EOF would make the loop
request non-existent pages and hang on the master’s poll-to-push (§6.8).
LOGWR_CONTEXT — per-session control
Section titled “LOGWR_CONTEXT — per-session control”// struct logwr_context -- src/transaction/log_writer.hstruct logwr_context { int rc; int last_error; bool shutdown; };| Field | Role | Why it exists |
|---|---|---|
rc | Net request handle (> 0 once open). | net_client_logwr_send_end_msg(ctx.rc,...) tears down the server thread. |
last_error | Last error code, echoed to the master. | The master decides whether to keep the writer entry. |
shutdown | Session must end. | Breaks the loop; set on crash, fatal error, or -f completion. |
LOGWR_ENTRY and LOGWR_INFO — the master-side mirror
Section titled “LOGWR_ENTRY and LOGWR_INFO — the master-side mirror”Each connected slave is one LOGWR_ENTRY on writer_info->writer_list:
| Field | Role | Why it exists |
|---|---|---|
thread_p | LOGWR thread serving this slave. | Suspend/resume target. |
fpageid | First pageid this slave is asking for. | Input to logwr_pack_log_pages. |
mode | Slave’s requested LOGWR_MODE. | Decides whether the flush thread waits. |
status | LOGWR_STATUS_* (WAIT/FETCH/DONE/DELAY/ERROR). | Handshake state with the Log Flush Thread. |
eof_lsa | Master EOF snapshot for this entry. | Bounds the pack; set under LOG_CS. |
last_sent_eof_lsa | Highest EOF already shipped. | logwr_is_delayed compares it to detect lag. |
tmp_last_sent_eof_lsa | Staging for last_sent_eof_lsa. | Committed only after the send round-trips. |
start_copy_time | Clock at fetch start. | Measures how long the slave delays the flush thread. |
copy_from_first_phy_page | The -f flag. | Routes the pack to the oldest-page branch. |
next | List linkage. | writer_list is singly linked. |
LOGWR_INFO (log_Gl.writer_info) is the rendezvous between the single Log
Flush Thread (LFT) and the per-slave LOGWR threads (LWTs). Every member:
| Field | Role | Why it exists |
|---|---|---|
writer_list | Head of the LOGWR_ENTRY list. | The set of slaves the LFT must consider waking. |
wr_list_mutex | Serializes mutation of writer_list on register/unregister. | The list is shared between every LWT and the LFT; distinct from the three cond pairs below. |
flush_start_cond / flush_start_mutex | An LWT suspends here awaiting the requested page. | The poll-to-push wait point (§6.8); the LFT signals once pages exist. |
flush_wait_cond / flush_wait_mutex | The LWT waits here after packing, for the flush to finish. | Sequences a packed LWT behind the LFT’s disk flush. |
flush_end_cond / flush_end_mutex | The LFT waits here for LWTs to finish their send. | Lets the LFT block until interested slaves consumed the pages. |
skip_flush | LFT skips waiting on writers this cycle. | Avoids a stall when there is nothing to coordinate. |
flush_completed | LFT finished the current flush. | The predicate LWTs test when woken on flush_wait_cond. |
is_init | writer_info is initialized. | Guards the handshake from running before setup. |
trace_last_writer | Enable last-writer timing instrumentation. | Diagnostics for delay attribution. |
last_writer_client_info | CLIENTIDS of the last delaying writer. | Identifies which slave caused flush latency. |
last_writer_elapsed_time | Time that writer made the LFT wait. | The measured delay, surfaced for HA monitoring. |
6.2 logwr_initialize — opening the slave-local active log
Section titled “6.2 logwr_initialize — opening the slave-local active log”Runs once at daemon start:
// logwr_initialize -- src/transaction/log_writer.cstrncpy (logwr_Gl.db_name, db_name, PATH_MAX - 1);if ((at_char = strchr (logwr_Gl.db_name, '@')) != NULL) { *at_char = '\0'; logwr_Gl.hostname = at_char + 1; /* <- split db@host */}log_nbuffers = LOGWR_COPY_LOG_BUFFER_NPAGES + 1; /* <- 128 data + 1 header */LOGWR_COPY_LOG_BUFFER_NPAGES is LOGPB_BUFFER_NPAGES_LOWER (128), the per-reply
page ceiling. Then, branch by branch:
- Buffer alloc — if
logpg_area == NULL, malloclog_nbuffers * LOG_PAGESIZE; on failure resetlogpg_area_size = 0, returnER_OUT_OF_VIRTUAL_MEMORY. toflushalloc — if NULL, callocmax_toflush = log_nbuffers - 1pointers; same OOM handling.logwr_read_log_header— if the slave active log exists, mount and read page 0; else defer creation (we need the master’snpagesfirst). Propagate errors.- “Already exists” guard —
start_pageid >= NULL_PAGEID(a-f) into a directory that already has replication logs (hdr.nxarv_pageid != NULL_PAGEID) returnsER_HA_GENERIC_ERROR. - Archive cursor seed —
last_arv_fpageid = hdr.nxarv_pageid,last_arv_num = hdr.nxarv_num; resetforce_flush/last_flush_time/ori_nxarv_pageid. - Bg-archiving branch (only if
PRM_ID_LOG_BACKGROUND_ARCHIVING) — mount the temp volume and read its header, recreating onER_LOG_INCOMPATIBLE_DATABASE; if absent,fileio_formata fresh one and flush its header. Mount/format failure returnsER_IO_MOUNT_FAIL/ER_HA_GENERIC_ERROR.
The active log is not created here in the normal case; that is deferred to the
first logwr_write_log_pages (§6.5), which fileio_formats it once it knows hdr.npages.
6.3 logwr_copy_log_file — the daemon main loop
Section titled “6.3 logwr_copy_log_file — the daemon main loop”The copylogdb lifecycle. After logwr_initialize (returning on init failure),
it loops:
flowchart TD
init["logwr_initialize"] -->|ok| loop{shutdown<br/>or need_shutdown?}
init -->|err| fin["logwr_finalize; return"]
loop -->|no| get["logwr_get_log_pages(ctx)<br/>= one request/reply"]
get -->|error| errb{reinit_copylog?}
errb -->|yes| reinit["logwr_reinit_copylog<br/>goto end"]
errb -->|no| clr["action &= DELAYED_WRITE"]
get -->|NO_ERROR| asy{action & ASYNC_WRITE?}
asy -->|yes| wlp["logwr_write_log_pages"]
asy -->|no| clr
wlp --> clr
clr --> loop
loop -->|yes| sig{need_shutdown<br/>and SEMISYNC?}
sig -->|yes| flush["force_flush=true<br/>logwr_write_log_pages"]
sig -->|no| endlbl["end:"]
flush --> endlbl
reinit --> endlbl
endlbl --> endmsg["if rc>0: net_client_logwr_send_end_msg"]
endmsg --> fin
Figure 6-2. logwr_copy_log_file control flow. The network round-trip is inside
logwr_get_log_pages; this loop orchestrates write-back, reinit, and shutdown.
Key branches: a logwr_get_log_pages error records ctx.last_error, and
ER_HA_LW_FAILED_GET_LOG_PAGE deregisters from the heartbeat (unrecoverable);
if reinit_copylog was set, run logwr_reinit_copylog and goto end. On
success, action & LOGWR_ACTION_ASYNC_WRITE triggers an eager
logwr_write_log_pages (SEMISYNC/SYNC write elsewhere). Every turn,
action &= LOGWR_ACTION_DELAYED_WRITE clears all bits except DELAYED_WRITE, which
must persist so the client keeps pulling remaining pages. On signal shutdown
with SEMISYNC it sets force_flush, does a final write, and returns
ER_HA_LW_STOPPED_BY_SIGNAL. At end:, if ctx.rc > 0 it sends the end
message; a completed -f copy deliberately sends ER_FAILED to force the server
thread down; always logwr_finalize.
logwr_get_log_pages (in network_interface_cl.c) derives the request from the
cursor:
// logwr_get_log_pages -- src/communication/network_interface_cl.cif (logwr_Gl.last_recv_pageid == logwr_Gl.hdr.eof_lsa.pageid) { /* caught up */ first_pageid_torecv = logwr_Gl.last_recv_pageid; mode = (logwr_Gl.last_recv_pageid == NULL_PAGEID) ? LOGWR_MODE_ASYNC : logwr_Gl.mode;} else { /* still draining */ if (logwr_Gl.last_recv_pageid == NULL_PAGEID) first_pageid_torecv = LOGPB_HEADER_PAGE_ID; /* <- prove DB identity */ else if (logwr_Gl.hdr.ha_file_status == LOG_HA_FILESTAT_SYNCHRONIZED) first_pageid_torecv = logwr_Gl.last_recv_pageid; /* re-fetch tail page */ else first_pageid_torecv = logwr_Gl.last_recv_pageid + 1; mode = (first_pageid_torecv == logwr_Gl.ori_nxarv_pageid) ? logwr_Gl.mode : LOGWR_MODE_ASYNC;}Three packed fields: int64 first pageid (possibly OR’d with the -f mask), int
mode, int last_error. While draining, mode is forced to ASYNC so the master never
blocks on a catching-up slave; it stays configured only at the archive boundary
(first_pageid_torecv == ori_nxarv_pageid). On ER_NET_SERVER_CRASHED /
ER_HA_LW_FAILED_GET_LOG_PAGE, a SEMISYNC slave force-flushes and sets
ctx->shutdown; the ER_NET_SERVER_CRASHED branch additionally stamps the local
header HA_SERVER_STATE_DEAD before flushing it (the ER_HA_LW_FAILED_GET_LOG_PAGE
branch does not — it leaves the recorded server state untouched).
6.4 logwr_set_hdr_and_flush_info — reconciling a reply
Section titled “6.4 logwr_set_hdr_and_flush_info — reconciling a reply”When a reply lands in logpg_area, this rebuilds toflush, copies the master
header into logwr_Gl.hdr, decides whether to archive, and advances the cursor.
It first walks the data pages (past the first LOG_PAGESIZE header slot):
// logwr_set_hdr_and_flush_info -- src/transaction/log_writer.cp = logwr_Gl.logpg_area + LOG_PAGESIZE; /* <- skip header page */while (p < (logwr_Gl.logpg_area + logwr_Gl.logpg_fill_size)) { logwr_Gl.toflush[num_toflush++] = (LOG_PAGE *) p; p += LOG_PAGESIZE;}logwr_Gl.ori_nxarv_pageid = logwr_Gl.hdr.nxarv_pageid; /* <- snapshot BEFORE overwrite */Branch A — num_toflush > 0 (data arrived): copy wire page 0 into hdr; seed
archive info if unset; set LOGWR_ACTION_ARCHIVING in two sub-cases — delayed
(last_arv_num+1 < hdr.nxarv_num and ha_file_status == ARCHIVED and
last_arv_fpageid <= last_recv_pageid → last_arv_lpageid = last_recv_pageid) or
at-boundary (last_arv_num+1 == hdr.nxarv_num and the last page reached
hdr.nxarv_pageid → last_arv_lpageid = hdr.nxarv_pageid - 1). Then cursor
advance: if the last page is below hdr.eof_lsa.pageid there is more —
last_recv_pageid = last page and set DELAYED_WRITE; else caught up —
last_recv_pageid = hdr.eof_lsa.pageid and clear DELAYED_WRITE.
Branch B — num_toflush == 0 (header-only reply): the master had nothing new.
Rebuild detection — if the server state is non-active, promotion times match,
but db_restore_time differs, the master was restored from a different image:
set reinit_copylog = true and return ER_LOG_DOESNT_CORRESPOND_TO_DATABASE. A
db_creation mismatch returns the same error (a different DB entirely).
Cursor rewind to force re-send of the tail: last_recv_pageid becomes
hdr.append_lsa.pageid - 1 if local status is not SYNCHRONIZED, else
hdr.eof_lsa.pageid - 1.
Tail (both branches): if the local header is not SYNCHRONIZED, the local
append_lsa is pinned to last_recv_pageid with NULL_OFFSET, recording where
the slave’s writable log ends so a restart resumes there (Chapter 10).
6.5 logwr_writev_append_pages and logwr_flush_all_append_pages
Section titled “6.5 logwr_writev_append_pages and logwr_flush_all_append_pages”logwr_write_log_pages (caller) lazily fileio_formats the active log sized
hdr.npages + 1 if absent, then — if LOGWR_ACTION_ARCHIVING is set — calls
logwr_archive_active_log before flushing (archiving the soon-overwritten
page first), then logwr_flush_all_append_pages, then logwr_flush_header_page.
SEMISYNC also gates on a ~1-second timer unless force_flush or mid-archive.
logwr_flush_all_append_pages coalesces toflush into runs that are both
logically and physically contiguous, one logwr_writev_append_pages per run:
// logwr_flush_all_append_pages -- src/transaction/log_writer.cif ((pageid != prv_pageid + 1) || (logwr_to_physical_pageid (pageid) != logwr_to_physical_pageid (prv_pageid) + 1)) { if (logwr_writev_append_pages (&logwr_Gl.toflush[idxflush], i - idxflush) == NULL) return er_errid (); /* not contiguous: flush the run, restart */ need_sync = true; flush_page_count += i - idxflush; idxflush = -1;}The second clause makes the circular log correct: two logically adjacent
pages can straddle the wraparound seam where physical pageid resets to 1, so they
split into two writes. After the loop the trailing run flushes, one
fileio_synchronize makes everything durable (plus the bg-archive volume if
dual-write is on), and toflush is cleared.
logwr_writev_append_pages does the fileio_writes in two phases. The
bg-archive write (if LOG_BACKGROUND_ARCHIVING) validates bg_arv_info->vdes,
backfills any gap via logwr_copy_necessary_log, and writes each page at
phy_pageid = fpageid - start_page_id + 1. The active write computes
phy_pageid = logwr_to_physical_pageid(fpageid) and writes each page at
phy_pageid + i. Both map ER_IO_WRITE_OUT_OF_SPACE to ER_LOG_WRITE_OUT_OF_SPACE
and signal failure by returning to_flush = NULL. The first data page is
checksum-validated before any write. The #ifdef UNSTABLE_TDE_FOR_REPLICATION_LOG
blocks re-encrypt TDE-marked pages and are compiled out in shipping builds — see
the TDE Open Question in Chapter 11.
Invariant — what is written is byte-identical to the master. The wire page
area is copied verbatim; the slave never re-serializes records. The only mutation
is physical placement (logwr_to_physical_pageid) and optional TDE
re-encryption. This is why copylogdb copies pages, not records.
6.6 logwr_to_physical_pageid — circular-log address math
Section titled “6.6 logwr_to_physical_pageid — circular-log address math”The active log is a fixed ring of hdr.npages pages; a monotonically increasing
logical pageid maps to a physical slot:
// logwr_to_physical_pageid -- src/transaction/log_writer.cif (logical_pageid == LOGPB_HEADER_PAGE_ID) /* header page is always slot 0 */ phy_pageid = 0;else { tmp_pageid = logical_pageid - logwr_Gl.hdr.fpageid; if (tmp_pageid >= logwr_Gl.hdr.npages) tmp_pageid %= logwr_Gl.hdr.npages; else if (tmp_pageid < 0) tmp_pageid = (logwr_Gl.hdr.npages - ((-tmp_pageid) % logwr_Gl.hdr.npages)); /* negative-safe */ tmp_pageid++; /* slot 0 reserved -> data starts at 1 */ if (tmp_pageid > logwr_Gl.hdr.npages) tmp_pageid %= logwr_Gl.hdr.npages; phy_pageid = (LOG_PHY_PAGEID) tmp_pageid;}Subtract fpageid (the logical pageid mapping to data slot 1), reduce modulo
npages (negative case keeps an old pageid mapping correctly), +1 to skip the
header slot, reduce once more if +1 pushed past the ring. The -f copy uses
LOGWR_COPY_FROM_FIRST_PHY_PAGE_MASK so the master repacks from the oldest
physical page rather than trusting a stale fpageid.
6.7 logwr_archive_active_log — rolling the active log to an archive
Section titled “6.7 logwr_archive_active_log — rolling the active log to an archive”When logwr_set_hdr_and_flush_info decides to seal an archive, this runs (before
the flush): build an archive header page, open the destination, copy the page
range, update the cursor. Each step falls to goto error on failure.
- Header page — malloc one page; fill
LOG_ARV_HEADER(db_creation,fpageid = last_arv_fpageid,arv_num = last_arv_num,npages = last_arv_lpageid - fpageid + 1). - Destination — bg-archiving reuses
bg_arv_info->vdes; an existing file isfileio_mounted; otherwisefileio_formata new archive sizednpages + 1. - Write header at physical page 0 (
FILEIO_WRITE_NO_COMPENSATE_WRITE). - Copy range — start at
current_page_id(bg) orlast_arv_fpageid, loop tolast_arv_lpageidinLOGPB_IO_NPAGESchunks clamped so a read never crosses the ring seam; each chunkfileio_read_pages(logical-pageid mismatch →ER_LOG_PAGE_CORRUPTED) thenfileio_write_pagesto the archive. - Bg-archive roll (if on) — dismount the temp volume,
fileio_renameit into the real archive name, format a fresh temp, reset and flush its header. - Cursor + header —
last_arv_num++,last_arv_fpageid = last_arv_lpageid + 1; temporarily pinhdr.append_lsa.pageid = last_arv_lpageid(so a concurrent applier never reads an immature active page), flush the header, restoreappend_lsa, emitER_LOG_ARCHIVE_CREATED.
Invariant — archive before overwrite. logwr_write_log_pages runs
logwr_archive_active_log before logwr_flush_all_append_pages, so any slot
about to be clobbered by ring wraparound is already in a durable archive.
Violating the order would lose log on the slave and break point-in-time recovery
(Chapter 10).
6.8 xlogwr_get_log_pages — the master-side responder
Section titled “6.8 xlogwr_get_log_pages — the master-side responder”Each slave is served by a thread spinning in xlogwr_get_log_pages (dispatched
from slogwr_get_log_pages). It mallocs a LOGWR_COPY_LOG_BUFFER_NPAGES * LOG_PAGESIZE send buffer and loops while (true), one request per iteration.
stateDiagram-v2 [*] --> Register : strip -f mask\n logwr_register_writer_entry Register --> Wait : entry->status == WAIT Register --> Delay : entry->status == DELAY Wait --> Timeout : ASYNC and 10s elapsed Timeout --> Register : unregister, continue Wait --> Interrupted : resume due to interrupt Interrupted --> Register : if delayed, continue Wait --> Pack : THREAD_LOGWR_RESUMED Delay --> Pack : LOG_CS_ENTER Pack --> Send : logwr_pack_log_pages ok Send --> NextReq : xlog_send_log_pages_to_client NextReq --> Register : xlog_get_page_request_with_reply Pack --> Err : pack failed Err --> [*]
Figure 6-3. xlogwr_get_log_pages per-request state machine. Wait is the
poll-to-push block: the thread sleeps until the Log Flush Thread wakes it because
the requested page now exists.
The poll-to-push suspend on entry->status == WAIT:
// xlogwr_get_log_pages -- src/transaction/log_writer.cif (mode == LOGWR_MODE_ASYNC) { timeout = LOGWR_THREAD_SUSPEND_TIMEOUT; /* 10s */ to.tv_sec = time (NULL) + timeout; to.tv_nsec = 0; }else { timeout = INF_WAIT; to.tv_sec = to.tv_nsec = 0; } /* SYNC/SEMISYNC block forever */rv = thread_suspend_with_other_mutex (thread_p, &writer_info->flush_start_mutex, timeout, &to, THREAD_LOGWR_SUSPENDED);A SYNC/SEMISYNC writer waits indefinitely until the LFT appends and signals; an
ASYNC writer waits at most 10 s, then on ER_CSS_PTHREAD_COND_TIMEDOUT
unregisters and continues. Other branches: interrupt/shutdown → ER_INTERRUPTED;
THREAD_RESUME_DUE_TO_INTERRUPT + logwr_is_delayed loops to ship the backlog;
mutex error → ER_FAILED. A LOGWR_STATUS_DELAY entry skips the wait and packs
immediately. After packing it waits on flush_wait_cond, sends via
xlog_send_log_pages_to_client, then xlog_get_page_request_with_reply blocks for
the next request. ASYNC entries unregister before the send, SYNC after; a -f mask
on a subsequent request is illegal (assert_release(false)).
logwr_pack_log_pages chooses the range: is_hdr_page_only packs only page 0;
copy_from_first_phy_page (-f) starts at logpb_find_oldest_available_page_id;
normally fpageid = entry->fpageid clamped to the unflushed boundary
nxio_lsa.pageid. Then the active-vs-archive branch:
// logwr_pack_log_pages -- src/transaction/log_writer.cif (!logpb_is_page_in_archive (fpageid)) /* still in active log */ lpageid = eof_lsa.pageid;else { /* already archived */ logpb_fetch_from_archive (thread_p, fpageid, NULL, NULL, &arvhdr, false); lpageid = arvhdr.fpageid + arvhdr.npages - 1; /* bounded by that archive */ if (fpageid == arvhdr.fpageid) ha_file_status = LOG_HA_FILESTAT_ARCHIVED;}if (((size_t)(lpageid - fpageid + 1)) > (LOGWR_COPY_LOG_BUFFER_NPAGES - 1)) lpageid = fpageid + (LOGWR_COPY_LOG_BUFFER_NPAGES - 1) - 1; /* cap at 127 pages */The archive lookup inside logpb_fetch_from_archive is where the guess
estimator and the archive-page cache live: logpb_get_archive_num_from_info_table
checks the in-memory logpb_Arv_page_info_table (a ring of ARV_PAGE_INFO
start/end/arv_num triples) for an O(1) hit, falling back to
logpb_get_guess_archive_num which interpolates from the loginfo file — locating
which archive holds a far-behind slave’s page without scanning all of them.
Packing fills page 0, loops fpageid..lpageid via logpb_copy_page_from_log_buffer
(or _from_file on an interrupted send), recomputes the checksum of still-mutable
pages at/after nxio_lsa.pageid (logpb_set_page_checksum) and validates
already-flushed ones. *status is DONE if EOF was included, else DELAY.
logwr_check_page_checksum is a sampled CRC: per 4096-byte block it CRCs the
first and last 16 bytes against hdr.checksum (a stored 0 means “unset”) — cheap
on the hot path while still catching torn or bit-rotted pages.
6.9 Chapter summary — key takeaways
Section titled “6.9 Chapter summary — key takeaways”copylogdbships pages, not records. The slave copies the master’s log pages byte-for-byte; record parsing is theapplylogdbside (Chapters 7–9).- Two disjoint struct families share one file.
LOGWR_GLOBAL/LOGWR_CONTEXT(CS_MODE) never coexist withLOGWR_INFO/LOGWR_ENTRY(SERVER_MODE); onlyLOGWR_MODEcrosses the wire. last_recv_pageidis the durable cursor.logwr_set_hdr_and_flush_infoadvances it; the next request derives from it, guarded by the monotonicity assert inlogwr_get_log_pages.- The circular log forces two-axis contiguity checks.
logwr_flush_all_append_pagessplits a run wherever logical or physical adjacency breaks, becauselogwr_to_physical_pageidwraps athdr.npageswith slot 0 pinned to the header. - Archive before overwrite.
logwr_write_log_pagesrunslogwr_archive_active_logahead of the flush, sealing any page ring wraparound is about to clobber — the invariant behind slave-side point-in-time recovery. - The master responder is poll-to-push.
xlogwr_get_log_pagessuspends a SYNC/SEMISYNC writer onflush_startindefinitely (ASYNC: 10 s) until the LFT signals; far-behind slaves are accelerated bylogpb_Arv_page_info_tableandlogpb_get_guess_archive_num. - Checksums are sampled, not full-page.
logwr_check_page_checksumCRCs the first/last 16 bytes of each 4 KB block, validating flushed pages whilelogpb_set_page_checksumrecomputes still-mutable tail pages before send.
Chapter 7: applylogdb Initialization and Log Fetch
Section titled “Chapter 7: applylogdb Initialization and Log Fetch”When an HA node is told to apply the log it copied, the apply daemon must
bootstrap one global state object, recover where it left off from the durable
_db_ha_apply_info bookmark, and read raw log pages off the slave-local
copied volumes. The theory (log shipping, master/slave split, the
copylogdb/applylogdb pair) lives in the high-level companion
cubrid-ha-replication.md; this chapter traces only the mechanism. Chapters
8-10 consume what is set up here.
Everything hangs off two file-scope globals — la_Info (LA_INFO) and
la_recdes_pool (LA_RECDES_POOL). There is exactly one of each (the daemon
is single-threaded per database), so the init functions write directly into
them rather than threading a context pointer.
7.1 The struct map
Section titled “7.1 The struct map”flowchart TD info["LA_INFO la_Info<br/>(the single global)"] act["LA_ACT_LOG act_log"] arv["LA_ARV_LOG arv_log"] filt["LA_REPL_FILTER repl_filter"] pb["LA_CACHE_PB *cache_pb"] ht["MHT_TABLE *hash_table<br/>pageid to buffer"] arr["LA_CACHE_BUFFER **log_buffer"] area["LA_CACHE_BUFFER_AREA *buffer_area"] buf["LA_CACHE_BUFFER<br/>fix_count + LOG_PAGE"] info --> act info --> arv info --> filt info --> pb pb --> ht pb --> arr pb --> area arr -.points into.-> buf area -.owns storage of.-> buf
Figure 7-1. LA_INFO and the caches it owns.
LA_INFO — the global apply context. Fields group into log-volume info,
the in-flight apply map, slave-side scratch, master-side cache, and the
persisted _db_ha_apply_info mirror.
| Field | Role | Why it exists |
|---|---|---|
log_path / loginf_path | Copied-volume dir; *_lginf info-file path | All volume names derive from these; archive-removal bookkeeping |
act_log / arv_log | Active / archive volume handles | See LA_ACT_LOG, LA_ARV_LOG |
last_file_state | Last ha_file_status | Detects format/sync transitions |
start_vsize / start_time | Startup VSZ / wall-clock | max_mem_size watchdog; archive-removal clock |
final_lsa | Read cursor — last processed LSA | Seeded from bookmark; advanced by apply loop |
committed_lsa / committed_rep_lsa | LSA of last applied LOG_COMMIT / repl record | Durable apply watermark (Ch 10); repl-record progress |
last_committed_lsa / last_committed_rep_lsa | Startup snapshots | Restart skips already-applied work |
repl_lists / repl_cnt / cur_repl | Per-txn LA_APPLY buckets, count, cursor | In-flight buffering (Ch 8); grown LA_REPL_LIST_COUNT (50) at a time |
total_rows / prev_total_rows / log_record_time | Row counters; server time of last commit | Throughput and replication-delay reporting |
commit_head / commit_tail | FIFO of pending LA_COMMIT | Orders commit application (Ch 9) |
last_deleted_archive_num / last_time_archive_deleted | Highest archive purged, when | Drive/throttle la_remove_archive_logs |
log_data / rec_type | Record-unpack scratch | rec_type = 2-byte slotted type; lazily alloc in la_apply_pre |
undo_unzip_ptr / redo_unzip_ptr | LOG_ZIP decompress scratch | Compressed undo/redo; alloc in la_apply_pre |
apply_state | HA_LOG_APPLIER_STATE_* | Reported to master; gates dbname-lock release |
max_mem_size | VSZ ceiling (MB) | Daemon self-restarts if exceeded |
cache_pb / cache_buffer_size | Page cache; buffers per expansion | See LA_CACHE_PB; size = LA_DEFAULT_CACHE_BUFFER_SIZE (100) |
last_is_end_of_record / is_end_of_record | EOF-of-log flags | ”Caught up” vs. “page missing” |
last_server_state / is_role_changed | Master HA-state tracking | Role-change handling (Ch 11) |
append_lsa / eof_lsa | Active-log header mirror | Bound the cursor; persisted |
required_lsa / required_lsa_changed | Start LSA of oldest un-applied txn; dirty flag | Reclaim floor: archives below may be purged; flag avoids needless catalog writes |
insert/update/delete/schema/commit/fail_counter / log_commit_time | Op statistics; last-commit time | Persisted to _db_ha_apply_info; delay reporting |
status / is_apply_info_updated | Row status, mid-update flag | IDLE/BUSY; crash-mid-update hint |
num_unflushed | Buffered-but-unflushed items | Flush at LA_MAX_UNFLUSHED_REPL_ITEMS (200) |
log_path_lockf_vdes / db_lockf_vdes | Path-lock / dbname-lock fds | Dup-daemon guard (7.4) vs. apply-time lock |
repl_filter / reinit_copylog | Table filter; “copylogdb must re-init” signal | See LA_REPL_FILTER (Ch 11); set when archive read sees reset volume |
tde_sock_for_dks / maxslotted_reclength | TDE data-key socket; heap_Maxslotted_reclength | Only under UNSTABLE_TDE_FOR_REPLICATION_LOG (Ch 11); row-reconstruction sizing (Ch 9) |
Invariant —
final_lsais the single read cursor and never moves backward within a run.la_apply_prere-seeds it fromcommitted_lsaeach outer iteration; the inner loop only advances it. The fetch layer refuses any page whoseappend_lsa.pageid < pageid, so it can never point past what the master produced — seeding it ahead ofeof_lsawould spinla_get_page_bufferforever.
LA_CACHE_PB / LA_CACHE_BUFFER / LA_CACHE_BUFFER_AREA — a hash table over a flat pool of fixed-size buffers, storage carved from one or more slabs.
LA_CACHE_PB field | Role | Why it exists |
|---|---|---|
hash_table | MHT_TABLE LOG_PAGEID to LA_CACHE_BUFFER* | O(1) lookup of a fetched page |
log_buffer | Buffer-pointer array | Clock victim scan iterates it |
num_buffers | Length of log_buffer | Grows when no victim found |
buffer_area | Head of slab list | Freed wholesale at shutdown |
LA_CACHE_BUFFER field | Role | Why it exists |
|---|---|---|
fix_count | Pin count | > 0 = in use, cannot evict |
recently_free | Second-chance bit | Set on release; cleared on first clock pass |
in_archive | Provenance flag | True if filled from an archive volume |
pageid | Logical page id of contents | Hash key; 0 = empty slot |
phy_pageid | Physical page in volume | From la_log_phypageid |
logpage | LOG_PAGE payload | The page image; SIZEOF_LA_CACHE_LOG_BUFFER(io_pagesize) |
LA_CACHE_BUFFER_AREA (one slab): the area header is followed contiguously by
its buffers, and next links to the previous slab — singly-linked so
expansion never moves existing buffers.
Invariant — every live buffer is reachable by
log_buffer[]index and (ifpageid != 0) byhash_tablekey, and the two agree.la_cache_buffer_replacecallsmht_remon a victim’s oldpageidbefore reuse;la_get_page_bufferre-inserts under the newpageid. A stale key would letmht_getreturn wrong contents — silent corruption.
LA_RECDES_POOL — RECDES descriptors plus one contiguous arena, allocated
once to avoid per-record malloc during apply.
| Field | Role | Why it exists |
|---|---|---|
recdes_arr | RECDES array | Handed out round-robin in row reconstruction |
area | page_size * num_recdes arena | Backs all recdes_arr[i].data |
next_idx | Round-robin cursor | Reset to 0 each apply pass |
db_page_size | Per-recdes capacity | Detects page-size change needing re-init |
num_recdes | Pool size | LA_MAX_UNFLUSHED_REPL_ITEMS (200) at call site |
is_initialized | Guard | Makes la_init_recdes_pool idempotent |
LA_ACT_LOG / LA_ARV_LOG — volume handles.
LA_ACT_LOG field | Role | Why it exists |
|---|---|---|
path[PATH_MAX] | Active-log path | From fileio_make_log_active_name |
log_vdes | Open fd | NULL_VOLDES until opened |
hdr_page | Header-page buffer | Re-read each loop to track master |
log_hdr | LOG_HEADER* into hdr_page->area | All append_lsa/nxarv_* reads |
db_iopagesize | IO page size from header | Sizes decompress/recdes scratch |
db_logpagesize | Log page size from header | Sizes every cache buffer; 4096 until header read |
LA_ARV_LOG field | Role | Why it exists |
|---|---|---|
path[PATH_MAX] | Archive path | Built per archive number on demand |
log_vdes | Open archive fd | Reopened when archive number changes |
hdr_page | Archive header buffer | Lazily malloc-ed on first archive read |
log_hdr | LOG_ARV_HEADER* into hdr_page | Gives fpageid/npages |
arv_num | Archive number open | Compared against needed number to reopen |
LA_REPL_FILTER (list/list_size/num_filters/type) is the
include/exclude table list; its mechanics are Ch 11, so only its
initialization to REPL_FILTER_NONE appears here.
7.2 Entry: cubrid hb start to la_apply_log_file
Section titled “7.2 Entry: cubrid hb start to la_apply_log_file”cubrid heartbeat start spawns applylogdb; its main lands in util_cs.c,
which calls la_apply_log_file (database_name, log_path, max_mem_size) in
log_applier.c. That call never returns under normal operation — it is the
daemon. Its prologue is a fixed sequence: install signal handlers and set
slave db-name/peer host; la_init; optionally sl_init if
PRM_ID_HA_SQL_LOGGING; la_check_duplicated (path lock — error returns);
la_init_cache_pb (NULL = OUT_OF_MEMORY); la_find_log_pagesize (open
active vol, read header); only then la_init_cache_log_buffer with the now-real
db_logpagesize; la_init_recdes_pool (db_iopagesize, 200);
la_get_last_ha_applied_info; committed_lsa := required_lsa; enter the apply
loop (Ch 8+).
Two ordering facts break easily in a refactor:
- Cache buffers cannot be allocated before the header is read.
la_init_cache_pballocates only the container (LA_CACHE_PB, fields NULL/0). Per-buffer size needsdb_logpagesize, unknown untilla_find_log_pagesize, sola_init_cache_log_bufferruns after it. la_check_duplicatedruns before any volume work, so a second daemon is rejected before it can race on the cache.
7.3 la_init — zeroing and defaulting the global
Section titled “7.3 la_init — zeroing and defaulting the global”la_init is pure state-setting, no failure path except a one-time VSZ probe.
Field semantics are in the 7.1 table; only non-obvious intent is annotated:
// la_init -- src/transaction/log_applier.cmemset (&la_Info, 0, sizeof (la_Info)); /* <- everything zeroed */strncpy (la_Info.log_path, log_path, PATH_MAX - 1);la_Info.cache_buffer_size = LA_DEFAULT_CACHE_BUFFER_SIZE; /* 100 */la_Info.act_log.db_iopagesize = LA_DEFAULT_LOG_PAGE_SIZE; /* 4096, provisional */la_Info.act_log.db_logpagesize = LA_DEFAULT_LOG_PAGE_SIZE;la_Info.act_log.log_vdes = la_Info.arv_log.log_vdes = NULL_VOLDES; /* no volume open */LSA_SET_NULL (&la_Info.committed_lsa); /* all cursors start NULL */// ... committed_rep_lsa, append_lsa, eof_lsa, required_lsa, final_lsa,// last_committed_lsa, last_committed_rep_lsa all SET_NULL ...la_Info.last_deleted_archive_num = -1; /* nothing purged yet */la_Info.max_mem_size = max_mem_size;if (!start_vsize) start_vsize = la_get_mem_size (); /* probe VSZ once per process */la_Info.start_vsize = start_vsize;la_recdes_pool.is_initialized = false; /* force pool (re)alloc */la_Info.repl_filter.type = REPL_FILTER_NONE;la_Info.reinit_copylog = false;The db_logpagesize = 4096 is a placeholder; anything sized by it before
la_find_log_pagesize would silently use 4096 — which is why
la_init_cache_pb allocates no buffers. The defaults exist only so the first
header read can malloc its provisional hdr_page. The repl_lists array is
not touched here; la_init_repl_lists lazily builds it, LA_REPL_LIST_COUNT
(50) buckets at a time.
7.4 la_apply_pre and the path lock
Section titled “7.4 la_apply_pre and the path lock”Pre-apply setup is split: la_check_duplicated (one-time, prologue) and
la_apply_pre (top of every outer iteration).
la_check_duplicated guards against a duplicate daemon. It builds
"<logpath>/<dbname>_lgla__lock" (LA_LOCK_SUFFIX = "_lgla__lock"), opens
it O_RDWR|O_CREAT, then write-locks via fileio_lock_la_log_path:
// la_check_duplicated -- src/transaction/log_applier.c*lockf_vdes = fileio_open (lock_path, O_RDWR | O_CREAT, 0644);if (*lockf_vdes == NULL_VOLDES) return ER_IO_MOUNT_FAIL; /* branch: cannot open */lockf_type = fileio_lock_la_log_path (dbname, lock_path, *lockf_vdes, last_deleted_arv_num);if (lockf_type == FILEIO_NOT_LOCKF) { /* branch: another daemon holds it */ fileio_close (*lockf_vdes); *lockf_vdes = NULL_VOLDES; return ER_FAILED;}return NO_ERROR; /* branch: lock acquired */The lock file also stores last_deleted_arv_num durably —
fileio_lock_la_log_path reads the integer back out, which is why the fd is
kept in log_path_lockf_vdes for later la_update_last_deleted_arv_num
writes. The dbname lock (db_lockf_vdes, via la_lock_dbname) is separate —
held only while actively applying.
Invariant — at most one applylogdb per (log_path, dbname) holds the path lock. Two daemons would double-apply rows and corrupt
committed_lsa. The OS write-lock enforces single-writer; the loser exits viaFILEIO_NOT_LOCKF.
la_apply_pre seeds the per-iteration cursor and lazily allocates scratch:
// la_apply_pre -- src/transaction/log_applier.cLSA_COPY (&la_Info.final_lsa, &la_Info.committed_lsa); /* <- restart cursor at the watermark */if (la_Info.rec_type == NULL) { /* 2-byte slotted rectype scratch, once */ la_Info.rec_type = (char *) malloc (DB_SIZEOF (INT16)); if (la_Info.rec_type == NULL) return false; /* branch: OOM */}if (la_Info.undo_unzip_ptr == NULL) { /* LOG_ZIP undo scratch */ la_Info.undo_unzip_ptr = log_zip_alloc (la_Info.act_log.db_iopagesize); if (la_Info.undo_unzip_ptr == NULL) return false; /* branch: OOM */}if (la_Info.redo_unzip_ptr == NULL) { /* LOG_ZIP redo scratch */ la_Info.redo_unzip_ptr = log_zip_alloc (la_Info.act_log.db_iopagesize); if (la_Info.redo_unzip_ptr == NULL) return false; /* branch: OOM */}return true;The three == NULL guards make allocation idempotent across iterations. The
first line — re-pointing final_lsa at committed_lsa — is how the loop
resumes from the last durable commit after a transient stall.
7.5 Seeding final_lsa from the bookmark
Section titled “7.5 Seeding final_lsa from the bookmark”la_get_last_ha_applied_info reads the _db_ha_apply_info row (via
la_get_ha_apply_info) and copies its persisted LSAs into la_Info.
flowchart TD
A["la_get_last_ha_applied_info"] --> B["la_get_ha_apply_info -> res"]
B --> C{"res > 0?<br/>row found"}
C -->|yes| D["LSA_COPY committed/rep/append/eof/final/required + counters"]
D --> E{"db_creation matches header?"}
E -->|no| Rx["return ER_FAILED"]
E -->|yes| F{"required_lsa NULL in row?"}
F -->|yes| Rx2["return ER_FAILED"]
F -->|no| G
C -->|res == 0| H["insert_apply_info := true"]
C -->|res < 0| Rz["return res"]
H --> G["NULL-fallback fixups"]
G --> G1["required NULL -> eof; committed/rep/final NULL -> required"]
G1 --> I{"insert_apply_info?"}
I -->|yes| J["la_insert_ha_apply_info"]
I -->|no| K["la_update_ha_apply_info_start_time"]
J --> L
K --> L{"res == 0?"}
L -->|yes| Rw["return ER_FAILED"]
L -->|res < 0| Rv["return res"]
L -->|ok| M["db_commit_transaction"]
M --> N["last_committed_lsa/rep_lsa := committed_lsa/rep_lsa"]
N --> O["return NO_ERROR"]
Figure 7-2. la_get_last_ha_applied_info, all branches. The NULL-fallback fixups (G/G1) are reached from both the row-found and the fresh-row paths.
The row-found (res > 0) path copies six LSAs and six counters off the row,
then guards two ways: a db_creation mismatch means the bookmark belongs to
a different database incarnation, so it returns ER_FAILED; a NULL
required_lsa in an existing row is also fatal (ER_FAILED) — a written row
must have a floor. A fresh database (res == 0) skips the copy and only sets
insert_apply_info. The NULL-fallback block then runs on both surviving
paths, not just the fresh one: required_lsa NULL falls to the header’s
eof_lsa, then each of committed_lsa / committed_rep_lsa / final_lsa
NULL falls to required_lsa. (On the row-found path required_lsa is already
non-NULL — the guard above caught NULL — so the fallback only ever patches the
three cursors there.) The row is then inserted or its start-time updated; only
on success does db_commit_transaction persist it. Finally
last_committed_lsa/last_committed_rep_lsa snapshot the startup watermark so
Ch 8 can skip records already applied.
Back in la_apply_log_file, one line completes the seed:
LSA_COPY (&la_Info.committed_lsa, &la_Info.required_lsa). Combined with
la_apply_pre’s final_lsa := committed_lsa, the first read cursor lands on
required_lsa — the oldest transaction still needing apply.
7.6 Allocating the caches
Section titled “7.6 Allocating the caches”la_init_cache_pb is trivial: malloc the LA_CACHE_PB, NULL every field,
return it (NULL on OOM). No buffers yet.
la_init_cache_log_buffer allocates the first slab, then the hash table:
// la_init_cache_log_buffer -- src/transaction/log_applier.cerror = la_expand_cache_log_buffer (cache_pb, slb_cnt, slb_size); /* slb_cnt=100 buffers */if (error != NO_ERROR) return error; /* branch: OOM */cache_pb->hash_table = mht_create ("...", cache_pb->num_buffers * 8, /* <- 8x headroom */ log_pageid_hash, mht_compare_logpageids_are_equal);if (cache_pb->hash_table == NULL) error = ER_OUT_OF_VIRTUAL_MEMORY;return error;la_expand_cache_log_buffer does one malloc covering the
LA_CACHE_BUFFER_AREA header plus slb_cnt contiguous buffers, then
reallocs log_buffer[]. The slab is prepended (area->next = cache_pb->buffer_area) and existing buffers never move, so expansion is safe
while pages are pinned. log_pageid_hash special-cases LOGPB_HEADER_PAGE_ID
to bucket 0.
la_init_recdes_pool is idempotent and self-healing on size change:
// la_init_recdes_pool -- src/transaction/log_applier.cif (la_recdes_pool.is_initialized == false) { la_recdes_pool.area = malloc (page_size * num_recdes); /* one arena + OOM branches */ la_recdes_pool.recdes_arr = malloc (sizeof (RECDES) * num_recdes); p = la_recdes_pool.area; for (i = 0; i < num_recdes; i++) { recdes = &la_recdes_pool.recdes_arr[i]; recdes->data = p; recdes->area_size = page_size; recdes->length = 0; /* carve from arena */ p += page_size; } la_recdes_pool.is_initialized = true;}else if (la_recdes_pool.db_page_size != page_size /* branch: size changed */ || la_recdes_pool.num_recdes != num_recdes) { la_clear_recdes_pool (); return la_init_recdes_pool (page_size, num_recdes); /* recurse once with new size */}la_recdes_pool.next_idx = 0; /* always reset cursor */7.7 The log-fetch layer
Section titled “7.7 The log-fetch layer”Above the cache: la_get_page -> la_get_page_buffer ->
(la_cache_buffer_replace -> la_log_fetch). la_get_page loops on
la_get_page_buffer until it succeeds and returns &cache_buffer->logpage.
la_get_page_buffer — hash lookup, fetch-on-miss, pin:
// la_get_page_buffer -- src/transaction/log_applier.ccache_buffer = (LA_CACHE_BUFFER *) mht_get (cache_pb->hash_table, &pageid);if (cache_buffer == NULL) { /* MISS */ cache_buffer = la_cache_buffer_replace (cache_pb, pageid, la_Info.act_log.db_logpagesize, la_Info.cache_buffer_size); if (cache_buffer == NULL || cache_buffer->logpage.hdr.logical_pageid != pageid) /* fetch failed / wrong page */ return NULL; (void) mht_rem (cache_pb->hash_table, &cache_buffer->pageid, NULL, NULL); /* drop stale key */ if (mht_put (cache_pb->hash_table, &cache_buffer->pageid, cache_buffer) == NULL) return NULL; /* hash insert failed */}else if (cache_buffer->logpage.hdr.logical_pageid != pageid) { /* stale HIT -> evict, retry */ (void) mht_rem (cache_pb->hash_table, &cache_buffer->pageid, NULL, NULL); return NULL;}cache_buffer->fix_count++; /* <- pin before returning */return cache_buffer;The “stale hit” branch (found in the hash but contents no longer match) returns NULL so the caller refetches; every successful return pins.
Release vs. invalidate. la_release_page_buffer unpins and keeps the
page, arming the second-chance bit:
// la_release_page_buffer -- src/transaction/log_applier.ccache_buffer = (LA_CACHE_BUFFER *) mht_get (cache_pb->hash_table, (void *) &pageid);if (cache_buffer != NULL) { if ((--cache_buffer->fix_count) <= 0) cache_buffer->fix_count = 0; /* <- clamp, never negative */ cache_buffer->recently_free = true; /* <- the bit Figure 7-3's clock reads */}la_invalidate_page_buffer evicts — drops the hash key, clears pin and
bit, zeroes identity so the slot reads empty:
// la_invalidate_page_buffer -- src/transaction/log_applier.cif (cache_buffer->pageid != 0) (void) mht_rem (cache_pb->hash_table, &cache_buffer->pageid, NULL, NULL); /* drop old key */cache_buffer->fix_count = 0;cache_buffer->recently_free = false; /* <- it is gone, not a candidate */cache_buffer->pageid = 0;la_cache_buffer_replace — clock-style victim finder feeding la_log_fetch:
flowchart TD
A["la_cache_buffer_replace"] --> B["found = -1"]
B --> C["scan log_buffer[] from 'last'"]
C --> D{"fix_count == 0?"}
D -->|no, pinned| C
D -->|yes| E{"recently_free?"}
E -->|yes| F["clear bit, num_recently_free++"]
F --> C
E -->|no| G["found = slot; break"]
C -->|scanned all| H{"found >= 0?"}
G --> H
H -->|yes| I["if pageid!=0 mht_rem old key<br/>fix_count=0; la_log_fetch"]
I --> J{"fetch ok?"}
J -->|no| K["victim->pageid=0; return NULL"]
J -->|yes| L["return victim"]
H -->|no| M{"num_recently_free > 0?"}
M -->|yes| C
M -->|no| N["la_expand_cache_log_buffer"]
N --> O{"expand ok?"}
O -->|no| P["return NULL"]
O -->|yes| C
Figure 7-3. la_cache_buffer_replace victim selection.
The static unsigned int last is the clock hand, persisted across calls. A
recently_free buffer gets one reprieve (bit cleared, num_recently_free++ —
exactly the bit la_release_page_buffer set); only a fix_count == 0,
not-recently-free buffer is chosen. A sweep that found no victim but cleared
bits loops again; a sweep that found and cleared nothing (all pinned) grows
the pool and retries. The victim’s old key is removed before la_log_fetch
overwrites it.
la_log_fetch reads the physical page, choosing active vs. archive:
// la_log_fetch -- src/transaction/log_applier.cphy_pageid = la_log_phypageid (pageid);if (la_Info.act_log.log_hdr->append_lsa.pageid < pageid) { /* page beyond what master wrote? */ error = la_fetch_log_hdr (&la_Info.act_log); /* re-read header, recheck */ if (error != NO_ERROR) return error; if (la_Info.act_log.log_hdr->append_lsa.pageid < pageid) return ER_LOG_NOTIN_ARCHIVE; /* genuinely not there yet */}do { if (LA_LOG_IS_IN_ARCHIVE (pageid)) { /* pageid < nxarv_pageid */ error = la_log_fetch_from_archive (pageid, (char *) &cache_buffer->logpage); if (error != NO_ERROR) { la_applier_need_shutdown = true; return error; } cache_buffer->in_archive = true; } else { error = la_log_io_read (la_Info.act_log.path, la_Info.act_log.log_vdes, &cache_buffer->logpage, phy_pageid, la_Info.act_log.db_logpagesize); if (error != NO_ERROR) return ER_LOG_READ; cache_buffer->in_archive = false; } if (cache_buffer->logpage.hdr.logical_pageid == pageid) break; /* got the right page */ usleep (100 * 1000); /* master may be archiving; retry */ error = la_fetch_log_hdr (&la_Info.act_log); if (error != NO_ERROR) return error;} while (error == NO_ERROR && --retry > 0); /* up to 5 tries */if (retry <= 0 || la_Info.act_log.log_hdr->append_lsa.pageid < pageid) return ER_LOG_NOTIN_ARCHIVE;cache_buffer->pageid = pageid; /* <- stamp identity only on success */cache_buffer->phy_pageid = phy_pageid;return error;LA_LOG_IS_IN_ARCHIVE(pageid) tests pageid < nxarv_pageid — pages below the
next-archive boundary are already in an archive volume.
la_log_fetch_from_archive finds the archive number (la_find_archive_num),
reopens the fd if it differs from arv_log.arv_num, reads the header, then the
page at offset pageid - fpageid + 1; its ER_PB_BAD_PAGEID branch gotos
log_reopen to recover a stale fd. The retry loop absorbs the race where the
master archives a page between the test and the read.
Invariant —
cache_buffer->pageidis stamped only after a verified read. On any failure it stays0(or is reset byla_cache_buffer_replace’s error branch), so a half-filled buffer is never published into the hash table.
7.8 Cross-page record copy: la_log_copy_fromlog
Section titled “7.8 Cross-page record copy: la_log_copy_fromlog”Records straddle page boundaries. la_log_copy_fromlog copies a record’s type
word and data out of the cached pages, hopping pages via the LA_LOG_READ_*
macros:
// la_log_copy_fromlog -- src/transaction/log_applier.cint rec_length = (int) sizeof (INT16); /* the 2-byte slotted rectype */pg = log_pgptr;while (rec_type != NULL && rec_length > 0) { /* phase 1: copy the rectype */ LA_LOG_READ_ADVANCE_WHEN_DOESNT_FIT (error, 0, log_offset, log_pageid, pg); /* hop if at edge */ if (pg == NULL) break; /* defensive: page vanished */ copy_length = (log_offset + rec_length <= LA_LOGAREA_SIZE) ? rec_length : LA_LOGAREA_SIZE - log_offset; /* clip to page end */ memcpy (rec_type + area_offset, (char *) pg->area + log_offset, copy_length); rec_length -= copy_length; area_offset += copy_length; log_offset += copy_length; *length = *length - DB_SIZEOF (INT16); /* rectype is not part of the data */}area_offset = 0; t_length = *length;while (t_length > 0) { /* phase 2: copy the record data */ LA_LOG_READ_ADVANCE_WHEN_DOESNT_FIT (error, 0, log_offset, log_pageid, pg); if (pg == NULL) break; copy_length = (log_offset + t_length <= LA_LOGAREA_SIZE) ? t_length : LA_LOGAREA_SIZE - log_offset; memcpy (area + area_offset, (char *) pg->area + log_offset, copy_length); t_length -= copy_length; area_offset += copy_length; log_offset += copy_length;}LA_LOG_READ_ADVANCE_WHEN_DOESNT_FIT checks whether offset + length >= LA_LOGAREA_SIZE — the usable bytes per page, defined as
la_Info.act_log.db_logpagesize - SSIZEOF(LOG_HDRPAGE) — and if so fetches the
next page via la_get_page (++pageid), resetting offset to 0. Because
la_get_page recurses into la_get_page_buffer, it pins the next page;
those pins are released later by la_release_all_page_buffers. Siblings
LA_LOG_READ_ALIGN / LA_LOG_READ_ADD_ALIGN step record-to-record in Ch 8.
Invariant —
*lengthexcludes the rectype after phase 1. Phase 1 decrements*lengthbysizeof(INT16)so phase 2 copies exactly the data. Withrec_type == NULL(overflow pages) phase 1 is skipped, so*lengthstays the full data length.
7.9 Chapter summary — key takeaways
Section titled “7.9 Chapter summary — key takeaways”- One global, two init phases.
la_initzeroesla_Infowith only provisional page sizes; cache buffers and the recdes pool cannot be sized untilla_find_log_pagesizereads the realdb_logpagesize.la_init_cache_pballocates only the empty container in between. - The path lock (
_lgla__lock,LA_LOCK_SUFFIX) is the single-writer guard.la_check_duplicatedrejects a second daemon viafileio_lock_la_log_path; the same file persistslast_deleted_archive_num. The dbname lock is a separate, apply-time-only lock. final_lsais the read cursor, re-seeded fromcommitted_lsaeach outer iteration byla_apply_pre. Startup chainsrequired_lsa -> committed_lsa -> final_lsa.- The bookmark drives recovery.
la_get_last_ha_applied_infocopies six LSAs and six counters from the row, guards a mismatcheddb_creationincarnation (and a NULLrequired_lsain an existing row), applies NULL-fallbacks on both the row-found and fresh-row paths so every cursor defaults down throughrequired_lsato the headereof_lsa, and snapshots the restart watermark. - The page cache is a hash table over a clock-replaced flat pool.
la_get_page_bufferpins (fix_count++);la_release_page_bufferunpins and armsrecently_free;la_invalidate_page_bufferevicts;la_cache_buffer_replaceconsumesrecently_freeas a second chance, growing the pool when all buffers are pinned. la_log_fetchchooses active vs. archive by thenxarv_pageidboundary (LA_LOG_IS_IN_ARCHIVE), re-reads the header and retries up to 5 times to absorb the master-archiving race, and stampspageidonly after a verified read.- Records straddle pages; the
LA_LOG_READ_*macros plusla_log_copy_fromloghop boundaries transparently, subtracting the 2-byte rectype in phase 1 so phase 2 copies exactly the payload.
Chapter 8: Per Record Dispatch and Per Transaction Buffering
Section titled “Chapter 8: Per Record Dispatch and Per Transaction Buffering”This chapter dissects the inner loop of applylogdb: for every log record the daemon walks forward over, la_log_record_process decides what kind of record it is and where its effects are stashed until the originating transaction’s commit arrives. The high-level companion (cubrid-ha-replication.md, “applylogdb apply pipeline”) explains why the slave buffers items per transaction and replays them in commit order; this chapter traces how, branch by branch. The caller (Chapter 7) has already fetched the page and located the LOG_RECORD_HEADER at final; our job is the switch (lrec->type). Two side structures absorb the dispatched records:
- The per-transaction apply lists
la_Info.repl_lists[]— oneLA_APPLYbucket per livetrid, each a doubly-linked list ofLA_ITEMitems. - The commit queue
la_Info.commit_head / commit_tail— a FIFO ofLA_COMMITnodes, one per end-of-transaction marker, that fixes the replay order.
8.1 The per-transaction apply structures
Section titled “8.1 The per-transaction apply structures”Three structs (defined at the top of log_applier.c) carry the buffered state.
LA_APPLY — one bucket per live transaction:
| Field | Role | Why |
|---|---|---|
tranid | owning transaction id; 0 marks a recycled slot | scan key for la_find_apply_list; 0 lets la_add_apply_list reuse a completed slot before growing |
num_items | count of buffered LA_ITEMs | the long-transaction governor compares it against LA_MAX_REPL_ITEMS |
is_long_trans | sticky flag set once the bucket sheds its tail | after it flips, la_set_repl_log stops allocating and only tracks last_lsa |
start_lsa | LSA of the transaction’s first record | lowest LSA still owned by an unfinished transaction; Chapter 10’s bookmark cannot pass it |
last_lsa | LSA of the most recent record for a long transaction | tells the Chapter 9 long-transaction replay how far to re-scan |
head / tail | ends of the doubly-linked LA_ITEM chain | head is the anchor preserved across a shed; tail is the append point |
LA_ITEM — one buffered replication record:
| Field | Role | Why |
|---|---|---|
next / prev | links in the per-transaction chain | a long transaction’s surviving head has both null — la_unlink_repl_item tolerates that |
log_type | LOG_REPLICATION_DATA or _STATEMENT | drives the unpack branch in la_make_repl_item and the lazy-key branch in la_get_item_pk_value |
item_type | master’s rcvindex (DATA) or statement type (STATEMENT) | identifies the concrete op to replay |
class_name | target class name | resolves the heap/index on the slave |
packed_key_value / _length | disk image of the PK, left packed | unpacked into key only on demand — no DB_VALUE for a key that may yet abort |
key | unpacked PK (DATA, on demand) or statement text (STATEMENT) | the value the slave applies; STATEMENT fills it eagerly with need_clear = true |
db_user / ha_sys_prm | statement-replication context | replays under the right user and system-parameter set |
lsa / target_lsa | replication record LSA and master target LSA | order and provenance of the item |
LA_COMMIT — one end-of-transaction marker in the FIFO:
| Field | Role | Why |
|---|---|---|
next / prev | FIFO links | the queue, not repl_lists[], fixes replay order |
type | LOG_COMMIT / LOG_SYSOP_END / LOG_ABORT | an ABORT node tells la_apply_commit_list to discard the bucket, not replay it |
tranid | owning transaction | matches the node back to its LA_APPLY bucket |
log_lsa | LSA of the EOT marker | the commit point copied into committed_lsa after a successful drain |
log_record_time | wall-clock commit time, 0 for sysops | feeds the time-bound replica delay |
8.2 The two prologue guards before the switch
Section titled “8.2 The two prologue guards before the switch”Every record passes through two gates before la_log_record_process looks at its type.
// la_log_record_process -- src/transaction/log_applier.cif (lrec->trid == NULL_TRANID || LSA_GT (&lrec->prev_tranlsa, final) || LSA_GT (&lrec->back_lsa, final)) { if (lrec->type != LOG_END_OF_LOG) { /* EOF sentinel is the only tolerated exception */ la_applier_need_shutdown = true; /* poison pill: caller exits the apply loop */ er_set (..., ER_HA_LA_INVALID_REPL_LOG_RECORD, 10, ...); return ER_LOG_PAGE_CORRUPTED; }}Guard 1 — corruption / not-yet-written detection. A record is suspect if its trid is NULL_TRANID, or if either back-pointer (prev_tranlsa, predecessor in the same transaction; back_lsa, predecessor in the global log) points forward past final. A back-pointer can only legitimately point at something already written, so a forward one means the page holds bytes not yet a valid record — typically a page fetched a hair before the master flushed it. LOG_END_OF_LOG is tolerated (the active tail’s sentinel carries null/garbage pointers); anything else is unrecoverable mid-stream.
INVARIANT — back-pointers never point past the record being read. Enforced by Guard 1’s
LSA_GTtests onback_lsaandprev_tranlsa. If violated, the daemon refuses to interpret the bytes (which would dereference uninitialized page memory and likely SEGV or apply garbage) and shuts the applier down. TheLOG_END_OF_LOGexception is the only legal way past this gate with a forward/null pointer.
Guard 2 — first-sight registration. The second guard fires when LSA_ISNULL (&lrec->prev_tranlsa) and the record is not an exemption (type != LOG_END_OF_LOG && type != LOG_DUMMY_HA_SERVER_STATE && trid != LOG_SYSTEM_TRANID — none own a bucket). prev_tranlsa == NULL marks a transaction’s first log record (no predecessor in its own chain), so the guard calls apply = la_add_apply_list (lrec->trid) and, the first time apply->start_lsa is null, copies final into it so Chapter 10’s bookmark knows the lowest LSA still belonging to an unfinished transaction. A NULL return means OOM: set la_applier_need_shutdown, return the captured er_errid () (or ER_FAILED).
Only after both guards does the function set la_Info.is_end_of_record = false and enter the type switch.
8.3 The dispatch switch, arm by arm
Section titled “8.3 The dispatch switch, arm by arm”Every arm is traced below. ER_INTERRUPTED is not a failure — the caller (Chapter 7) reads it as “stop this page walk and re-evaluate”, cleanly breaking the inner loop for end-of-log, role change, and crash-recovery markers.
LOG_END_OF_LOG. The tail. Two sub-cases, both returning ER_INTERRUPTED: if the next page exists and the current page is archived (la_does_page_exist(pageid+1) && la_does_page_exist(pageid) == LA_PAGE_EXST_IN_ARCHIVE_LOG), it is a premature EOF in an archive file — warn and skip via final->pageid++; final->offset = 0; otherwise it is the genuine active-log tail — set la_Info.is_end_of_record = true.
LOG_REPLICATION_DATA / LOG_REPLICATION_STATEMENT. The productive arm: both feed la_set_repl_log (pg_ptr, lrec->type, lrec->trid, final) to append the record to the transaction’s bucket (Section 8.4); a non-NO_ERROR return poisons the daemon and propagates.
LOG_SYSOP_END / LOG_COMMIT. The flush trigger — the arm that drains buffered items to the slave.
// la_log_record_process -- src/transaction/log_applier.ccase LOG_SYSOP_END:case LOG_COMMIT: if (LSA_GT (final, &la_Info.committed_lsa)) { /* idempotency gate vs durable bookmark */ eot_time = (lrec->type == LOG_SYSOP_END) ? 0 : la_retrieve_eot_time (pg_ptr, final); error = la_add_node_into_la_commit_list (lrec->trid, final, lrec->type, eot_time); if (eot_time != 0) { error = la_delay_replica (eot_time); ... } /* time-bound replica */ if (la_Info.status == LA_STATUS_IDLE) la_Info.status = LA_STATUS_BUSY; do { error = la_apply_commit_list (&lsa_apply, final_pageid); /* drains buckets in commit order */ ... /* error triage below; on success advance committed_lsa, bump commit_counter for LOG_COMMIT */ } while (!LSA_ISNULL (&lsa_apply)); } else { la_free_repl_items_by_tranid (lrec->trid); } /* already applied across a restart: discard */ break;The outer LSA_GT is the idempotency gate (LSA at/behind the durable bookmark → already applied last run, discard the buckets). Each successful la_apply_commit_list pass copies lsa_apply into committed_lsa and increments commit_counter for real commits only. The non-obvious branch is the inner error triage in the do/while: it fires when la_apply_commit_list returns ER_NET_CANT_CONNECT_SERVER, then inspects er_errid () — recoverable server-down aborts (ER_TM_SERVER_DOWN_UNILATERALLY_ABORTED, ER_LK_UNILATERALLY_ABORTED) are swallowed and the loop retries; any other er_errid () re-raises ER_NET_SERVER_COMM_ERROR and returns. Independently, ER_HA_LA_EXCEED_MAX_MEM_SIZE and ER_TDE_CIPHER_IS_NOT_LOADED (Chapter 11’s TDE path) poison and return; flush errors (LA_IS_FLUSH_ERROR) return immediately.
LOG_ABORT. A rolled-back transaction needs a commit-queue slot so replay order stays gap-free, but with no items: the arm calls la_add_node_into_la_commit_list (lrec->trid, final, LOG_ABORT, 0); when la_apply_commit_list later dequeues an ABORT node it discards that trid’s bucket.
LOG_DUMMY_CRASH_RECOVERY. Written by a master that itself recovered from a crash; contiguity can’t be trusted across it — log a notification, jump final to lrec->forw_lsa, return ER_INTERRUPTED to force a re-anchor. LOG_END_CHKPT. Checkpoint-end marker — break (no-op).
LOG_DUMMY_HA_SERVER_STATE. The HA heartbeat and role-change tripwire. After ha_server_state = la_get_ha_server_state (pg_ptr, final), three outcomes: (a) ha_server_state == NULL (unreadable) → log and break; (b) state != HA_SERVER_STATE_ACTIVE && state != HA_SERVER_STATE_TO_BE_STANDBY and we hold the DB lock file (la_Info.db_lockf_vdes != NULL_VOLDES) → set la_Info.is_role_changed = true, return ER_INTERRUPTED — the failover tripwire (Chapter 11); (c) state still active and la_is_repl_lists_empty () → la_update_ha_apply_info_log_record_time (ha_server_state->at_time) then flush the bookmark via la_log_commit (true) (returning on error). The empty-lists check matters: flushing the bookmark on a heartbeat while a transaction is still buffered would skip un-applied work.
default. Any other record type — break (ignored).
flowchart TD
SW{"lrec->type"} --> INT["EOL / CRASH_RECOVERY / HA role-change\n-> ER_INTERRUPTED, break page walk"]
SW --> RD["REPL_DATA/STATEMENT -> la_set_repl_log buffers"]
SW --> CM["SYSOP_END / COMMIT -> enqueue LA_COMMIT, drain loop"]
SW --> AB["ABORT -> enqueue LA_COMMIT type=ABORT"]
SW --> NOP["END_CHKPT / HA active / default -> break"]
Figure 8-1: the switch arms grouped by outcome. The three ER_INTERRUPTED arms break the page walk; the rest buffer, flush, abort-placeholder, or no-op.
After the switch, an epilogue is a second corruption net for the last record of an archive page: the predicate lrec->forw_lsa.pageid == -1 || lrec->type <= LOG_SMALLER_LOGREC_TYPE || lrec->type >= LOG_LARGER_LOGREC_TYPE (inclusive <=/>= — the boundary sentinels themselves count as invalid) returns ER_LOG_PAGE_CORRUPTED (after bumping past an archive page if applicable); otherwise NO_ERROR.
8.4 la_set_repl_log — buffering with the long-transaction bypass
Section titled “8.4 la_set_repl_log — buffering with the long-transaction bypass”la_set_repl_log looks up the bucket with apply = la_find_apply_list (tranid), then four branches:
// la_set_repl_log -- src/transaction/log_applier.c (condensed branch heads)if (apply == NULL) { er_log_debug (...); return NO_ERROR; } /* (1) never registered: silently drop */if (apply->is_long_trans) { LSA_COPY (&apply->last_lsa, lsa); return NO_ERROR; } /* (2) already long: track last_lsa */if (apply->num_items >= LA_MAX_REPL_ITEMS) { /* (3) crossing the 1000-item threshold */ la_free_all_repl_items_except_head (apply); apply->is_long_trans = true; LSA_COPY (&apply->last_lsa, lsa); return NO_ERROR;}item = la_make_repl_item (log_pgptr, log_type, tranid, lsa); /* (4) normal: build (NULL -> ER_OUT_OF_VIRTUAL_MEMORY) + la_add_repl_item */Non-obvious branches: (1) drops a transaction whose first record we never saw (began before the applier’s start point). Branches (2)/(3) are the memory governor — at the threshold la_free_all_repl_items_except_head releases every item except the first (preserving the list anchor and its start_lsa companion) and flips is_long_trans; thereafter (2) only advances apply->last_lsa so the long-transaction replay (Chapter 9) knows how far to re-scan from the master log directly.
INVARIANT — a long transaction holds at most one buffered item (its head). Enforced by
la_free_all_repl_items_except_headat the crossover and by the already-long branch (which never allocates). Memory is bounded; the price is that Chapter 9 re-reads the records straight from the log betweenstart_lsaandlast_lsa— exactly the blow-upLA_MAX_REPL_ITEMSexists to prevent.
la_free_all_repl_items_except_head walks from apply->head->next, freeing each via la_free_repl_item; a null head returns immediately. la_free_repl_item first unlinks the item (la_unlink_repl_item, which tolerates a long transaction’s unlinked head), then releases class_name (and, only when class_name was set, clears key via pr_clear_value), db_user, ha_sys_prm, and packed_key_value before freeing the node.
8.5 la_make_repl_item / la_add_repl_item — unpacking and linking
Section titled “8.5 la_make_repl_item / la_add_repl_item — unpacking and linking”la_make_repl_item builds a populated LA_ITEM: align/advance past the record header (LA_LOG_READ_ALIGN, LA_LOG_READ_ADVANCE_WHEN_DOESNT_FIT — any failure → NULL), copy repl_log->length bytes into a heap area, get a zeroed skeleton from la_new_repl_item (lsa, &repl_log->lsa) (copies lsa and the master’s target_lsa), then switch (log_type):
// la_make_repl_item -- src/transaction/log_applier.ccase LOG_REPLICATION_DATA: /* layout: [int key_len][string class_name][aligned key] */ ptr = or_unpack_int (area, &item->packed_key_value_length); ptr = or_unpack_string (ptr, &item->class_name); item->packed_key_value = malloc (item->packed_key_value_length); if (item->packed_key_value == NULL) goto error_return; ptr = PTR_ALIGN (ptr, MAX_ALIGNMENT); /* 8-byte align, see or_pack_mem_value */ memcpy (item->packed_key_value, ptr, item->packed_key_value_length); item->item_type = repl_log->rcvindex; break; /* rcvindex identifies the DML op */case LOG_REPLICATION_STATEMENT: /* layout: [int type][string class][string stmt][string user][string prm] */ ptr = or_unpack_int (area, &item->item_type); ptr = or_unpack_string (ptr, &item->class_name); ptr = or_unpack_string (ptr, &str_value); db_make_string (&item->key, str_value); item->key.need_clear = true; ptr = or_unpack_string (ptr, &item->db_user); ptr = or_unpack_string (ptr, &item->ha_sys_prm); break;default: er_set (..., ER_GENERIC_ERROR, 0); goto error_return;LOG_REPLICATION_DATA (Chapter 3) keeps the PK packed, unpacked into item->key only on demand by la_get_item_pk_value; item_type carries rcvindex. LOG_REPLICATION_STATEMENT (Chapter 4) puts the statement text in item->key (need_clear = true) plus db_user / ha_sys_prm. The default arm and any alloc failure jump to error_return (frees area and the half-built item, returns NULL); on success item->log_type is stamped, area freed, item returned. la_add_repl_item is a plain tail append, incrementing apply->num_items.
8.6 The per-trid bucket: find, add, and realloc-on-demand
Section titled “8.6 The per-trid bucket: find, add, and realloc-on-demand”la_Info.repl_lists[] is a flat array of LA_APPLY *; each non-recycled slot holds one transaction’s tranid and the doubly-linked LA_ITEM chain (head to tail), while a recycled slot has tranid == 0.
la_find_apply_list (tranid) linearly scans repl_lists[0 .. cur_repl) for the matching tranid, else NULL. la_add_apply_list (tranid) is find-or-allocate: (1) if la_find_apply_list has it, return it; (2) else reuse the first recycled slot (tranid == 0, left by a completed transaction) by stamping tranid; (3) else if cur_repl == repl_cnt (full), grow via la_init_repl_lists (true) (failure → NULL); (4) claim repl_lists[cur_repl], bump cur_repl, return it.
la_init_repl_lists (need_realloc) owns the array’s storage:
// la_init_repl_lists -- src/transaction/log_applier.c (condensed)if (need_realloc == false) { /* first call: malloc LA_REPL_LIST_COUNT (50) pointers; repl_cnt = 50, cur_repl = 0, j = 0 */ ...} else { /* grow: realloc to repl_cnt + 50; j = old repl_cnt marks first NEW slot, then repl_cnt += 50 */ ...}for (i = j; i < la_Info.repl_cnt; i++) { /* init ONLY [j, repl_cnt); each malloc OOM -> free + ER_OUT_OF_VIRTUAL_MEMORY */ la_Info.repl_lists[i] = malloc (DB_SIZEOF (LA_APPLY)); ...->tranid = 0; num_items = 0; is_long_trans = false; start_lsa = last_lsa = NULL; head = tail = NULL;}Starting the loop at j = old_repl_cnt means the realloc initializes only the newly added slots — existing buckets and their item chains survive untouched.
INVARIANT —
cur_repl <= repl_cnt, and slots[0, cur_repl)are the only live buckets.la_add_apply_listnever claimsrepl_lists[cur_repl]without first ensuringcur_repl < repl_cnt(the grow branch fires precisely when they are equal), and reuses a recycledtranid == 0slot ahead of growing, so the array grows only when all live slots are occupied. Breaking this indexes past the array (heap corruption).
8.7 The commit queue: la_add_node_into_la_commit_list and la_retrieve_eot_time
Section titled “8.7 The commit queue: la_add_node_into_la_commit_list and la_retrieve_eot_time”la_add_node_into_la_commit_list (tranid, lsa, type, eot_time) mallocs an LA_COMMIT, fills type (LOG_COMMIT / LOG_SYSOP_END / LOG_ABORT), log_record_time = eot_time, log_lsa (from the EOT marker) and tranid, then appends to the FIFO at la_Info.commit_tail (or seeds head+tail if empty; OOM → ER_OUT_OF_VIRTUAL_MEMORY). This queue — not repl_lists[] — fixes replay order: Chapter 9’s la_apply_commit_list walks it head-first, so transactions apply in strict master-commit order regardless of how their DML records interleaved on the log.
la_retrieve_eot_time (pgptr, lsa) reads the wall-clock commit time: position past the record header, align/advance to fit a LOG_REC_DONETIME, return 0 on any read failure (the “no time” sentinel the commit arm uses to skip la_delay_replica) or donetime->at_time on success. Only LOG_COMMIT calls it; LOG_SYSOP_END hard-codes 0 (no EOT timestamp).
8.8 Chapter summary — key takeaways
Section titled “8.8 Chapter summary — key takeaways”la_log_record_processgates every record through two guards — a corruption/forward-pointer check that shuts the applier down (except the EOF sentinel), and a first-sightprev_tranlsa == NULLregistration that allocates the bucket and stampsstart_lsa.- The dispatch
switchhas eight reachable arms; three (LOG_END_OF_LOG,LOG_DUMMY_CRASH_RECOVERY, the role-change case ofLOG_DUMMY_HA_SERVER_STATE) returnER_INTERRUPTEDto break the page walk, not to signal failure. Records are buffered until theLOG_COMMIT/LOG_SYSOP_ENDarm firesla_apply_commit_list;LOG_ABORTenqueues an order-preserving placeholder with no items. la_set_repl_logenforces the long-transaction cap: atLA_MAX_REPL_ITEMS(1000) a bucket sheds all but its head, setsis_long_trans, and thereafter only trackslast_lsa— bounding memory at the cost of a later log re-scan.la_make_repl_itemunpacksLOG_REPLICATION_DATA(lazy packed-key +rcvindex) andLOG_REPLICATION_STATEMENT(statement +db_user+ha_sys_prm) differently, sharing anerror_returnthat frees the half-built item.repl_lists[]is a recycle-first, grow-by-50 array;la_add_apply_listreusestranid == 0slots beforela_init_repl_lists (true)reallocs, keepingcur_repl <= repl_cnt.- The
LA_COMMITFIFO fromla_add_node_into_la_commit_list— timestamped byla_retrieve_eot_timefor real commits,0for sysops — imposes master-commit replay order on the slave.
Chapter 9: Applying a Committed Transaction and Reconstructing the Row Image
Section titled “Chapter 9: Applying a Committed Transaction and Reconstructing the Row Image”Chapter 8 left us with the per-transaction picture: each trid owns an
LA_APPLY whose head .. tail chain holds the buffered LA_ITEMs, and
la_Info.commit_head .. commit_tail holds the LA_COMMIT records queued
per LOG_COMMIT/LOG_ABORT/LOG_SYSOP_END. This chapter closes the
apply lifecycle: when the main loop drains a commit record, how are the
buffered events replayed in order, how is the after-image rebuilt from the
master’s heap log, and how does the row reach the slave server? The
“pull, buffer until commit, replay” theory lives in the companion
(cubrid-ha-replication.md §“Per-transaction buffering until commit on
the slave” and §“Reconstructing the row image — case analysis”); here we
trace branches.
9.1 la_apply_commit_list — one commit record per call
Section titled “9.1 la_apply_commit_list — one commit record per call”The main loop calls this whenever it advances past a commit. It processes
exactly one head-of-queue commit (la_Info.commit_head whose type
is LOG_COMMIT/LOG_SYSOP_END/LOG_ABORT), calls la_apply_repl_log,
LSA_COPYs the drained log_lsa into the caller’s *lsa, then unlinks
and free_and_inits the node (see Figure 9-1).
If commit_head == NULL or the head is not a commit-class record, the
body is skipped and *lsa stays NULL. Apply success or failure both
unlink and free the node — the function does not abort on apply error,
propagating it up so the caller decides whether to reconnect. Only
LOG_COMMIT copies log_record_time (the master’s commit stamp the delay
report subtracts from now()).
Invariant: the commit queue is drained strictly FIFO, one node per
call. commit_head advances by exactly one node and only the head is
freed. If a future change looped over multiple commits, the per-call
*lsa return (the caller’s durable bookmark, Chapter 10) would no longer
identify a single safe restart point.
flowchart TB
A["la_apply_commit_list(lsa)"] --> C{"commit_head != NULL\nand type in COMMIT/SYSOP_END/ABORT?"}
C -- no --> Z["return NO_ERROR, lsa stays NULL"]
C -- yes --> D["la_apply_repl_log(tranid, type, log_lsa)"]
D --> E["LSA_COPY(lsa, commit.log_lsa)"]
E --> F{"type == LOG_COMMIT?"}
F -- yes --> G["log_record_time = commit.log_record_time"]
F -- no --> H["unlink + free commit node"]
G --> H
H --> I["return error"]
Figure 9-1 — la_apply_commit_list: one commit node drained per call.
9.2 la_apply_repl_log — draining one transaction’s item chain
Section titled “9.2 la_apply_repl_log — draining one transaction’s item chain”The per-transaction fan-out looks up the LA_APPLY for the trid, then
walks apply->head replaying each item.
Pre-flight branches (before la_lock_dbname and the loop):
la_find_apply_list(tranid) == NULL returns NO_ERROR (never buffered);
rectype == LOG_ABORT calls la_clear_applied_info and returns. The
already-applied guard apply->head == NULL || LSA_LE(commit_lsa, &la_Info.last_committed_lsa) (nothing buffered, or commit_lsa <= the
restart bookmark of Chapter 10) means a previous run handled it — here
LOG_SYSOP_END calls la_free_all_repl_items (frees only items; the list
node is reused for the rest of the long transaction), any other type calls
la_clear_applied_info for a full reset. Otherwise it takes the slave
DB-name lock and loops.
The replay loop — per-item dispatch:
// la_apply_repl_log -- src/transaction/log_applier.c (loop, condensed)while (item) { /* ... periodic la_release_all_page_buffers every LA_MAX_REPL_ITEM_WITHOUT_RELEASE_PB ... */ if (LSA_GT (&item->lsa, &la_Info.last_committed_rep_lsa) && la_need_filter_out (item) == false) { /* <- skip already-done + filtered */ if (item->log_type == LOG_REPLICATION_DATA) switch (item->item_type) { case RVREPL_DATA_UPDATE_START: case RVREPL_DATA_UPDATE_END: case RVREPL_DATA_UPDATE: error = la_apply_update_log (item); break; case RVREPL_DATA_INSERT: error = la_apply_insert_log (item); break; case RVREPL_DATA_DELETE: error = la_apply_delete_log (item); break; default: assert_release (false); } /* <- corruption */ else if (item->log_type == LOG_REPLICATION_STATEMENT) error = la_apply_statement_log (item); else assert_release (false); if (error == NO_ERROR) LSA_COPY (&la_Info.committed_rep_lsa, &item->lsa); /* advance bookmark */ else { /* error sub-branches -- below */ } } next_item = la_get_next_repl_item (item, apply->is_long_trans, &apply->last_lsa); la_free_repl_item (apply, item); item = next_item; if (item != NULL && LSA_GT (&item->lsa, commit_lsa)) { assert (rectype == LOG_SYSOP_END); has_more_commit_items = true; break; }}log_type chooses data vs. statement; for data, item_type chooses
the verb. RVREPL_DATA_UPDATE_START and _END both fall through to
la_apply_update_log — the START/END bracket only mattered on the
master (Chapter 4). Any other data item_type or unknown log_type trips
assert_release(false).
Invariant: committed_rep_lsa only advances on a successful item
apply, and only forward. It is LSA_COPY’d to item->lsa strictly
after error == NO_ERROR; the skip guard LSA_GT(&item->lsa, &last_committed_rep_lsa) reads the restart baseline, so a re-run never
re-applies a covered item. Break this and a crash mid-batch would drop or
double-apply rows.
Error sub-branches (the else when error != NO_ERROR):
- Flush error (
LA_IS_FLUSH_ERROR(error)):goto end— the caller reconnects and retries the whole transaction. - Server-down (
ER_IS_SERVER_DOWN_ERROR(er_errid())): convert toER_NET_CANT_CONNECT_SERVER,goto end. - Retryable, not ignorable (
la_ignore_on_errorfalse andla_retry_on_errortrue):er_setanER_HA_GENERIC_ERRORnotification,LA_SLEEP(10, 0), thencontinue— jumping back to thewhilewithout advancing or freeingitem, retrying it after the sleep (theLA_RETRY_ON_ERRORloop). - Ignorable (else): falls through, item freed, loop advances — the failure is swallowed.
Loop advance / long-transaction break. la_get_next_repl_item
dispatches on apply->is_long_trans: a normal transaction returns
item->next (la_get_next_repl_item_from_list); only when
is_long_trans does it re-read the log via apply->last_lsa
(la_get_next_repl_item_from_log). If the next item’s LSA exceeds
commit_lsa it must be a LOG_SYSOP_END partial commit (asserted), so
the loop breaks with has_more_commit_items = true. At end: for
LOG_SYSOP_END, survivors past commit_lsa are re-attached via
la_free_and_add_next_repl_item (else la_free_all_repl_items empties the
list but keeps the node); a normal LOG_COMMIT calls
la_clear_applied_info for a full reset.
9.3 The three data-DML appliers — common shape
Section titled “9.3 The three data-DML appliers — common shape”la_apply_insert_log, la_apply_update_log, la_apply_delete_log all
open with la_flush_repl_items (false) and close on the same two-arm
tail: error → la_log_apply_error (bumps fail_counter), success → bump
the per-verb counter and num_unflushed. Between those bookends they
diverge in two real ways (Figure 9-2): insert and update fix a log
page and rebuild the after-image while delete does neither, and the two
image-rebuilders resolve the class at different points.
flowchart TB
B["la_flush_repl_items(false)"] --> P["la_get_page(item->target_lsa.pageid)"]
P -- NULL --> XR["return er_errid()"]
P -- ok --> CIN{"insert: db_find_class first\nupdate: defer class to after guards"}
CIN --> E["la_get_recdes(&target_lsa, pgptr, recdes, &rcvindex, is_mvcc_class)"]
E --> F{"recdes.type ASSIGN_ADDRESS\nor RELOCATION?"}
F -- yes --> X["ER_FAILED -> end"]
F -- no --> G{"rcvindex in allowed set?"}
G -- no --> X
G -- yes --> J["la_repl_add_object(class_obj, item, recdes)"]
J --> K["counter++ ; num_unflushed++ ; la_release_page_buffer"]
Figure 9-2 — Common insert/update apply flow. Delete skips P-G and the page release.
Class-resolution order differs by verb. la_apply_insert_log calls
db_find_class before la_get_recdes, because it must derive
is_mvcc_class = la_is_mvcc_class (ws_oid (class_obj)) and pass it in (the
flag drives the §9.5 loaddb up-conversion). la_apply_update_log reverses
this: it calls la_get_recdes with a hard-coded is_mvcc_class = false,
runs its guards, and only then db_find_class — sound because the update
redo log already carries the fully-formed image. A missing class thus
exits early in insert but post-guard in update.
The type guards differ. Both first reject recdes->type == REC_ASSIGN_ADDRESS || REC_RELOCATION (an indirection that escaped
la_get_recdes) with ER_FAILED. Then insert accepts only
RVHF_INSERT/RVHF_MVCC_INSERT; update accepts RVHF_UPDATE,
RVOVF_CHANGE_LINK, RVHF_MVCC_INSERT, RVHF_UPDATE_NOTIFY_VACUUM,
RVHF_INSERT_NEWHOME.
Page-fix lifecycle is insert/update only. Both pin the heap-redo log
page with la_get_page (item->target_lsa.pageid) — a NULL return is an
immediate return er_errid() before the end label (no counter, no
release) — and both la_release_page_buffer (old_pageid) on exit.
Delete is the pkey-only path. la_apply_delete_log never touches a
log page or la_get_recdes — it needs only the key in
item->packed_key_value, calling la_repl_add_object (class_obj, item, NULL); on error la_log_apply_error(..., ER_HA_LA_FAILED_TO_APPLY_DELETE),
on success it bumps delete_counter/num_unflushed.
// la_apply_insert_log -- src/transaction/log_applier.c (condensed)pgptr = la_get_page (item->target_lsa.pageid);if (pgptr == NULL) return er_errid (); /* page-fix failure: before end label */class_obj = db_find_class (item->class_name); /* insert: class resolved FIRST */recdes = la_assign_recdes_from_pool ();is_mvcc_class = la_is_mvcc_class (ws_oid (class_obj));error = la_get_recdes (&item->target_lsa, pgptr, recdes, &rcvindex, la_Info.rec_type, is_mvcc_class);// ... guards on recdes->type and rcvindex, then ...error = la_repl_add_object (class_obj, item, recdes);9.4 la_get_recdes — the five-case after-image reconstructor
Section titled “9.4 la_get_recdes — the five-case after-image reconstructor”Given the target LSA of a heap redo record, la_get_recdes produces the
on-disk RECDES. Its is_mvcc_class input (passed in from §9.3, false
for update) only gates the §9.5 MVCC tweaks at the tail. The base call is
always la_get_log_data (§9.6), which fills recdes->data/length and
sets recdes->type = *(INT16 *) rec_type; after that an if/else if
ladder runs exactly one of the four indirection cases below (a fifth
implicit case is “none of them”).
| Case | Condition | Meaning | Helper |
|---|---|---|---|
| 1 | RVOVF_CHANGE_LINK | Overflow record updated; redo only changed the link. Re-stitch, force REC_BIGONE. | la_get_overflow_recdes(RVOVF_PAGE_UPDATE) |
| 2 | type == REC_BIGONE | Fresh overflow insert: home slot points to an overflow-page chain. | la_get_overflow_recdes(RVOVF_NEWPAGE_INSERT) |
| 3 | RVHF_INSERT + REC_ASSIGN_ADDRESS | Deferred-insert: slot reserved first, real image in a later UPDATE of the trid. | la_get_next_update_log (then case 2 if BIGONE) |
| 4 | RVHF_UPDATE/_NOTIFY_VACUUM + REC_RELOCATION | Home slot forwards; real image at the relocated RVHF_INSERT_NEWHOME. | la_get_relocation_recdes |
Each arm re-reads recdes->type from the helper’s rec_type; case 3
re-enters case 2 if the chased real row is itself REC_BIGONE. If none
match (the common normal REC_HOME path), the base image is already full
— the implicit fifth case. Then the §9.5 MVCC tweaks run.
9.5 MVCC header injection on the slave
Section titled “9.5 MVCC header injection on the slave”A master heap record may lack the slot-resident MVCC insert id; the slave
re-materializes the layout its own server expects. For RVHF_MVCC_INSERT
it just calls la_make_room_for_mvcc_insid (recdes). The second arm
(is_mvcc_class && *rcvindex == RVHF_INSERT && type != REC_BIGONE) exists
for the loaddb bulk insert on a CS-mode master, whose rows are logged
as plain RVHF_INSERT without MVCC flags: it sets VALID_INSID, calls
la_make_room_for_mvcc_insid, and if recdes->length > la_Info.maxslotted_reclength (the record will overflow into REC_BIGONE)
also sets VALID_DELID|VALID_PREV_VERSION and calls
la_make_room_for_mvcc_delid_and_prev_ver. Both helpers assert their
flag preconditions, then LA_MOVE_INSIDE_RECORD-shift the body right;
LA_MOVE_INSIDE_RECORD asserts area_size >= length + delta, so an
undersized buffer trips an assert rather than corrupting memory.
9.6 la_get_log_data and the decompress path
Section titled “9.6 la_get_log_data and the decompress path”la_get_log_data reads one heap log record’s data area, switching on
lrec->type:
LOG_UNDOREDO_DATA/LOG_DIFF_UNDOREDO_DATA/ MVCC variants — pick theMVCC_UNDOREDOvsUNDOREDOlayout. Ifmatch_rcvindex == 0or the record’srcvindexmatches, publish*rcvindex/*logs; else*logs = NULL(“not the record you wanted”). DIFF types callla_get_undoredo_difffor the undo image; others align past it.LOG_UNDO_DATA/LOG_MVCC_UNDO_DATAandLOG_REDO_DATA/LOG_MVCC_REDO_DATA— same match/publish overLOG_REC_UNDO/LOG_REC_REDO.default—*logs = NULL, no data.
If ZIP_CHECK(temp_length), the redo is log_unzip’d and
la_get_zipped_data produces the final image — for DIFF by log_diff
(XOR) of the undo against the decompressed redo, otherwise a straight copy
— peeling the leading INT16 rec_type when requested and allocating
when is_overflow. Otherwise the raw payload is copied directly.
9.7 The three chase helpers
Section titled “9.7 The three chase helpers”la_get_overflow_recdes walks prev_tranlsa backward, prepending
every LOG_REDO-type record of the trid (head ends first), stopping at a
trid change or LOG_DUMMY_OVF_RECORD, then concatenates the parts,
stripping LA_OVF_FIRST_PART/LA_OVF_REST_PARTS headers. Malloc failure
frees the partial list and returns ER_OUT_OF_VIRTUAL_MEMORY.
la_get_next_update_log (deferred-insert chaser) scans forward from
prev_lrec->forw_lsa for the UPDATE whose data.{pageid,offset,volid}
match the reserved slot and whose rcvindex is
RVHF_UPDATE/_NOTIFY_VACUUM. Failure mode: hitting
LOG_COMMIT/LOG_ABORT for the trid first returns ER_GENERIC_ERROR —
the expected update never arrived.
la_get_relocation_recdes follows prev_tranlsa one hop back to the
RVHF_INSERT_NEWHOME record (calling la_get_log_data with
match_rcvindex == RVHF_INSERT_NEWHOME); a null prev LSA or trid mismatch
raises ER_LOG_PAGE_CORRUPTED.
9.8 la_disk_to_obj and la_repl_add_object — to the slave server
Section titled “9.8 la_disk_to_obj and la_repl_add_object — to the slave server”la_disk_to_obj (SQL-logging path only, not the main flush) turns a
RECDES into a DB_OTMPL, parsing the MVCC header for the skip size:
mvcc_flags == 0 advances past chn only; otherwise past INSID/DELID
(OR_MVCCID_SIZE), chn, then a PREV_VERSION LOG_LSA if flagged, before
la_get_current reads columns. A buf->ptr > buf->endptr overrun returns
ER_TF_BUFFER_OVERFLOW.
la_repl_add_object is the bridge to the slave server. It returns early
if au_fetch_class or sm_flush_objects fails, then (skipping
sm_partitioned_class_type for delete) maps item_type to an operation
in a switch — UPDATE_* → LC_UPDATE_OPERATION_TYPE, INSERT →
LC_INSERT_OPERATION_TYPE, DELETE → LC_FLUSH_DELETE, default: assert(false) on an unknown verb — and finally queues the object via
__gv_loc_repl.ws_add_to_repl_obj_list (class_oid, item->packed_key_value, ..., recdes, operation, has_index). The network flush happens later in
la_flush_repl_items.
9.9 la_need_filter_out — per-item table filtering
Section titled “9.9 la_need_filter_out — per-item table filtering”Before any data item, la_apply_repl_log calls this. It strips [...]
quoting from item->class_name, then returns false (never filter)
when filter->type == REPL_FILTER_NONE, for most statement replication
(log_type == LOG_REPLICATION_STATEMENT && item_type != CUBRID_STMT_TRUNCATE
— only truncate, a bulk delete on one table, is filterable), or for the
serial system class (CT_SERIAL_NAME, which must always replicate to keep
sequence state consistent). Otherwise it scans filter->list for the
class and returns true when REPL_FILTER_INCLUDE_TBL && !filter_found
or REPL_FILTER_EXCLUDE_TBL && filter_found.
9.10 la_apply_statement_log — statement replay and determinism
Section titled “9.10 la_apply_statement_log — statement replay and determinism”This path replays SQL text, not a row image: it flushes pending row
objects hard (la_flush_repl_items(true)), optionally sets item->db_user
(skipped for stored-procedure and user DDL via need_set_user = false)
and item->ha_sys_prm around the call, runs res = la_update_query_execute(stmt_text, false), and buckets the affected-row
count (DDL → schema_counter; INSERT/DELETE/UPDATE → matching counter).
TRUNCATE short-circuits to NO_ERROR if filtered;
CUBRID_STMT_DROP_LABEL/default returns NO_ERROR (unsupported, ignored).
Non-happy-path branches:
- User not found — when
need_set_userand adb_useris set, ifau_find_user(item->db_user) == NULL, a server-downer_errid()is converted toER_NET_CANT_CONNECT_SERVER, otherwiseerror = er_errid()(asserted non-zero); either way itbreaks out of the switch. - Query-execute failure — if
res < 0,error = er_errid(), and anER_IS_SERVER_DOWN_ERROR(error)is again converted toER_NET_CANT_CONNECT_SERVER. - Final failure arm — after the switch, any
error != NO_ERRORer_setsER_HA_LA_FAILED_TO_APPLY_STATEMENT(class, stmt, error, message) and bumpsla_Info.fail_counter++before returning.
Because this executes SQL on the slave, statement-based DML inherits any
non-determinism (NOW(), RAND(), …) — CUBRID does not rewrite or
block such functions at emit time. This is the divergence risk in the
companion’s Open Questions (cubrid-ha-replication.md §“Open Questions”
item 8). Row-based replication (§9.3–9.8) avoids it by shipping the
materialized after-image, not the recipe.
9.11 Chapter summary — key takeaways
Section titled “9.11 Chapter summary — key takeaways”la_apply_commit_listdrains one commit per call and returns its LSA as the durable-bookmark candidate; onlyLOG_COMMITupdates the delay clocklog_record_time.la_apply_repl_logreplays the item chain in LSA order, dispatching onlog_typethenitem_type(UPDATE_START/_ENDboth route to update);la_get_next_repl_itemreturnsitem->nextnormally and only re-reads the log vialast_lsawhenis_long_trans.committed_rep_lsaadvances only on a successful apply, only forward — the skip guard makes re-runs idempotent. Retryable errorsLA_SLEEP(10,0)+continuewithout advancing; flush/server-downgoto endto reconnect; ignorable errors are swallowed.- Delete is the pkey-only path (no log page,
la_repl_add_object(..., NULL)); insert and update pin a heap-redo log page (la_get_page/la_release_page_buffer) and rebuild the after-image, differing in their acceptedrcvindexsets and in class-resolution order — insert resolves the class (andla_is_mvcc_class) beforela_get_recdes, update after its guards. la_get_recdesresolves five record shapes — normalREC_HOME,RVOVF_CHANGE_LINKoverflow-update,REC_BIGONEoverflow-insert,RVHF_INSERT+REC_ASSIGN_ADDRESSdeferred-insert (la_get_next_update_log), andRVHF_UPDATE*+REC_RELOCATIONrelocation (la_get_relocation_recdes).- The slave re-materializes MVCC headers via
la_make_room_for_mvcc_insid/..._delid_and_prev_ver(notably to up-convert loaddb rows), after thela_get_log_data/la_get_undoredo_diff/la_get_zipped_dataunzip-and-XOR-diff decode. la_repl_add_objectandla_apply_statement_logboth have hard error exits — the former returns early onau_fetch_class/sm_flush_objectsfailure and asserts on an unknown verb; the latter bumpsfail_counterander_setsER_HA_LA_FAILED_TO_APPLY_STATEMENTon any non-NO_ERRORexit, converting server-down toER_NET_CANT_CONNECT_SERVER.
Chapter 10: Durable Apply Bookmark and Restart Recovery
Section titled “Chapter 10: Durable Apply Bookmark and Restart Recovery”On boot, applylogdb (Ch 7–9) must answer: where did I stop applying?
CUBRID keeps that bookmark in the slave catalog table _db_ha_apply_info,
updated inside the same slave transaction that applies each batch of
replicated rows — so bookmark advance and row apply commit or roll back
together. This chapter covers the struct mirror (LA_HA_APPLY_INFO), the
writer (la_log_commit), the boot read-back, the SQL helpers, the floor
(la_find_required_lsa), and archive trimming (la_remove_archive_logs).
For why a durable cursor is needed see the companion’s “Forward log
walking with a position cursor and durable bookmark” and “Atomic emission
of replication and commit records” — not re-derived here.
10.1 LA_HA_APPLY_INFO — the in-memory mirror of the catalog row
Section titled “10.1 LA_HA_APPLY_INFO — the in-memory mirror of the catalog row”LA_HA_APPLY_INFO is a one-to-one image of a single _db_ha_apply_info
row keyed by (db_name, copied_log_path). It is a transfer struct:
la_get_ha_apply_info fills it from a SELECT, the values are copied into
the long-lived global la_Info, and the fields then live scattered across
la_Info and are written back via la_update_ha_last_applied_info.
// struct la_ha_apply_info -- src/transaction/log_applier.cstruct la_ha_apply_info{ char db_name[256]; DB_DATETIME creation_time; char copied_log_path[4096]; LOG_LSA committed_lsa; /* last committed commit log lsa */ LOG_LSA committed_rep_lsa; /* last committed replication log lsa */ LOG_LSA append_lsa; /* append lsa of active log header */ LOG_LSA eof_lsa; /* eof lsa of active log header */ LOG_LSA final_lsa; /* last processed log lsa */ LOG_LSA required_lsa; /* start lsa of first txn to be applied */ DB_DATETIME log_record_time; DB_DATETIME log_commit_time; DB_DATETIME last_access_time; int status; INT64 insert_counter; INT64 update_counter; INT64 delete_counter; INT64 schema_counter; INT64 commit_counter; INT64 fail_counter; DB_DATETIME start_time;};| Field | Role | Why it exists |
|---|---|---|
db_name | Row key half 1; master DB prefix. | Disambiguates rows per master. |
creation_time | Master log_hdr->db_creation snapshot. | Boot guard against log re-creation (10.3). |
copied_log_path | Row key half 2; slave log_path. | Distinguishes two copylog streams. |
committed_lsa | Highest committed commit LSA. | ”How far replicated”; re-apply floor. |
committed_rep_lsa | Highest committed REPL LSA. | Separate stream; REPL-filter floor. |
append_lsa | Master header append_lsa at write. | Lag diagnostics. |
eof_lsa | Master header eof_lsa. | Lag diagnostics; readable bound. |
final_lsa | Last LSA processed (read+dispatched). | Restart resume cursor; seeds la_Info.final_lsa. |
required_lsa | Start LSA of oldest open txn. | Archive-trim floor (10.6–10.7). |
log_record_time | Last applied commit record time (master). | Replication-delay metric. |
log_commit_time | Slave commit wall-clock. | Delay / operator visibility. |
last_access_time | SYS_DATETIME of last write. | Liveness heartbeat. |
status | LA_STATUS_IDLE / _BUSY. | Forced IDLE on read-back (10.2). |
insert_counter … fail_counter | Cumulative row/failure tallies. | Survive restarts; resume from stored value. |
start_time | When this instance (re)started. | Separates “since boot” from cumulative. |
la_init_ha_apply_info zeroes the struct, then NULL-marks the six LSA
fields:
// la_init_ha_apply_info -- src/transaction/log_applier.cmemset ((void *) ha_apply_info, 0, sizeof (LA_HA_APPLY_INFO));LSA_SET_NULL (&ha_apply_info->committed_lsa);/* ... committed_rep, append, eof, final ... */LSA_SET_NULL (&ha_apply_info->required_lsa);memset zeroes the bytes, but LSA_SET_NULL writes the sentinel
(pageid = -1) that LSA_ISNULL tests for — a zero LSA is a valid
page-0 position, not “absent”. This lets la_get_last_ha_applied_info
distinguish “column was SQL NULL” from “column was 0”.
Invariant — the struct is a strict mirror of one catalog row. Every field maps to one
_db_ha_apply_infocolumn (LSAs split into_pageid/_offsetpairs).la_get_ha_apply_infoasserts bothcol_cnt == LA_OUT_VALUE_COUNT(the SELECT returned exactly 23 columns) andout_value_idx == LA_OUT_VALUE_COUNTafter the decode; the column guard is what actually catches a schema that grew a column. The INSERT mirror,la_insert_ha_apply_info, assertsin_value_idx == LA_IN_VALUE_COUNT(15 bound params; the other 11 columns are SQL literals). Add a column without bumping these counts and theassert_releasefires — schema and struct cannot silently drift.
flowchart LR cat["_db_ha_apply_info row\n(LSA cols split pageid/offset)"] mem["LA_HA_APPLY_INFO\n(transient)"] glob["la_Info\n(resident)"] cat -->|"la_get_ha_apply_info SELECT"| mem mem -->|"la_get_last_ha_applied_info copy"| glob glob -->|"la_update_ha_last_applied_info UPDATE"| cat
Figure 10-1. The row, its transient struct image, and the resident globals — and the three functions that move data between them.
10.2 Reading the row — la_get_ha_apply_info
Section titled “10.2 Reading the row — la_get_ha_apply_info”A parameterized SELECT against CT_HA_APPLY_INFO_NAME keyed by
(prefix_name, log_path), returning the affected-tuple count as
res (>0 found, 0 not found, <0 error). Branch map:
db_find_class (CT_HA_APPLY_INFO_NAME) == NULL→ catalog class missing;assert (er_errid () != NO_ERROR), return the error id (the “DB not yet HA-prepared” path).db_execute_with_values (...)→res. Onlyres > 0decodes a tuple; else fall through to cleanup and returnres(0 = no row, the caller’s insert path).db_query_first_tupleswitch —DB_CURSOR_SUCCESSdecodes all 23 out-values (the six LSA columns testDB_IS_NULLon both halves andLSA_SET_NULL+out_value_idx += 2on NULL;append_lsa/eof_lsaread unconditionally;statusforced toLA_STATUS_IDLE).END/ERROR/default→res = ER_FAILED.- Cleanup:
db_query_end+db_value_clearon every in-value, always reached; out-values cleared only in theSUCCESSarm.
The NULL-tolerant decode mirrors la_init_ha_apply_info: a legitimately
NULL column (e.g. committed_lsa before the first commit) decodes back to
LSA_ISNULL, not a bogus page-0 position.
10.3 Seeding the cursor at boot — la_get_last_ha_applied_info
Section titled “10.3 Seeding the cursor at boot — la_get_last_ha_applied_info”The restart-recovery entry point. It runs once during applylogdb
startup (Ch 7) and decides the resume LSA:
// la_get_last_ha_applied_info -- src/transaction/log_applier.clog_db_creation = act_log->log_hdr->db_creation;db_localdatetime (&log_db_creation, &log_db_creation_time);res = la_get_ha_apply_info (la_Info.log_path, act_log->log_hdr->prefix_name, &apply_info);if (res > 0) { LSA_COPY (&la_Info.committed_lsa, &apply_info.committed_lsa); /* ... committed_rep, append, eof, final, required, counters ... */ if ((log_db_creation_time.date != apply_info.creation_time.date) || (log_db_creation_time.time != apply_info.creation_time.time)) return ER_FAILED; /* <- stored row is a different DB incarnation */ if (LSA_ISNULL (&la_Info.required_lsa)) { er_set (...ER_HA_GENERIC_ERROR...); return ER_FAILED; } /* <- malformed row */ }else if (res == 0) insert_apply_info = true; /* <- first ever boot: no row yet */else return res; /* <- SELECT error, propagate */Three top-level branches — row found, no row (flag insert),
error (return) — plus two hard failures inside the found branch: a
creation-time mismatch (the copied log was replaced by a different DB
incarnation) and a NULL required_lsa (an existing row must carry
one). Then four LSA_ISNULL fixups establish the fresh-row floor in
dependency order:
if (LSA_ISNULL (&la_Info.required_lsa)) LSA_COPY (&la_Info.required_lsa, &act_log->log_hdr->eof_lsa); /* anchor at master EOF */if (LSA_ISNULL (&la_Info.committed_lsa)) LSA_COPY (&la_Info.committed_lsa, &la_Info.required_lsa);/* same for committed_rep_lsa */if (LSA_ISNULL (&la_Info.final_lsa)) LSA_COPY (&la_Info.final_lsa, &la_Info.required_lsa); /* <- cursor seed */On a brand-new applier all four collapse onto the master’s current
eof_lsa (replication starts from “now”); on a resume the stored
final_lsa is non-NULL and untouched. Finally the row is persisted and
the seed locked in:
if (insert_apply_info == true) res = la_insert_ha_apply_info (&log_db_creation_time);else res = la_update_ha_apply_info_start_time ();if (res == 0) return ER_FAILED; /* <- 0 affected rows == failure */else if (res < 0) return res;(void) db_commit_transaction ();LSA_COPY (&la_Info.last_committed_lsa, &la_Info.committed_lsa);LSA_COPY (&la_Info.last_committed_rep_lsa, &la_Info.committed_rep_lsa);Invariant —
last_committed_lsais the high-water floor that blocks regression. Seeded here from the storedcommitted_lsa, it is whatla_update_ha_last_applied_infocompares against withLSA_GEbefore writing (10.4). Without it a restart could persist a lower committed LSA than was previously durable, silently rewinding the mark.
10.3a First-boot insert — la_insert_ha_apply_info branch by branch
Section titled “10.3a First-boot insert — la_insert_ha_apply_info branch by branch”Called only when la_get_last_ha_applied_info found no row (res == 0),
this INSERTs the 26-column row: 15 bound ? params (db_name, creation
time, copied path, and the six LSA pageid/offset pairs) plus 11 SQL
literals (SYS_DATETIME for the three times and start_time; 0 for
status and the six counters). After binding the params:
// la_insert_ha_apply_info -- src/transaction/log_applier.cassert_release (in_value_idx == LA_IN_VALUE_COUNT); /* <- exactly 15 binds, else abort */
res = la_update_query_execute_with_values (query_buf, in_value_idx, &in_value[0], true);for (i = 0; i < in_value_idx; i++) db_value_clear (&in_value[i]);
if (res <= 0) { return res; /* <- INSERT failed: no log-info file written */ }
/* create log info */msg = msgcat_message (MSGCAT_CATALOG_CUBRID, MSGCAT_SET_LOG, MSGCAT_LOG_LOGINFO_COMMENT);if (msg == NULL) { msg = "COMMENT: %s for database %s\n"; /* <- msgcat miss: hard-coded fallback */ }fp = fopen (la_Info.loginf_path, "w");if (fp == NULL) { er_set (...ER_LOG_MOUNT_FAIL..., la_Info.loginf_path); /* <- warning only, not fatal */ }else { (void) fprintf (fp, msg, CUBRID_MAGIC_LOG_INFO, la_Info.loginf_path); fflush (fp); fclose (fp); /* <- .log_info stamped with magic comment */ }return res;Four branches after binding: the assert_release guard (same drift check
as 10.1’s invariant); the res <= 0 early return propagating an INSERT
failure without touching the .log_info file; the msg == NULL
msgcat-miss path substituting a literal format string; and the fopen
outcome — fp == NULL raises a non-fatal ER_LOG_MOUNT_FAIL warning,
else write the CUBRID_MAGIC_LOG_INFO comment + fflush/fclose. The
.log_info file is best-effort: its failure never aborts the insert
because res (the affected-row count) is already > 0 and is returned
either way.
10.4 Writing the row — la_update_ha_last_applied_info
Section titled “10.4 Writing the row — la_update_ha_last_applied_info”Called from la_log_commit, this UPDATEs all twelve LSA halves, two
optional times, six counters, and last_access_time = SYS_DATETIME, keyed
by (db_name, copied_log_path). Two pieces of logic matter:
// la_update_ha_last_applied_info -- src/transaction/log_applier.cif (LSA_GE (&la_Info.committed_lsa, &la_Info.last_committed_lsa)) { /* bind committed_lsa */ }else { /* bind last_committed_lsa instead */ } /* <- never regress the mark *//* same LSA_GE guard for committed_rep_lsa; times use IFNULL(?, col) */
res = la_update_query_execute_with_values (query_buf, in_value_idx, &in_value[0], true);if (res == 0) /* <- 0 rows matched: row was deleted out from under us */ { db_localdatetime (&la_Info.act_log.log_hdr->db_creation, &log_db_creation_time); res = la_insert_ha_apply_info (&log_db_creation_time); if (res > 0) res = la_update_query_execute_with_values (query_buf, in_value_idx, &in_value[0], true); }The committed columns are written as MAX(current, last_committed) so the
durable mark cannot regress even if the in-memory value momentarily lags;
log_record_time / log_commit_time use IFNULL(?, col) so a batch with
no commit record leaves the stored timestamp intact. If the row was
deleted (UPDATE matched zero rows) the function re-INSERTs via
la_insert_ha_apply_info and retries the UPDATE so la_Info state is not
lost (same pattern in la_update_ha_apply_info_log_record_time). It does
not commit — leaving the transaction open so la_log_commit can commit
the bookmark together with the applied rows.
10.5 The commit point — la_log_commit
Section titled “10.5 The commit point — la_log_commit”la_log_commit (bool update_commit_time) is the single chokepoint where
a slave apply batch becomes durable, invoked from the dispatch path (Ch 8)
at commit records, at server-state records when the repl list is empty,
and from the main apply loop. It recomputes required_lsa, snapshots the
master header LSAs, flushes buffered rows, stages the bookmark UPDATE, and
commits — each step gating the next:
// la_log_commit -- src/transaction/log_applier.c(void) la_find_required_lsa (&la_Info.required_lsa); /* recompute floor */LSA_COPY (&la_Info.append_lsa, &la_Info.act_log.log_hdr->append_lsa);LSA_COPY (&la_Info.eof_lsa, &la_Info.act_log.log_hdr->eof_lsa);if (update_commit_time) la_Info.log_commit_time = time (0);error = la_flush_repl_items (true); /* push buffered rows */if (error != NO_ERROR) return error; /* <- not flushed: nothing committed */res = la_update_ha_last_applied_info (); /* stage bookmark UPDATE */if (res > 0) error = la_commit_transaction (); /* <- atomic: rows + bookmark */else { la_Info.fail_counter++; if (ER_IS_SERVER_DOWN_ERROR (res)) error = ER_NET_CANT_CONNECT_SERVER; /* loop reconnects */ else { er_set (...); error = NO_ERROR; } /* <- swallow, retry same final_lsa */ }return error;flowchart TD
A["la_log_commit(update_commit_time)"] --> B["la_find_required_lsa"]
B --> C["copy append_lsa, eof_lsa from header"]
C --> D{update_commit_time?}
D -->|yes| E["log_commit_time = time(0)"]
D -->|no| F["la_flush_repl_items(true)"]
E --> F
F --> G{flush error?}
G -->|"!= NO_ERROR"| H["return error -- nothing committed"]
G -->|NO_ERROR| I["la_update_ha_last_applied_info -> res"]
I --> J{res > 0?}
J -->|yes| K["la_commit_transaction -- atomic apply plus bookmark"]
J -->|no| L["fail_counter++"]
L --> M{ER_IS_SERVER_DOWN_ERROR?}
M -->|yes| N["error = ER_NET_CANT_CONNECT_SERVER"]
M -->|no| O["er_set HA_GENERIC; error = NO_ERROR -- swallow"]
K --> P["return error"]
N --> P
O --> P
Figure 10-2. la_log_commit branch map. Every exit either commits both
the rows and the bookmark, or commits neither.
Invariant — apply and bookmark are one transaction.
la_flush_repl_itemsissues the row mutations andla_update_ha_last_applied_infothe bookmark UPDATE, both on the same connection with no intervening commit; the singlela_commit_transaction(callingdb_commit_transaction) makes them durable together. A crash after staging but before commit rolls both back — no window where the bookmark says “applied through X” but the rows for X are missing.
On bookmark-UPDATE failure (res <= 0) a server-down error becomes
ER_NET_CANT_CONNECT_SERVER so the loop reconnects and re-applies; any
other failure is swallowed to NO_ERROR after fail_counter++. The
rows never committed, so the next iteration retries from the same
final_lsa — cost is one idempotent re-apply (10.8).
10.6 Computing the floor — la_find_required_lsa
Section titled “10.6 Computing the floor — la_find_required_lsa”required_lsa is the lowest LSA the daemon might still re-read: the
start_lsa of the oldest open transaction in the per-transaction lists
(Ch 8). la_log_commit recomputes it before every commit.
// la_find_required_lsa -- src/transaction/log_applier.cLSA_SET_NULL (&lowest_lsa);for (i = 0; i < la_Info.cur_repl; i++) { if (la_Info.repl_lists[i]->tranid <= 0) continue; /* <- empty/closed slot */ if (LSA_ISNULL (&lowest_lsa) || LSA_GT (&lowest_lsa, &la_Info.repl_lists[i]->start_lsa)) LSA_COPY (&lowest_lsa, &la_Info.repl_lists[i]->start_lsa); }if (LSA_ISNULL (&lowest_lsa)) LSA_COPY (required_lsa, &la_Info.final_lsa); /* <- no open txn: floor = cursor */else LSA_COPY (required_lsa, &lowest_lsa);Three branches: skip closed slots (tranid <= 0), track the running
minimum, and — if no transaction is open — fall back to final_lsa so the
floor advances during idle periods and archives can still be trimmed.
Invariant —
required_lsa <= final_lsaand never above any open transaction’s start. Everything strictly belowrequired_lsais guaranteed never re-read — the preconditionla_remove_archive_logsrelies on to delete an archive safely.
10.7 Trimming slave-local archives — la_remove_archive_logs
Section titled “10.7 Trimming slave-local archives — la_remove_archive_logs”copylogdb continually appends archive logs to the slave’s log_path
(Ch 6). la_remove_archive_logs reclaims that space, gated by both a
max-count cap and the required_lsa floor, returning the new
last_deleted_arv_num (or the old one if nothing was deleted).
// la_remove_archive_logs -- src/transaction/log_applier.cif (LA_LOG_IS_IN_ARCHIVE (la_Info.required_lsa.pageid)) /* floor still in an archive? */ { error = la_find_archive_num (&required_arv_num, la_Info.required_lsa.pageid); if (error != NO_ERROR) return last_deleted_arv_num; /* <- can't locate: keep all */ max_arv_count = MAX (log_max_archives, nxarv_num - required_arv_num); /* protect floor */ }else max_arv_count = log_max_archives; /* floor in active log: cap only */if (current_arv_count > max_arv_count) /* nxarv_num - last_deleted - 1 */ { /* first = last_deleted+1; last = nxarv_num - max_arv_count - 1 */ if (last_arv_num_to_delete < 0 || last_arv_num_to_delete < first_arv_num_to_delete) return last_deleted_arv_num; /* <- nothing eligible */ /* throttle: cap (last-first+1) at max_arv_count_to_delete */ for (i = first ...; i <= last_arv_num_to_delete; i++) fileio_unformat (NULL, archive_name); /* <- physical delete */ return last_arv_num_to_delete; }return last_deleted_arv_num; /* under the cap: keep all */Branch summary:
- Floor in archive (
pageid < nxarv_pageid): raisemax_arv_countso archives fromrequired_arv_numup are retained; ifla_find_archive_numfails, bail keeping all. - Floor in active log: only
HA_COPY_LOG_MAX_ARCHIVEScaps retention. - Under the cap, or eligible range empty/negative: unchanged.
max_arv_count_to_deletethrottle: caller passes1for interval-paced deletion orINT_MAXwhen a new archive just appeared.
The caller feeds the return into la_Info.last_deleted_archive_num,
persists it via la_update_last_deleted_arv_num, then stamps
la_Info.last_time_archive_deleted.
Invariant — an archive below
required_lsais deleted only afterla_log_commithas persisted that floor.required_lsais written inside the apply-batch transaction (10.5) before the loop reaches the trim step, so the persisted floor is never higher than what was applied. A crash after deleting but before the next commit is safe: the durablerequired_lsaalready excludes the deleted range.
10.8 The crash window and re-apply idempotency
Section titled “10.8 The crash window and re-apply idempotency”Order matters only between la_repl_add_object staging a row into the
bulk-flush list (Ch 9) and la_log_commit committing it. A crash there
rolls the transaction back — staged rows and the bookmark UPDATE vanish
together (10.5 invariant); on restart la_get_last_ha_applied_info reads
the previous committed final_lsa and the daemon re-applies that range.
Re-apply is idempotent, and both guards live in la_apply_repl_log — not
in its la_apply_commit_list caller, which only dequeues each pending
commit and hands it down. la_apply_repl_log skips an entire commit whose
LSA is LSA_LE (commit_lsa, &la_Info.last_committed_lsa) (Ch 9), and for a
commit that does proceed it filters each REPL item, applying only those
with LSA_GT (&item->lsa, &la_Info.last_committed_rep_lsa). A record
committed before the crash is dropped as already-applied; one only staged
is re-applied cleanly. The companion’s “Open Questions” covers the
remaining edge — a non-idempotent user trigger observing a re-apply, which
this path does not guard — so cross-link there.
10.9 Chapter summary — key takeaways
Section titled “10.9 Chapter summary — key takeaways”LA_HA_APPLY_INFOis a transient one-to-one image of one_db_ha_apply_inforow keyed by(db_name, copied_log_path); resident state lives field-by-field inla_Info, and theLA_*_VALUE_COUNTasserts keep struct and schema from drifting.final_lsais the restart resume cursor,required_lsathe minimal-needed floor; on a fresh applier both collapse onto the master’seof_lsa, whilecommitted_lsa/committed_rep_lsaare the monotonic already-applied marks.la_insert_ha_apply_infowrites the 15-bind row once on first boot (and as the self-heal step inside the UPDATE path); after the asserted bind count it propagates an INSERT failure verbatim and treats the.log_infofile as best-effort, never aborting onER_LOG_MOUNT_FAIL.la_log_commitis the single atomic chokepoint: flush buffered rows and stage the bookmark UPDATE on one connection, then commit both with onela_commit_transaction— durable together or not at all.la_update_ha_last_applied_infonever lets the committed marks regress (LSA_GEguard) and self-heals a deleted row by re-INSERTing and retrying the UPDATE.la_get_last_ha_applied_inforejects a stale bookmark on a creation-time mismatch or NULLrequired_lsa, and seedslast_committed_lsa/last_committed_rep_lsaas the anti-regression floor.la_find_required_lsaandla_remove_archive_logstogether reclaim slave-local archive space only above the persistedrequired_lsa, making thela_repl_add_object→la_log_commitcrash window safe via idempotent re-apply.
Chapter 11: Special Paths Long Transactions Role Change Filters and TDE
Section titled “Chapter 11: Special Paths Long Transactions Role Change Filters and TDE”Chapters 8 to 10 trace the steady-state apply lifecycle. This chapter collects
the four paths that break that shape: a transaction too large to stage; a
planned role change demoting the slave; a table-level filter that drops events;
and TDE-encrypted pages needing a key shared over a socket. For the steady-state
mechanics and the master/slave topology these perturb, assume the companion
cubrid-ha-replication.md (sections “Apply pipeline” and “Failover”).
11.1 Long transactions: trading memory for an O(records) re-walk
Section titled “11.1 Long transactions: trading memory for an O(records) re-walk”Chapter 8 showed la_set_repl_log appending each REPL record to the
apply->head .. apply->tail list, capped at LA_MAX_REPL_ITEMS. A transaction
that exceeds it flips the LA_APPLY entry into long-transaction mode and
discards the staged items:
// la_set_repl_log -- src/transaction/log_applier.c if (apply->is_long_trans) /* already long: track last_lsa only */ { LSA_COPY (&apply->last_lsa, lsa); return NO_ERROR; } if (apply->num_items >= LA_MAX_REPL_ITEMS) /* cap hit: switch to long mode */ { la_free_all_repl_items_except_head (apply); /* <- discard the staged list */ apply->is_long_trans = true; LSA_COPY (&apply->last_lsa, lsa); return NO_ERROR; }The entry now tracks only start_lsa (set by la_log_record_process at the
first record) and last_lsa (bumped on every later REPL record); the item list
is gone except the head.
INVARIANT — a long transaction is fully described by start_lsa .. last_lsa
in the log, not by its staged item list. la_set_repl_log enforces it by
refusing to append once is_long_trans is set, and by keeping last_lsa
current on every record.
At commit, la_apply_repl_log walks the items via la_get_next_repl_item,
which dispatches on the flag: is_long_trans true →
la_get_next_repl_item_from_log (item, last_lsa) (re-read log); false →
la_get_next_repl_item_from_list (item), which is return item->next. The
from-log path is the expensive one:
// la_get_next_repl_item_from_log -- src/transaction/log_applier.c LSA_COPY (&curr_lsa, &item->lsa); while (!LSA_ISNULL (&curr_lsa)) { curr_log_record = LOG_GET_LOG_RECORD_HEADER (la_get_page (curr_lsa.pageid), &curr_lsa); // ... condensed: snapshot start-record trid into prev_repl_log_record ... if (!LSA_EQ (&curr_lsa, &prev_repl_lsa) && prev_repl_log_record->trid == curr_log_record->trid) { if (LSA_GT (&curr_lsa, last_lsa) || curr_log_record->type == LOG_COMMIT || curr_log_record->type == LOG_ABORT || LSA_GE (&curr_lsa, &la_Info.act_log.log_hdr->eof_lsa)) break; if (curr_log_record->type == LOG_REPLICATION_DATA || curr_log_record->type == LOG_REPLICATION_STATEMENT) { next_item = la_make_repl_item (...); break; } } la_release_page_buffer (curr_lsa.pageid); LSA_COPY (&curr_lsa, &curr_log_record->forw_lsa); /* follow forward chain */ }Every branch of the loop is exercised:
- First iteration seeds
prev_repl_log_recordwith the start-record trid (theprev_repl_log_record == NULLclause, condensed above). - Same LSA as start (
LSA_EQ) — skip; this is why the loop never returns the item it was handed. - Different trid — interleaved transaction; release page, follow
forw_lsa. - Same trid, past
last_lsa/ COMMIT / ABORT / EOF — exhausted,breakwithnext_item == NULL. - Same trid, a REPL record —
la_make_repl_item,break.
Each retrieval follows forw_lsa forward, so applying the whole transaction is
O(total-log-records-spanned) — the trade-off in takeaway 2.
11.2 Role change: demoting the slave on a planned failover
Section titled “11.2 Role change: demoting the slave on a planned failover”The master writes a LOG_DUMMY_HA_SERVER_STATE marker on every HA server-state
change (cub_master drives these on failover — cross-link
cubrid-heartbeat.md). The applier handles it in la_log_record_process:
// la_log_record_process, case LOG_DUMMY_HA_SERVER_STATE -- src/transaction/log_applier.c ha_server_state = la_get_ha_server_state (pg_ptr, final); /* NULL -> log + break */ if (ha_server_state->state != HA_SERVER_STATE_ACTIVE && ha_server_state->state != HA_SERVER_STATE_TO_BE_STANDBY) /* demotion */ { if (la_Info.db_lockf_vdes != NULL_VOLDES) /* <- still hold the db lock */ { la_Info.is_role_changed = true; return ER_INTERRUPTED; } /* <- clean stop */ } else if (la_is_repl_lists_empty ()) /* benign: la_log_commit (true) */Branches:
- Leaves ACTIVE/TO_BE_STANDBY and db lock held — peer is no longer a
master to follow; set
is_role_changed, returnER_INTERRUPTED(the agreed clean-shutdown signal, not an error). - Same demotion, lock not held — nothing to release; fall through.
- ACTIVE/TO_BE_STANDBY, lists empty — benign; record the timestamp, force a commit so the bookmark stays current.
- ACTIVE/TO_BE_STANDBY, lists non-empty — nothing here; pending transactions apply normally.
ER_INTERRUPTED propagates to the la_apply_log_file main loop, which on
is_role_changed == true calls la_unlock_dbname (..., clear_owner = true),
releasing ownership of the db lock so a new master can claim it.
(LOG_DUMMY_CRASH_RECOVERY, the case just above, also returns
ER_INTERRUPTED but without is_role_changed — there the interrupt forces a
refetch and keeps the lock.)
la_change_state is the complementary half: once per main-loop iteration it
reports the applier’s own HA_LOG_APPLIER_STATE, driven by the log header’s
ha_server_state / ha_file_status. Its first branch is an early-out — when
last_server_state, last_file_state, and last_is_end_of_record all still
equal the current values, return NO_ERROR. Otherwise it chooses a target state
through a two-level switch:
- Caught up (
is_end_of_record && ha_file_status == LOG_HA_FILESTAT_SYNCHRONIZED):ACTIVE/TO_BE_STANDBY/TO_BE_ACTIVE→WORKING;DEAD/STANDBY/MAINTENANCE→DONE(and, unlessDEAD,la_clear_all_repl_and_commit_listflushes pending lists, since a standby master produces no commits to drain them). - Still catching up (else): every live state →
RECOVERING(two switch cases with different already-in-state guards, but both targetRECOVERING). - Unknown
ha_server_state(either switch):er_log_debug ("BUG ..."),return ER_FAILED.
On a real transition — new_state != HA_LOG_APPLIER_STATE_NA and, after a
second apply_state == new_state early-out, the new state actually differs —
la_change_state forces a commit (la_log_commit (true)) before
boot_notify_ha_log_applier_state, so the reported bookmark is durable.
INVARIANT — the applier never advertises a new state without first making its
last-applied bookmark durable. Enforced by the unconditional la_log_commit (true) ahead of boot_notify_ha_log_applier_state.
11.3 Table-level filtering: REPL_FILTER_TYPE and per-item evaluation
Section titled “11.3 Table-level filtering: REPL_FILTER_TYPE and per-item evaluation”The slave can replicate only some tables (INCLUDE) or all-but-some
(EXCLUDE). The policy is one enum plus a parallel-array filter struct.
// REPL_FILTER_TYPE -- src/transaction/log_applier.htypedef enum { REPL_FILTER_NONE, REPL_FILTER_INCLUDE_TBL, REPL_FILTER_EXCLUDE_TBL } REPL_FILTER_TYPE;| Enumerator | Role | Why it exists |
|---|---|---|
REPL_FILTER_NONE | la_need_filter_out returns false at once. | Default; zero overhead when replicating everything. |
REPL_FILTER_INCLUDE_TBL | Allow-list: drop unless the class is listed. | ”Replicate only these tables.” |
REPL_FILTER_EXCLUDE_TBL | Deny-list: drop if the class is listed. | ”Replicate all except these tables.” |
The runtime state LA_REPL_FILTER (la_Info.repl_filter) is a parallel array:
// struct la_repl_filter -- src/transaction/log_applier.cstruct la_repl_filter { char **list; int list_size; int num_filters; REPL_FILTER_TYPE type; };| Field | Role | Why it exists |
|---|---|---|
list | strdup’d class names from the filter file. | The allow/deny set; grown in LA_NUM_REPL_FILTER chunks by la_add_repl_filter. |
list_size | Allocated capacity of list. | Tells la_add_repl_filter when to realloc; distinct from entries used. |
num_filters | Populated entries (<= list_size). | Iteration bound in la_need_filter_out; a count, not a capacity. |
type | The REPL_FILTER_TYPE policy. | Selects allow vs deny, or short-circuits to “no filter”. |
la_create_repl_filter loads it once at startup. Branch map:
type=PRM_ID_HA_REPL_FILTER_TYPE;REPL_FILTER_NONE→ return now (no read).- Filter file =
PRM_ID_HA_REPL_FILTER_FILE; empty orfopenfailure →ER_HA_LA_REPL_FILTER_GENERIC; a relative path resolves viaenvvar_confdir_file. - Per line:
trim, strip newline, skip blanks; validate user/identifier length (over-long →goto error_return); normalise viasm_user_specified_name, resolve vialocator_find_class. A missing class is non-fatal — the error is raised inside aner_stack_push/er_stack_poppair (so it is emitted then immediately popped off the error stack) and the entry is still added viala_add_repl_filter, since the table may be created later. error_returncloses the file, callsla_destroy_repl_filter.
The filter is evaluated per item at apply time, not at staging — in the
la_apply_repl_log loop, the apply dispatch runs only when
LSA_GT (&item->lsa, &la_Info.last_committed_rep_lsa) && la_need_filter_out (item) == false.
la_need_filter_out itself:
// la_need_filter_out -- src/transaction/log_applier.c if (filter->type == REPL_FILTER_NONE || (item->log_type == LOG_REPLICATION_STATEMENT && item->item_type != CUBRID_STMT_TRUNCATE) || strcasecmp (class_name, CT_SERIAL_NAME) == 0) return false; /* never filter these */ for (i = 0; i < filter->num_filters; i++) if (strcasecmp (filter->list[i], class_name) == 0) { filter_found = true; break; } if ((filter->type == REPL_FILTER_INCLUDE_TBL && filter_found == false) || (filter->type == REPL_FILTER_EXCLUDE_TBL && filter_found == true)) return true; return false;So statement replication (except TRUNCATE) and the serial catalog
(CT_SERIAL_NAME) are never filtered; otherwise INCLUDE drops an absent class
and EXCLUDE drops a present one.
Cost note for modifiers: the class name comes from a la_get_recdes walk in
la_apply_* before la_need_filter_out runs, so filtered events still pay the
record-description decode — the filter saves the slave apply, not the log read.
11.4 TDE: sharing data keys over a unix socket
Section titled “11.4 TDE: sharing data keys over a unix socket”When the master encrypts log pages (TDE), copylogdb ships ciphertext and
applylogdb decrypts each fetched page — both behind the
UNSTABLE_TDE_FOR_REPLICATION_LOG guard (off by default).
Note on
la_load_tde. No symbol namedla_load_tdeexists; the conceptual “applier loads-and-serves the keys” role isla_start_dk_sharing(listener setup) plus its server threadla_process_dk_request(the real counterpart tocopylogdb’slogwr_load_tde). Grep for those two.
The decrypt point is la_log_fetch (and the archive path): under the guard, if
LOG_IS_PAGE_TDE_ENCRYPTED (&cache_buffer->logpage) it calls
tde_decrypt_log_page (..., logwr_get_tde_algorithm (...), ...) in place.
The keys come from the applier, which neither daemon owns intrinsically. In
la_apply_log_file it calls tde_get_data_keys (), sets
tde_Cipher.is_loaded, and calls la_start_dk_sharing — which builds the path
via fileio_make_ha_sock_name (..., TDE_HA_SOCK_NAME), unlinks any stale node,
socket/bind/listen, then pthread_create (la_process_dk_request) (each
failure → a distinct ER_TDE_DK_SHARING_*). The client logwr_load_tde
connects, writes its log_path as a handshake, reads a status word, and on
success reads a TDE_DATA_KEY_SET:
// logwr_load_tde -- src/transaction/log_writer.c fileio_make_ha_sock_name (sock_path, logwr_Gl.log_path, TDE_HA_SOCK_NAME); // ... socket / connect / write(log_path) / read(err_msg) ... if (err_msg != NO_ERROR) { close (client_sockfd); return err_msg; } /* refused */ // ... read TDE_DATA_KEY_SET dks; memcpy perm_key / temp_key / log_key ... tde_Cipher.is_loaded = true;The server thread la_process_dk_request validates each client before handing
over keys:
// la_process_dk_request -- src/transaction/log_applier.c if (error == NO_ERROR) /* prior read may set ER_TDE_DK_SHARING_SOCK_READ */ { if (!tde_is_loaded ()) error = ER_TDE_CIPHER_IS_NOT_LOADED; /* <- keys not ready yet */ else if (memcmp (buf, la_Info.log_path, PATH_MAX) != 0) error = ER_TDE_WRONG_DK_REQUEST; /* <- client's log_path mismatch */ } // ... write `error` to client first; only if NO_ERROR send TDE_DATA_KEY_SET ...The status word is always sent first (so logwr_load_tde learns why it was
refused), and only the all-clear path writes the TDE_DATA_KEY_SET, after which
la_log_fetch can decrypt pages. Full handshake: applier tde_get_data_keys +
listen → client connects, writes log_path → applier returns the status word →
on NO_ERROR, sends the TDE_DATA_KEY_SET.
LIMITATION — TDE-for-replication is compile-gated and off by default. Every
code path in this section sits behind #ifdef UNSTABLE_TDE_FOR_REPLICATION_LOG;
a stock build replicates plaintext log pages and never opens the key socket.
On the master, tde_encrypted rides on each staged LOG_REPL_RECORD (bool tde_encrypted in struct log_repl, replication.h). At emission in
log_manager.c, if (repl_rec->tde_encrypted) triggers
prior_set_tde_encrypted (node, repl_rec->rcvindex), so the redo node inherits
the flag. Chapter 2 covers where tde_encrypted is first set; here the point is
only that it survives onto the prior-list node.
11.5 Abort and teardown cleanup
Section titled “11.5 Abort and teardown cleanup”An abort is not freed where it is read. In la_log_record_process a
LOG_ABORT only queues itself — la_add_node_into_la_commit_list (lrec->trid, final, LOG_ABORT, 0) — exactly like a commit, so it is processed in LSA order
when la_apply_commit_list reaches it. The staged items are released later, in
la_apply_repl_log, whose very first branch short-circuits the abort:
// la_apply_repl_log -- src/transaction/log_applier.c if (rectype == LOG_ABORT) { la_clear_applied_info (apply); /* free items + reset LSAs, then return */ return NO_ERROR; } if (apply->head == NULL || LSA_LE (commit_lsa, &la_Info.last_committed_lsa)) { if (rectype == LOG_SYSOP_END) la_free_all_repl_items (apply); /* preserve slot LSAs */ else la_clear_applied_info (apply); /* full reset */ return NO_ERROR; }So la_clear_applied_info (the per-LA_APPLY reset: la_free_all_repl_items
then null start_lsa / last_lsa / tranid) is reached on abort and on an
empty / already-committed apply list, while the LOG_SYSOP_END arm of that same
guard keeps the slot LSAs via la_free_all_repl_items. Because
la_free_all_repl_items also clears is_long_trans, a reused slot never
inherits the long-transaction flag from a prior occupant.
la_free_repl_items_by_tranid is the other cleanup entry point, distinct from
the abort path. It is called (a) from la_clear_all_repl_and_commit_list when
la_change_state drops to a non-DEAD standby (11.2), and (b) from the
LOG_COMMIT / LOG_SYSOP_END else-branch in la_log_record_process when the
record’s LSA is <= committed_lsa (a stale, already-applied EOT). It both
clears the staged items and unlinks any commit-list node for that trid:
// la_free_repl_items_by_tranid -- src/transaction/log_applier.c apply = la_find_apply_list (tranid); if (apply) la_clear_applied_info (apply); /* free items + reset LSAs */ // ... walk commit_head; for the node with commit->tranid == tranid, unlink + free_and_init ... if (la_Info.commit_head == NULL) la_Info.commit_tail = NULL; /* keep head/tail consistent */INVARIANT — the commit list’s head/tail are always mutually consistent.
la_free_repl_items_by_tranid enforces it by repairing commit_tail when it
unlinks the last node (the commit->next == NULL arm sets commit_tail = commit->prev) and by resetting commit_tail = NULL whenever commit_head
becomes NULL.
The full LOG_SYSOP_END vs commit dispatch through la_apply_commit_list is
Chapter 9’s concern; here the point is only which leaf frees the slot and which
preserves it.
11.6 Chapter summary — key takeaways
Section titled “11.6 Chapter summary — key takeaways”- A transaction over
LA_MAX_REPL_ITEMSflipsis_long_trans, discards its item list, and is thereafter described only bystart_lsa .. last_lsa;la_set_repl_logrefuses further appends once the flag is set. - Applying a long transaction re-reads REPL records via
la_get_next_repl_item_from_log, followingforw_lsaand skipping foreign-trid / non-REPL records — O(records-spanned) for a bounded footprint. - A demotion arrives as
LOG_DUMMY_HA_SERVER_STATE; on a non-ACTIVE/TO_BE state with the db lock held, the applier setsis_role_changed, returnsER_INTERRUPTED, and the main loop releases the lock (clear_owner = true). la_change_statereports the applier’s own state from the log header and always forces a durable commit beforeboot_notify_ha_log_applier_state.- Filtering is a per-item decision (
la_need_filter_out) over aREPL_FILTER_TYPEallow/deny list; statement replication (except TRUNCATE) and the serial catalog are never filtered, and filtered events still pay thela_get_recdesdecode. - TDE is compile-gated (
UNSTABLE_TDE_FOR_REPLICATION_LOG):applylogdbserves keys (la_start_dk_sharing/la_process_dk_request),copylogdbfetches them (logwr_load_tde), pages decrypt inla_log_fetch, andtde_encryptedreaches the redo node viaprior_set_tde_encrypted. - An abort is queued via
la_add_node_into_la_commit_listlike a commit and freed later inla_apply_repl_log(rectype == LOG_ABORT→la_clear_applied_info);la_free_repl_items_by_tranidis the separate path used on standby teardown and on stale already-applied EOTs, and it is what keeps the commit list’s head/tail consistent.
Position hints as of this revision
Section titled “Position hints as of this revision”The following are line numbers as observed on 2026-06-22; symbols are the canonical anchor and line numbers are hints that decay.
| Symbol | File | Line |
|---|---|---|
logwr_get_log_pages | src/communication/network_interface_cl.c | 9153 |
slogwr_get_log_pages | src/communication/network_interface_sr.cpp | 8500 |
la_apply_log_file (call site) | src/executables/util_cs.c | 3170 |
CT_SERIAL_NAME | src/object/schema_system_catalog_constants.h | 45 |
ws_add_to_repl_obj_list | src/object/work_space.c | 5360 |
do_replicate_statement | src/query/execute_statement.c | 16136 |
qexec_execute_selupd_list | src/query/query_executor.c | 13832 |
serial_update_serial_object | src/query/serial.c | 918 |
btree_update | src/storage/btree.c | 14696 |
btree_insert | src/storage/btree.c | 26876 |
heap_get_class_name | src/storage/heap_file.c | 9718 |
heap_get_class_tde_algorithm | src/storage/heap_file.c | 11083 |
heap_insert_logical | src/storage/heap_file.c | 23460 |
heap_delete_logical | src/storage/heap_file.c | 23676 |
heap_update_logical | src/storage/heap_file.c | 23867 |
locator_insert_force | src/transaction/locator_sr.c | 4938 |
locator_insert_force heap_insert_logical call | src/transaction/locator_sr.c | 5065 |
locator_insert_force add_or_remove_index call | src/transaction/locator_sr.c | 5184 |
locator_update_force | src/transaction/locator_sr.c | 5396 |
locator_update_force heap_update_logical call | src/transaction/locator_sr.c | 6028 |
locator_update_force repl_add_update_lsa guard | src/transaction/locator_sr.c | 6052 |
locator_update_force repl_add_update_lsa call | src/transaction/locator_sr.c | 6055 |
locator_delete_force | src/transaction/locator_sr.c | 6116 |
locator_delete_force_internal | src/transaction/locator_sr.c | 6172 |
locator_delete_force_internal isold_object skip | src/transaction/locator_sr.c | 6251 |
locator_delete_force_internal heap_delete_logical call | src/transaction/locator_sr.c | 6327 |
locator_delete_force_internal add_or_remove_index call | src/transaction/locator_sr.c | 6349 |
locator_delete_force_internal add_or_remove_index_for_moving call | src/transaction/locator_sr.c | 6355 |
locator_force_for_multi_update repl_info flag set | src/transaction/locator_sr.c | 6634 |
locator_force_for_multi_update REPL_INFO_TYPE_RBR_START assignment | src/transaction/locator_sr.c | 6636 |
locator_attribute_info_force | src/transaction/locator_sr.c | 7461 |
locator_attribute_info_force dispatch switch | src/transaction/locator_sr.c | 7569 |
locator_add_or_remove_index | src/transaction/locator_sr.c | 7695 |
locator_add_or_remove_index_internal | src/transaction/locator_sr.c | 7760 |
locator_add_or_remove_index_internal repl_log_insert guard | src/transaction/locator_sr.c | 8038 |
locator_add_or_remove_index_internal repl_log_insert call | src/transaction/locator_sr.c | 8042 |
locator_update_index | src/transaction/locator_sr.c | 8260 |
locator_update_index pk_btid_index discovery | src/transaction/locator_sr.c | 8416 |
locator_update_index repl_log_insert call | src/transaction/locator_sr.c | 8804 |
xrepl_set_info | src/transaction/locator_sr.c | 11707 |
xchksum_insert_repl_log_and_demote_table_lock | src/transaction/locator_sr.c | 12620 |
prior_set_tde_encrypted | src/transaction/log_append.cpp | 1565 |
enum LOG_PRIOR_LSA_LOCK | src/transaction/log_append.hpp | 66 |
LA_DEFAULT_CACHE_BUFFER_SIZE | src/transaction/log_applier.c | 80 |
LA_REPL_LIST_COUNT | src/transaction/log_applier.c | 85 |
LA_PAGE_EXST_IN_ARCHIVE_LOG | src/transaction/log_applier.c | 89 |
LA_LOCK_SUFFIX | src/transaction/log_applier.c | 94 |
LA_MAX_REPL_ITEMS | src/transaction/log_applier.c | 98 |
LA_LOG_IS_IN_ARCHIVE | src/transaction/log_applier.c | 112 |
LA_LOGAREA_SIZE | src/transaction/log_applier.c | 118 |
LA_LOG_READ_ADVANCE_WHEN_DOESNT_FIT | src/transaction/log_applier.c | 119 |
LA_MOVE_INSIDE_RECORD | src/transaction/log_applier.c | 156 |
LA_IS_FLUSH_ERROR | src/transaction/log_applier.c | 174 |
struct la_cache_buffer | src/transaction/log_applier.c | 178 |
struct la_cache_buffer_area | src/transaction/log_applier.c | 191 |
struct la_cache_pb | src/transaction/log_applier.c | 198 |
struct la_repl_filter | src/transaction/log_applier.c | 207 |
la_repl_filter | src/transaction/log_applier.c | 207 |
struct la_act_log | src/transaction/log_applier.c | 216 |
struct la_arv_log | src/transaction/log_applier.c | 227 |
la_item | src/transaction/log_applier.c | 237 |
la_item (struct) | src/transaction/log_applier.c | 237 |
la_apply | src/transaction/log_applier.c | 255 |
la_apply (struct) | src/transaction/log_applier.c | 255 |
la_commit | src/transaction/log_applier.c | 267 |
la_commit (struct) | src/transaction/log_applier.c | 267 |
la_info | src/transaction/log_applier.c | 280 |
struct la_info | src/transaction/log_applier.c | 280 |
repl_lists | src/transaction/log_applier.c | 298 |
commit_head | src/transaction/log_applier.c | 304 |
commit_tail | src/transaction/log_applier.c | 305 |
struct la_recdes_pool | src/transaction/log_applier.c | 383 |
LA_HA_APPLY_INFO | src/transaction/log_applier.c | 393 |
la_init_ha_apply_info | src/transaction/log_applier.c | 606 |
la_log_phypageid | src/transaction/log_applier.c | 630 |
la_log_fetch_from_archive | src/transaction/log_applier.c | 912 |
la_log_fetch | src/transaction/log_applier.c | 1063 |
la_expand_cache_log_buffer | src/transaction/log_applier.c | 1177 |
la_cache_buffer_replace | src/transaction/log_applier.c | 1232 |
la_get_page_buffer | src/transaction/log_applier.c | 1297 |
la_get_page | src/transaction/log_applier.c | 1336 |
la_release_page_buffer | src/transaction/log_applier.c | 1364 |
la_invalidate_page_buffer | src/transaction/log_applier.c | 1418 |
la_find_required_lsa | src/transaction/log_applier.c | 1471 |
la_get_ha_apply_info | src/transaction/log_applier.c | 1514 |
la_insert_ha_apply_info | src/transaction/log_applier.c | 1716 |
la_update_ha_apply_info_start_time | src/transaction/log_applier.c | 1860 |
la_update_ha_apply_info_log_record_time | src/transaction/log_applier.c | 1897 |
la_get_last_ha_applied_info | src/transaction/log_applier.c | 1965 |
la_update_ha_last_applied_info | src/transaction/log_applier.c | 2074 |
la_assign_recdes_from_pool | src/transaction/log_applier.c | 2381 |
la_init_recdes_pool | src/transaction/log_applier.c | 2416 |
la_init_cache_pb | src/transaction/log_applier.c | 2474 |
log_pageid_hash | src/transaction/log_applier.c | 2500 |
la_init_cache_log_buffer | src/transaction/log_applier.c | 2528 |
la_apply_pre | src/transaction/log_applier.c | 2693 |
la_init_repl_lists | src/transaction/log_applier.c | 2773 |
la_is_repl_lists_empty | src/transaction/log_applier.c | 2837 |
la_find_apply_list | src/transaction/log_applier.c | 2860 |
la_add_apply_list | src/transaction/log_applier.c | 2889 |
la_log_copy_fromlog | src/transaction/log_applier.c | 2960 |
la_new_repl_item | src/transaction/log_applier.c | 3012 |
la_add_repl_item | src/transaction/log_applier.c | 3050 |
la_get_item_pk_value | src/transaction/log_applier.c | 3073 |
la_make_repl_item | src/transaction/log_applier.c | 3092 |
la_unlink_repl_item | src/transaction/log_applier.c | 3228 |
la_free_repl_item | src/transaction/log_applier.c | 3266 |
la_free_all_repl_items_except_head | src/transaction/log_applier.c | 3300 |
la_free_and_add_next_repl_item | src/transaction/log_applier.c | 3327 |
la_free_all_repl_items | src/transaction/log_applier.c | 3358 |
la_clear_applied_info | src/transaction/log_applier.c | 3378 |
la_clear_all_repl_and_commit_list | src/transaction/log_applier.c | 3392 |
la_set_repl_log | src/transaction/log_applier.c | 3419 |
la_add_node_into_la_commit_list | src/transaction/log_applier.c | 3473 |
la_retrieve_eot_time | src/transaction/log_applier.c | 3515 |
la_make_room_for_mvcc_insid | src/transaction/log_applier.c | 3669 |
la_make_room_for_mvcc_delid_and_prev_ver | src/transaction/log_applier.c | 3700 |
la_disk_to_obj | src/transaction/log_applier.c | 3732 |
la_get_zipped_data | src/transaction/log_applier.c | 3803 |
la_get_undoredo_diff | src/transaction/log_applier.c | 3880 |
la_get_log_data | src/transaction/log_applier.c | 3949 |
la_get_overflow_recdes | src/transaction/log_applier.c | 4249 |
la_get_next_update_log | src/transaction/log_applier.c | 4393 |
la_get_relocation_recdes | src/transaction/log_applier.c | 4552 |
la_get_recdes | src/transaction/log_applier.c | 4604 |
la_repl_add_object | src/transaction/log_applier.c | 4882 |
la_apply_delete_log | src/transaction/log_applier.c | 5000 |
la_apply_update_log | src/transaction/log_applier.c | 5110 |
la_is_mvcc_class | src/transaction/log_applier.c | 5218 |
la_apply_insert_log | src/transaction/log_applier.c | 5311 |
la_apply_statement_log | src/transaction/log_applier.c | 5496 |
la_apply_repl_log | src/transaction/log_applier.c | 5739 |
la_apply_commit_list | src/transaction/log_applier.c | 5920 |
la_free_repl_items_by_tranid | src/transaction/log_applier.c | 5973 |
la_get_next_repl_item | src/transaction/log_applier.c | 6024 |
la_get_next_repl_item_from_list | src/transaction/log_applier.c | 6036 |
la_get_next_repl_item_from_log | src/transaction/log_applier.c | 6043 |
la_log_record_process | src/transaction/log_applier.c | 6101 |
la_change_state | src/transaction/log_applier.c | 6397 |
la_log_commit | src/transaction/log_applier.c | 6531 |
la_commit_transaction | src/transaction/log_applier.c | 6647 |
la_check_duplicated | src/transaction/log_applier.c | 6796 |
la_init | src/transaction/log_applier.c | 6917 |
la_remove_archive_logs | src/transaction/log_applier.c | 7490 |
la_need_filter_out | src/transaction/log_applier.c | 7723 |
la_add_repl_filter | src/transaction/log_applier.c | 7784 |
la_create_repl_filter | src/transaction/log_applier.c | 7831 |
la_apply_log_file | src/transaction/log_applier.c | 8074 |
la_start_dk_sharing | src/transaction/log_applier.c | 8743 |
la_process_dk_request | src/transaction/log_applier.c | 8796 |
REPL_FILTER_TYPE | src/transaction/log_applier.h | 48 |
log_does_allow_replication | src/transaction/log_comm.c | 272 |
log_tdes | src/transaction/log_impl.h | 475 |
num_repl_records | src/transaction/log_impl.h | 522 |
cur_repl_record | src/transaction/log_impl.h | 523 |
append_repl_recidx | src/transaction/log_impl.h | 524 |
fl_mark_repl_recidx | src/transaction/log_impl.h | 525 |
repl_records | src/transaction/log_impl.h | 526 |
repl_insert_lsa | src/transaction/log_impl.h | 527 |
repl_update_lsa | src/transaction/log_impl.h | 528 |
suppress_replication | src/transaction/log_impl.h | 531 |
log_append_undoredo_crumbs repl_insert/update_lsa stamp | src/transaction/log_manager.c | 2198 |
log_append_redo_crumbs repl_insert/update_lsa stamp | src/transaction/log_manager.c | 2463 |
log_sysop_commit_internal | src/transaction/log_manager.c | 3825 |
log_sysop_abort | src/transaction/log_manager.c | 4038 |
log_append_repl_info_internal | src/transaction/log_manager.c | 4555 |
prior_set_tde_encrypted | src/transaction/log_manager.c | 4587 |
log_append_repl_info | src/transaction/log_manager.c | 4623 |
log_append_repl_info_with_lock | src/transaction/log_manager.c | 4629 |
log_append_repl_info_and_commit_log | src/transaction/log_manager.c | 4647 |
log_append_donetime_internal | src/transaction/log_manager.c | 4679 |
log_append_commit_log | src/transaction/log_manager.c | 4779 |
log_append_commit_log_with_lock | src/transaction/log_manager.c | 4802 |
log_append_supplemental_info | src/transaction/log_manager.c | 4837 |
log_commit_local | src/transaction/log_manager.c | 5159 |
log_abort_local | src/transaction/log_manager.c | 5277 |
log_commit | src/transaction/log_manager.c | 5352 |
log_complete | src/transaction/log_manager.c | 5653 |
log_complete_for_2pc | src/transaction/log_manager.c | 5758 |
logpb_Arv_page_info_table | src/transaction/log_page_buffer.c | 282 |
logpb_get_guess_archive_num | src/transaction/log_page_buffer.c | 4558 |
logpb_get_archive_num_from_info_table | src/transaction/log_page_buffer.c | 6386 |
LOG_REPLICATION_DATA | src/transaction/log_record.hpp | 116 |
LOG_REPLICATION_STATEMENT | src/transaction/log_record.hpp | 117 |
log_rec_replication | src/transaction/log_record.hpp | 228 |
struct log_rec_replication | src/transaction/log_record.hpp | 228 |
logtb_free_tran_index | src/transaction/log_tran_table.c | 1202 |
logtb_clear_tdes | src/transaction/log_tran_table.c | 1493 |
logtb_initialize_tdes | src/transaction/log_tran_table.c | 1614 |
LOGWR_COPY_LOG_BUFFER_NPAGES | src/transaction/log_writer.c | 70 |
logwr_to_physical_pageid | src/transaction/log_writer.c | 195 |
logwr_initialize | src/transaction/log_writer.c | 428 |
logwr_set_hdr_and_flush_info | src/transaction/log_writer.c | 639 |
logwr_writev_append_pages | src/transaction/log_writer.c | 838 |
logwr_flush_all_append_pages | src/transaction/log_writer.c | 1016 |
logwr_archive_active_log | src/transaction/log_writer.c | 1275 |
logwr_write_log_pages | src/transaction/log_writer.c | 1512 |
logwr_copy_log_file | src/transaction/log_writer.c | 1659 |
logwr_load_tde | src/transaction/log_writer.c | 1834 |
logwr_check_page_checksum | src/transaction/log_writer.c | 1978 |
logwr_pack_log_pages | src/transaction/log_writer.c | 2263 |
xlogwr_get_log_pages | src/transaction/log_writer.c | 2571 |
LOGWR_CONTEXT | src/transaction/log_writer.h | 41 |
LOGWR_MODE | src/transaction/log_writer.h | 48 |
LOGWR_COPY_FROM_FIRST_PHY_PAGE_MASK | src/transaction/log_writer.h | 55 |
LOGWR_GLOBAL | src/transaction/log_writer.h | 69 |
LOGWR_ENTRY | src/transaction/log_writer.h | 142 |
LOGWR_INFO | src/transaction/log_writer.h | 157 |
wr_list_mutex | src/transaction/log_writer.h | 160 |
RVREPL_DATA_INSERT | src/transaction/recovery.h | 149 |
RVREPL_DATA_UPDATE | src/transaction/recovery.h | 150 |
RVREPL_DATA_DELETE | src/transaction/recovery.h | 151 |
RVREPL_STATEMENT | src/transaction/recovery.h | 152 |
RVREPL_DATA_UPDATE_START | src/transaction/recovery.h | 153 |
RVREPL_DATA_UPDATE_END | src/transaction/recovery.h | 154 |
REPL_LOG_IS_NOT_EXISTS | src/transaction/replication.c | 43 |
REPL_LOG_IS_FULL | src/transaction/replication.c | 45 |
REPL_LOG_INFO_ALLOC_SIZE | src/transaction/replication.c | 49 |
repl_log_info_alloc | src/transaction/replication.c | 165 |
repl_add_update_lsa | src/transaction/replication.c | 229 |
repl_log_insert | src/transaction/replication.c | 293 |
repl_log_insert rcvindex refinement | src/transaction/replication.c | 344 |
repl_log_insert payload packing | src/transaction/replication.c | 363 |
repl_log_insert lsa switch | src/transaction/replication.c | 428 |
repl_log_insert flush-mark tail | src/transaction/replication.c | 466 |
repl_log_insert_statement | src/transaction/replication.c | 512 |
repl_start_flush_mark | src/transaction/replication.c | 606 |
repl_end_flush_mark | src/transaction/replication.c | 635 |
repl_log_abort_after_lsa | src/transaction/replication.c | 673 |
REPL_INFO_TYPE | src/transaction/replication.h | 43 |
REPL_INFO_TYPE | src/transaction/replication.h | 49 |
repl_info | src/transaction/replication.h | 52 |
repl_info_statement | src/transaction/replication.h | 60 |
log_repl_flush | src/transaction/replication.h | 70 |
enum log_repl_flush | src/transaction/replication.h | 70 |
log_repl | src/transaction/replication.h | 79 |
log_repl (LOG_REPL_RECORD) | src/transaction/replication.h | 79 |
repl_data | src/transaction/replication.h | 85 |
tde_encrypted | src/transaction/replication.h | 88 |
Sources
Section titled “Sources”cubrid-ha-replication.md— the high-level companion. See alsocubrid-log-manager-detail.md(the log it ships) andcubrid-heartbeat.md(failover control).- Raw analyses under
raw/code-analysis/cubrid/distributed/. - Code:
src/transaction/log_applier.{c,h},log_writer.{c,h},replication.{c,h}; master log inlog_manager.c. - Methodology:
knowledge/methodology/code-analysis-detail-doc.md.