CUBRID Prior List — Code-Level Deep Dive
Where this document fits: The high-level analysis
cubrid-prior-list.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 a single prior node from construction through LSA assignment to its drain into the log page buffer.
Contents:
Chapter 1: Data Structure Map
Section titled “Chapter 1: Data Structure Map”The reader question this chapter answers: what exactly is queued on the prior list, and what state does the queue itself carry? Every later chapter — build (Ch 3–4), stamp and splice (Ch 5), detach (Ch 6), copy into pages (Ch 7) — manipulates the six structs and one enum defined here. We cover every field, mark who writes it and when, and end with a pointer diagram.
We do not repeat the producer/consumer theory or the Postgres/InnoDB/Oracle comparison — those live in the high-level companion cubrid-prior-list.md; this is the field-level expansion of its ### LOG_PRIOR_NODE and ### LOG_PRIOR_LSA_INFO overviews. Structs are declared in log_record.hpp (on-disk header + variant payloads) and log_append.hpp (queue node, state, helper inputs); constructors in log_append.cpp.
1.1 The queued unit — log_prior_node (LOG_PRIOR_NODE)
Section titled “1.1 The queued unit — log_prior_node (LOG_PRIOR_NODE)”A LOG_PRIOR_NODE is one heap-allocated log record in flight: built by a producer but not yet copied into the shared LOG_PAGE buffer. The queue is a singly-linked list of these; each node owns up to three malloc’d payload buffers and embeds the record header written verbatim into the log.
// log_prior_node -- src/transaction/log_append.hppstruct log_prior_node{ LOG_RECORD_HEADER log_header; LOG_LSA start_lsa; /* for assertion */ bool tde_encrypted; /* whether the log page which'll contain this node has to be encrypted */ /* data header info */ int data_header_length; char *data_header; /* data info */ int ulength; char *udata; int rlength; char *rdata; LOG_PRIOR_NODE *next;};| Field | Role | Why it exists |
|---|---|---|
log_header | Embedded LOG_RECORD_HEADER (1.3): trid, type, three LSA links. | The node is the log record. Producer fills trid/type; attach fills the LSA links under the mutex (Ch 5). |
start_lsa | Snapshot of the assigned prior_lsa at attach. Comment: “for assertion”. | The drain asserts the copied byte lands here — drift check; never read by recovery. |
tde_encrypted | Flag: the log page holding this node must be TDE-encrypted. | Set at build by prior_set_tde_encrypted (Ch 3, Ch 10); propagated per-record to the page by the drain. |
data_header_length | Byte length of data_header. | The variant header is type-dependent in size; the length lets the drain copy it blindly. |
data_header | malloc’d buffer for the type-specific header (a log_rec_* struct). | Separates the fixed log_header from the variant header (Ch 3–4). |
ulength | Byte length of udata. May be negative as a zip / no-data sentinel (Ch 4). | Tells the drain how many undo bytes to copy; feeds list_size. |
udata | malloc’d undo buffer (possibly zlib-compressed). | ”Before image” for rollback/undo; absent for redo-only records. |
rlength | Byte length of rdata. | Redo byte count; same dual role as ulength. |
rdata | malloc’d redo buffer (possibly compressed or diff-encoded). | ”After image” for crash redo; absent for undo-only records. |
next | Forward link to the next node, NULL at tail. | Makes the list a singly-linked FIFO. Written by attach (Ch 5), torn down by the drain (Ch 6). |
Role matrix — live payload segments by record type (log_rectype values):
| Record type group | data_header | udata | rdata |
|---|---|---|---|
LOG_UNDOREDO_DATA / LOG_MVCC_UNDOREDO_DATA / LOG_*_DIFF_UNDOREDO_DATA | undoredo header | undo image | redo image (diff-encoded in DIFF variants — Ch 4) |
LOG_UNDO_DATA / LOG_MVCC_UNDO_DATA | undo header | undo image | empty |
LOG_REDO_DATA / LOG_MVCC_REDO_DATA | redo header | empty | redo image |
LOG_COMMIT / LOG_ABORT and other control records | small fixed header (often LOG_REC_DONETIME) | empty | empty |
Invariant 1.A — payload pointers and lengths agree.
udata != NULLimpliesulength > 0(and symmetrically forrdata/rlength); NULL implies non-positive length. A non-NULL pointer with length 0 makes the drain emit a zero-length segment, corrupting every following record’s offset.
Invariant 1.B — single-owner until attach. Before
prior_lsa_next_recordsplices the node (Ch 5) only the builder holds the pointer andnextis garbage that MUST NOT be read; after attach the list owns it and the producer must not touch it. Use after attach, or a double free in the drain, is a cross-boundary use-after-free.
Figure 1-1 —
LOG_PRIOR_NODE: fixed header, three malloc’d segments, forward link.
1.2 The queue state — log_prior_lsa_info (LOG_PRIOR_LSA_INFO)
Section titled “1.2 The queue state — log_prior_lsa_info (LOG_PRIOR_LSA_INFO)”Exactly one exists in the server: the singleton log_Gl.prior_info (no per-thread or per-volume copy). It holds the list head/tail, the LSN cursor, byte accounting, the detached “flush” sublist, and the attach-vs-detach mutex.
// log_prior_lsa_info -- src/transaction/log_append.hppstruct log_prior_lsa_info{ LOG_LSA prior_lsa; LOG_LSA prev_lsa; /* list */ LOG_PRIOR_NODE *prior_list_header; LOG_PRIOR_NODE *prior_list_tail; INT64 list_size; /* bytes */ /* flush list */ LOG_PRIOR_NODE *prior_flush_list_header; std::mutex prior_lsa_mutex; log_prior_lsa_info ();};The constructor log_prior_lsa_info::log_prior_lsa_info (log_append.cpp) zero-inits cursors to NULL_LSA, pointers to NULL, list_size to 0.
| Field | Mutated by | Role | Why it exists |
|---|---|---|---|
prior_lsa | producer (attach, under mutex) | The next LSN to hand out; each attach copies it into the node then advances past the bytes. | Monotonic LSN allocator; bumped only under the mutex, so LSNs are gap-free and strictly increasing. |
prev_lsa | producer (attach, under mutex) | Address of the previous attached record; copied into log_header.back_lsa, then overwritten. | Builds the whole-log backward chain. Distinct from per-transaction prev_tranlsa (1.3). |
prior_list_header | producer (first attach) + drain (detach) | Head of the FIFO of un-drained nodes. | Where the drain starts; NULL means empty. |
prior_list_tail | producer (every attach) | Tail of the FIFO; new nodes splice here. | O(1) append. |
list_size | producer (attach, +=) + drain (reset at detach) | Byte total (sizeof(node) + data_header_length + ulength + rlength). | Backpressure vs logpb_get_memsize() (Ch 9). |
prior_flush_list_header | drain | Head of the list after detach; the drain re-parents the chain here. | Decouples what producers append to from what the drain consumes. |
prior_lsa_mutex | both | std::mutex serialising LSN-assign + attach against detach. | Makes the multi-producer / single-consumer hand-off safe; held only for the splice and cursor bump (Ch 5). |
Invariant 1.C — cursors advance only under
prior_lsa_mutex. Bothprior_lsaandprev_lsaare plainLOG_LSA(not atomic); their only writers are inside the lock inprior_lsa_next_record_internal(Ch 5). Concurrent bumps would let two records claim overlapping byte ranges and make the log unrecoverable.
Invariant 1.D — header/tail consistency.
prior_list_header == NULLiffprior_list_tail == NULLiff the queue is empty, enforced by the attach branchif (tail == NULL) { header = tail = node; } else { tail->next = node; tail = node; }(Ch 5). Nulling one without the other loses records or appends to a stale tail.
Invariant 1.E —
list_sizetracks exactly the un-drained nodes. Incremented per attach, zeroed when the drain detaches the list. Drift high fires backpressure spuriously; drift low lets the queue grow unbounded and exhaust the log-area memory budget.
Figure 1-2 —
log_prior_lsa_info: producers grow the header/tail FIFO; the drain re-parents it under prior_flush_list_header.
1.3 The embedded record header — log_rec_header (LOG_RECORD_HEADER)
Section titled “1.3 The embedded record header — log_rec_header (LOG_RECORD_HEADER)”log_prior_node.log_header is a LOG_RECORD_HEADER — the fixed-size prefix of every log record in memory and on disk, read back byte-for-byte by recovery.
// log_rec_header -- src/transaction/log_record.hppstruct log_rec_header{ LOG_LSA prev_tranlsa; /* Address of previous log record for the same transaction */ LOG_LSA back_lsa; /* Backward log address */ LOG_LSA forw_lsa; /* Forward log address */ TRANID trid; /* Transaction identifier of the log record */ LOG_RECTYPE type; /* Log record type (e.g., commit, abort) */};| Field | Set when | Role | Why it exists |
|---|---|---|---|
prev_tranlsa | attach (Ch 5), from the transaction descriptor | Backward link to the previous record of the same transaction. | Lets rollback/undo walk one transaction in reverse. Distinct from back_lsa. |
back_lsa | attach, from prior_info.prev_lsa | Backward link to the previous record in the whole log. | Global reverse scan; consumes prev_lsa (1.2). |
forw_lsa | attach, from the next prior_lsa | Forward link to the next record. | Forward scan during the redo pass. |
trid | build (Ch 3) | Owning transaction id. | Recovery groups by transaction; vacuum/CDC filter on it. |
type | build | A log_rectype value. | Dispatches the recovery handler; tells the drain which segments exist (1.1). |
Two backward chains: back_lsa (all records) vs prev_tranlsa (this transaction). LOG_LSA is the {pageid, offset} pair from log_lsa.hpp; NULL_LSA marks “no such record.” The variant header in data_header typically begins with a LOG_DATA — what log_data_addr (1.4) feeds.
1.4 Producer helper inputs — log_crumb and log_data_addr
Section titled “1.4 Producer helper inputs — log_crumb and log_data_addr”Not stored in the node — inputs to the build path; Ch 3–4 dissect their consumers prior_lsa_alloc_and_copy_data / prior_lsa_alloc_and_copy_crumbs.
// log_crumb -- src/transaction/log_append.hppstruct log_crumb{ int length; const void *data;};| Field | Role | Why it exists |
|---|---|---|
length | Byte length of this fragment. | A “crumb” is one piece of a scatter-gather payload. |
data | const pointer to caller-owned bytes (not yet copied). | Lets the caller describe undo/redo as non-contiguous fragments; the build path concatenates them into one buffer. const signals copy-out. |
// log_data_addr -- src/transaction/log_append.hppstruct log_data_addr{ using offset_type = PGLENGTH; const VFID *vfid; /* File the page belongs to, or NULL when not file-associated */ PAGE_PTR pgptr; offset_type offset; /* Offset or slot */ // + ctors};| Field | Role | Why it exists |
|---|---|---|
vfid | File id of the modified page, or NULL when not file-backed. | Stored into data_header (LOG_DATA.volid); NULL is a legal “no file” branch. |
pgptr | Pointer to the in-memory page being changed. | The build path reads page/volume id from it to fill LOG_DATA. |
offset | Offset (or slot) within the page. | Recovery target offset; high bits double as LOG_RV_RECORD_* flags (LOG_RV_RECORD_SET_MODIFY_MODE) — Ch 10. |
LOG_DATA_ADDR_INITIALIZER is { NULL, NULL, 0 } (marked todo: remove me).
1.5 The append-side state — log_append_info (LOG_APPEND_INFO)
Section titled “1.5 The append-side state — log_append_info (LOG_APPEND_INFO)”LOG_APPEND_INFO is the flush cursor on the far side — the second of the “two cursors” (Ch 2), the durability state Ch 7–8 read and write. Distinguish prior_lsa (queue) from nxio_lsa (durability).
| Field | Mutated by | Role | Why it exists |
|---|---|---|---|
vdes | log init / archiving | Volume descriptor (fd) of the active log volume (init NULL_VOLDES). | The flush path writes pages through it. |
nxio_lsa | flush (Ch 8) | Atomic. Lowest LSN not yet written to disk — the WAL frontier. | Read lock-free by commit waiters; atomic as the one append-side field read without the mutex. Accessed via get_nxio_lsa/set_nxio_lsa. |
prev_lsa | append/copy (Ch 7) | LSA of the last record copied into the pages. | Append-side mirror of the queue’s prev_lsa; header note: not really belonging here. |
log_pgptr | append/copy | The currently fixed authoritative LOG_PAGE being filled. | Destination the drain copies node bytes into (Ch 7). |
appending_page_tde_encrypted | append/copy | Whether the appended page must be TDE-encrypted. | Page-level counterpart of node-level tde_encrypted (1.1). |
The WAL safety property is nxio_lsa <= prior_lsa always — a record must be queued (and given an LSN) before it can flush; Ch 8 traces how the atomic load and under-mutex bump preserve this.
1.6 The locking selector — LOG_PRIOR_LSA_LOCK
Section titled “1.6 The locking selector — LOG_PRIOR_LSA_LOCK”// LOG_PRIOR_LSA_LOCK -- src/transaction/log_append.hppenum LOG_PRIOR_LSA_LOCK{ LOG_PRIOR_LSA_WITHOUT_LOCK = 0, LOG_PRIOR_LSA_WITH_LOCK = 1};| Value | Meaning | Why it exists |
|---|---|---|
LOG_PRIOR_LSA_WITHOUT_LOCK (0) | Caller has not taken prior_lsa_mutex; the helper must take it. | The common path: prior_lsa_next_record enters with the lock unheld. |
LOG_PRIOR_LSA_WITH_LOCK (1) | Caller already holds prior_lsa_mutex; the helper must not re-lock. | For commit/postpone records (Ch 5/Ch 10) that update transaction state and attach under one lock. Passed via prior_lsa_next_record_with_lock. |
This is the with_lock parameter threaded into prior_lsa_next_record_internal. Re-locking when held (or skipping when not) is the classic double-lock / lost-update bug; the enum makes lock state explicit.
1.7 Chapter summary — key takeaways
Section titled “1.7 Chapter summary — key takeaways”- A
LOG_PRIOR_NODEis one in-flight log record: embeddedLOG_RECORD_HEADER, three parallel(length, ptr)segments (data_header,udata,rdata), atde_encryptedflag, assertion-onlystart_lsa, and anextlink. Which segments are live followslog_header.type(1.1). log_Gl.prior_infois the singleLOG_PRIOR_LSA_INFOholding the queue: FIFO head/tail, theprior_lsa/prev_lsacursors,list_size, the detachedprior_flush_list_header, andprior_lsa_mutex. Producers grow the FIFO and bump cursors only under the mutex; the drain detaches and resetslist_size.- Two distinct backward chains in
log_rec_header:back_lsalinks every record,prev_tranlsalinks only one transaction’s. log_crumbandlog_data_addrare build-time inputs, not stored state — crumbs are scatter-gather fragments;log_data_addrcarries target file/page/offset (offset high bits =LOG_RV_RECORD_*mode).LOG_APPEND_INFOis the durability-side cursor: atomicnxio_lsais the WAL frontier read lock-free by commit waiters; the safety property isnxio_lsa <= prior_lsa(Ch 8).LOG_PRIOR_LSA_LOCKmakes the caller’s lock state explicit so the attach helper neither double-locks nor skips the mutex — the linchpin of the race-free multi-producer attach.- The invariants later chapters must preserve: payload/length agreement (1.A), single-owner-until-attach (1.B), cursors only under the mutex (1.C), header/tail consistency (1.D), exact
list_sizeaccounting (1.E).
Chapter 2: Initialization Memory and the Two Cursors
Section titled “Chapter 2: Initialization Memory and the Two Cursors”Chapter 1 mapped the structs at rest. This chapter answers the boot-time
question: where do these structures come from, how is a per-node piece of
memory born and reclaimed, and how does the producer-side prior_lsa cursor
stay welded to the page-side append_lsa cursor? The answer is in three
places — the queue state’s C++ constructor, the LSN-reset macros in
log_initialize_internal, and the per-thread compression scratch. The why
lives in the high-level companion (cubrid-prior-list.md §“CUBRID’s
Approach”).
2.1 The queue state is constructed empty
Section titled “2.1 The queue state is constructed empty”log_Gl.prior_info is a log_prior_lsa_info value member of the global
log_Gl — not malloc’d, not memset, but C++ default-constructed with
log_Gl, every field given a deliberate empty value:
// log_prior_lsa_info::log_prior_lsa_info -- src/transaction/log_append.cpplog_prior_lsa_info::log_prior_lsa_info () : prior_lsa (NULL_LSA), prev_lsa (NULL_LSA) , prior_list_header (NULL), prior_list_tail (NULL), list_size (0) , prior_flush_list_header (NULL), prior_lsa_mutex () { }Each field’s construction value, role at boot, and reason for existing:
| Field | Role | Why it exists |
|---|---|---|
prior_lsa | NULL_LSA; next LSA to hand out, re-seeded before append | Cursor the producer advances under the mutex. |
prev_lsa | NULL_LSA; LSA of the last attached node, re-seeded before append | Feeds the new node’s back_lsa (physical back-link). |
prior_list_header | NULL; empty queue head | Producer attach point; drain detach point. |
prior_list_tail | NULL; empty queue tail | O(1) append-at-tail target. |
list_size | 0; no bytes queued | Backpressure trigger (Ch. 9); accounting starts at zero. |
prior_flush_list_header | NULL; drain not running | Single-drain liveness flag (Ch. 6); NULL outside the drain CS. |
prior_lsa_mutex | default std::mutex, unlocked | The only mutex on the producer path. |
The constructor does not establish run-time cursor values — prior_lsa
/ prev_lsa are NULL_LSA here, not usable LSNs; they become real only
after log_initialize_internal seeds them (§2.3). Construction makes the
queue coherent and empty; seeding makes it live.
2.2 The two reset macros — one writer for two cursors each
Section titled “2.2 The two reset macros — one writer for two cursors each”The coupling between the prior and page-side cursors is not emergent; it is
enforced by construction through two helpers (misnamed “macros” — actually
functions in log_append.cpp) that each write two locations at once:
// LOG_RESET_APPEND_LSA -- src/transaction/log_append.cppLOG_RESET_APPEND_LSA (const LOG_LSA *lsa) { log_Gl.hdr.append_lsa = *lsa; /* page-side append cursor */ log_Gl.prior_info.prior_lsa = *lsa; } /* producer-side prior cursor -- SAME value */// LOG_RESET_PREV_LSA -- src/transaction/log_append.cppLOG_RESET_PREV_LSA (const LOG_LSA *lsa) { log_Gl.append.prev_lsa = *lsa; /* page-side prev-record cursor */ log_Gl.prior_info.prev_lsa = *lsa; } /* producer-side prev-record cursor -- SAME value */The key detail: there is no code path that sets prior_lsa without also
setting hdr.append_lsa to the same value, nor prior_info.prev_lsa
without append.prev_lsa. The writer is always one of these two functions —
the mechanical basis of the cursor-coupling invariant (§2.6). The
// todo - concurrency safe-guard comment is honest: these run under
LOG_CS during boot/recovery when no producer can race, so the dual write
is effectively atomic and never invoked on a live system. The third helper,
LOG_APPEND_PTR, returns log_pgptr->area + hdr.append_lsa.offset — the
byte address at the page-side cursor, read at boot to recover the EOF record
(Branch C below).
2.3 Where the cursors are seeded during log_initialize_internal
Section titled “2.3 Where the cursors are seeded during log_initialize_internal”log_initialize_internal (log_manager.c) is the boot/restart entry,
running inside LOG_CS_ENTER (so the dual-write reset macros are safe). It
seeds the two cursors at one of three points by startup branch:
Branch A — media crash, no mountable active log. The header is
synthesised, append_lsa forced to {LOGPAGEID_MAX, 0}, then
LOG_RESET_APPEND_LSA (&log_Gl.hdr.append_lsa) drags prior_lsa to the
same value (inline comment: /* sync append_lsa to prior_lsa */).
Branch B — recovery needed (normal crash). The header is read from the
mounted active log; log_recovery leaves append_lsa at the true end of
log, resetting the cursors through the same macros as it replays.
Branch C — clean shutdown, nothing to recover. The illustrative path; it
shows the coupling-to-the-page explicitly. After
logpb_fetch_start_append_page positions the page-side cursor, the EOF
record is read off the page and prev_lsa seeded from its back_lsa:
// log_initialize_internal -- src/transaction/log_manager.c (clean-shutdown branch, condensed)if (log_Gl.hdr.append_lsa.pageid > 0 || log_Gl.hdr.append_lsa.offset > 0) { eof = (LOG_RECORD_HEADER *) LOG_APPEND_PTR (); /* read EOF record at append cursor */ LOG_RESET_PREV_LSA (&eof->back_lsa); /* prev_lsa := eof->back_lsa (both copies) */ }Note what is not in Branch C: no explicit LOG_RESET_APPEND_LSA call.
append_lsa was set when the header was fetched (logpb_fetch_header),
leaving prior_lsa stale relative to it — the gap §2.4 closes.
2.4 The closing self-check — the invariant made executable
Section titled “2.4 The closing self-check — the invariant made executable”After the branches converge, log_initialize_internal runs a pair of
defensive equality checks — the cursor-coupling invariant written as code:
// log_initialize_internal -- src/transaction/log_manager.c (post-recovery convergence)if (!LSA_EQ (&log_Gl.append.prev_lsa, &log_Gl.prior_info.prev_lsa)) { assert (0); LOG_RESET_PREV_LSA (&log_Gl.append.prev_lsa); } /* defense: force equal */if (!LSA_EQ (&log_Gl.hdr.append_lsa, &log_Gl.prior_info.prior_lsa)) { assert (0); LOG_RESET_APPEND_LSA (&log_Gl.hdr.append_lsa); } /* defense: force equal */Trace the branches: each check skips when equal (the expected outcome); on
mismatch assert(0) fires in debug, and release builds fall through to the
defense reset, which forces the prior copy to the page-side value —
reconciling Branch C’s missing LOG_RESET_APPEND_LSA. After both checks pass
(or are corrected), the producer begins life with prior_lsa == append_lsa
and prior_info.prev_lsa == append.prev_lsa. The create-database path
reaches the same state more directly — LSA_SET_NULL then
LOG_RESET_PREV_LSA(&log_Gl.append.prev_lsa) starts both prev_lsa copies
at NULL_LSA, in lockstep.
2.5 Per-node memory — malloc by producer, free by drain
Section titled “2.5 Per-node memory — malloc by producer, free by drain”There is no node pool; a LOG_PRIOR_NODE is a free-store object
allocated and reclaimed at opposite ends of the pipeline.
- Producer allocates —
prior_lsa_alloc_and_copy_data(and its sister_crumbs) does the single nodemallocoutside the prior mutex, then zeroes lengths and NULLs pointers:
// prior_lsa_alloc_and_copy_data -- src/transaction/log_append.cpp (condensed)node = (LOG_PRIOR_NODE *) malloc (sizeof (LOG_PRIOR_NODE));if (node == NULL) { er_set (...ER_OUT_OF_VIRTUAL_MEMORY...); return NULL; }node->log_header.type = rec_type; node->tde_encrypted = false;/* data_header / udata / rdata lengths set 0, all four pointers + next set NULL */The per-type generators then malloc data_header / udata / rdata
separately (Ch. 3-4) — up to four heap allocations per record.
- Drain frees — the detached node and all three byte buffers are released
in the drain walk (
logpb_append_prior_lsa_list, Ch. 6) after the bytes reach the log page.
Invariant — node ownership is single and transfers at attach. Before
attach the producer owns the node and may free it on an error path; after
prior_lsa_next_record links it onto the tail under the mutex, ownership
belongs to the drain and the producer must never touch it again. Enforced
structurally: the attach functions return only the assigned LOG_LSA, never
the node pointer, so a correct caller keeps no handle to misuse — a cached
pointer is a use-after-free once the drain frees it.
list_size accounting begins at link time: the attach adds
sizeof(LOG_PRIOR_NODE) + data_header_length + ulength + rlength to
log_Gl.prior_info.list_size — zero from construction until the first
attach; the drain resets it during detach (Ch. 6).
2.6 The cursor-coupling invariant
Section titled “2.6 The cursor-coupling invariant”Invariant — the prior and page-side cursors start, and stay, in lockstep. At the end of initialization:
log_Gl.prior_info.prior_lsa == log_Gl.hdr.append_lsalog_Gl.prior_info.prev_lsa == log_Gl.append.prev_lsaHow it is established. Only LOG_RESET_APPEND_LSA / LOG_RESET_PREV_LSA
write these fields at boot, each setting both copies equal (§2.2); the §2.4
self-check corrects toward the page side on any drift.
How it is maintained at run time. The cursors then diverge bounded by
design: the producer advances prior_lsa under prior_lsa_mutex (Ch. 5)
while append_lsa lags until the drain copies records into pages (Ch. 7), so
append_lsa <= prior_lsa, the gap = queued bytes.
logpb_append_next_record guards every drained node with if (!LSA_EQ (&node->start_lsa, &log_Gl.hdr.append_lsa)) logpb_fatal_error (...), so the
page cursor walks the exact LSAs the prior cursor handed out.
What breaks if violated. Started behind append_lsa, the producer
re-issues already-consumed LSAs — duplicate LSNs, a corrupt log. Started
ahead, the first drained node’s start_lsa fails the guard above and
trips logpb_fatal_error. The boot-time equality is the precondition for
the single-writer-of-LSAs scheme.
2.7 The compression scratch — off to the side
Section titled “2.7 The compression scratch — off to the side”Compression staging memory is not part of the queue and not per-node; it is per-thread scratch letting the producer zlib a payload before allocating the node’s final buffers. Two pieces plus a lifecycle hook.
(1) The zlib contexts (LOG_ZIP), lazy per worker thread — off
THREAD_ENTRY in SERVER_MODE, file statics in SA. log_append_get_zip_undo
(and the identical _redo) in SERVER_MODE resolves
a null thread_p via thread_get_thread_entry_info(), returns NULL if it
stays null, else log_zip_alloc (IO_PAGESIZE)s on first use and returns it;
the SA #else arm returns the file-static log_zip_undo. This
null-thread guard, lazy-alloc-on-first-use, return-NULL/false-on-OOM shape
is shared by get_zip_redo, get_data_ptr, and realloc_data_ptr. The
THREAD_ENTRY fields are constructed NULL and freed in ~thread_entry via
log_zip_free, so each worker owns its compressor — no contention.
(2) The lifecycle hooks. log_append_init_zip (from
logpb_initialize_pool under LOG_CS) sets log_Zip_support = false if
PRM_ID_LOG_COMPRESS is off; else in SERVER_MODE it only sets it true
(contexts come lazily from get_zip_undo/_redo), while the SA #else arm
eagerly allocs log_zip_undo/redo and log_data_ptr = malloc (IO_PAGESIZE * 2), freeing what succeeded and clearing log_Zip_support on any failure.
log_append_final_zip early-returns at if (!log_Zip_support) return;; its
SERVER_MODE arm is then empty (the per-thread destructor cleans up), while
the SA #else arm frees both contexts and log_data_ptr.
(3) The staging buffer log_data_ptr — per-thread (global in SA) scratch
holding the concatenated, uncompressed crumbs long enough to feed
log_zip. Two accessors share the shape from part (1):
// log_append_get_data_ptr -- src/transaction/log_append.cpp (SERVER_MODE, condensed)if (thread_p == NULL) thread_p = thread_get_thread_entry_info ();if (thread_p == NULL) return NULL; /* null-thread early-return */if (thread_p->log_data_ptr == NULL) { /* first use this thread */ thread_p->log_data_length = IO_PAGESIZE * 2; thread_p->log_data_ptr = malloc (thread_p->log_data_length); if (thread_p->log_data_ptr == NULL) /* OOM */ { thread_p->log_data_length = 0; er_set (...ER_OUT_OF_VIRTUAL_MEMORY...); } }return thread_p->log_data_ptr; /* may be NULL on OOM */get_data_ptr (above) lazily mallocs IO_PAGESIZE * 2 on first use.
log_append_realloc_data_ptr adds a grow branch: it grows only when
log_data_length < length, rounding alloc_len up to IO_PAGESIZE via
CEIL_PTVDIV and realloc-ing; on realloc-OOM it frees the old buffer,
zeroes the length, returns false. Both SA #else arms run identical logic
against the file-static log_data_ptr / log_data_length.
The only consumer is the undoredo generator
(prior_lsa_gen_undoredo_record_from_crumbs, Ch. 4); none of it touches
log_Gl.prior_info or prior_lsa_mutex, so it backs “build outside the
mutex” work cheaply.
flowchart LR
subgraph THR["per THREAD_ENTRY (SERVER_MODE)"]
ZU["log_zip_undo\nlazy LOG_ZIP"]
ZR["log_zip_redo\nlazy LOG_ZIP"]
SB["log_data_ptr\npage-rounded scratch"]
end
CR["crumbs in"] --> SB
SB -->|log_zip| ZU
SB -->|log_zip| ZR
ZU --> UD["node->udata\nmalloc'd, compressed"]
ZR --> RD["node->rdata\nmalloc'd, compressed"]
Figure 2-1 — Compression staging is per-thread and disjoint from the queue:
crumbs concatenate into log_data_ptr, compress through the per-thread zlib
contexts, and emit into the node’s own udata/rdata. The prior queue and
its mutex are not touched.
2.8 Chapter summary — key takeaways
Section titled “2.8 Chapter summary — key takeaways”-
The queue state is C++-constructed empty, not zeroed — every field
NULL_LSA/NULL/0, coherent but cursors not yet usable. -
Two reset helpers each write two cursors —
LOG_RESET_APPEND_LSApairshdr.append_lsawithprior_info.prior_lsa,LOG_RESET_PREV_LSApairs the twoprev_lsacopies; the only boot-time writers, so they couple the cursors by construction. -
log_initialize_internalseeds then self-checks — it seeds from the page-side header across the three startup branches, then a closingLSA_EQpair (assert(0)+ defense reset toward the page side) guarantees both copies match. -
The cursor-coupling invariant — cursors start in lockstep and stay bounded (
append_lsa <= prior_lsa, gap = queued bytes); behind duplicates LSNs, ahead tripslogpb_append_next_record’sstart_lsa == append_lsaguard intologpb_fatal_error. -
Nodes are malloc’d by the producer, freed by the drain — no pool. Ownership transfers at attach; the API returns only the LSA, so the producer keeps no handle.
list_sizestarts at zero, bumps at attach. -
Compression scratch is per-thread and disjoint from the queue — lazy per-
THREAD_ENTRYLOG_ZIPcontexts plus a page-roundedlog_data_ptrlet producers compress outsideprior_lsa_mutex.init_ziponly gateslog_Zip_supportin server mode (eager alloc is SA);final_zipfrees only in SA.
Chapter 3: Building a Node Outside the Mutex for Non Undoredo Records
Section titled “Chapter 3: Building a Node Outside the Mutex for Non Undoredo Records”The reader question this chapter answers: how does a transaction turn an append-API call into a fully formed LOG_PRIOR_NODE without touching any global state? Chapter 2 established the two cursors and the mutex guarding them; this chapter stays before that mutex. Everything runs on the calling worker thread’s private stack — private memory, private payload — so the emerging node has never been seen by log_Gl.prior_info. Attachment (Ch 5) and draining (Ch 6) come later.
The entry point is prior_lsa_alloc_and_copy_data. Its sibling prior_lsa_alloc_and_copy_crumbs (Ch 4) handles the undoredo family with compression/diff encoding; this is the non-undoredo path — postpone, dbout-redo, 2PC prepare, end-checkpoint, and a catch-all for every other small record type.
3.1 The LOG_PRIOR_NODE being filled in
Section titled “3.1 The LOG_PRIOR_NODE being filled in”Each generator is handed a freshly-malloc’d LOG_PRIOR_NODE and fills three independently-allocated buffers hanging off it — data header, undo payload, redo payload.
// log_prior_node -- src/transaction/log_append.hppstruct log_prior_node{ LOG_RECORD_HEADER log_header; LOG_LSA start_lsa; /* for assertion */ bool tde_encrypted; int data_header_length; char *data_header; int ulength; char *udata; int rlength; char *rdata; LOG_PRIOR_NODE *next;};| Field | Role | Why it exists |
|---|---|---|
log_header | On-disk LOG_RECORD_HEADER; only .type set here, LSA links / .trid under the mutex (Ch 5). | Copy-out (Ch 7) emits it verbatim. |
start_lsa | Assertion-only copy of the attach LSA; unused here. | Asserts the node landed where promised. |
tde_encrypted | TDE-encryption flag; false here. | Classification deferred for non-undoredo. |
data_header_length | Byte size of data_header. | Copy-out emits exactly this many bytes. |
data_header | Heap buffer for the type-specific header. | Keeps the node generic across record types. |
ulength / udata | Length + heap buffer of the undo payload. | 2PC global-tran info, checkpoint tran array. |
rlength / rdata | Length + heap buffer of the redo payload. | Postpone recovery data, dbout-redo image. |
next | List link once attached. | NULL during construction; attach (Ch 5) wires it. |
Invariant — a node owns exactly the buffers it allocated, and nothing else. Construction never aliases caller pointers: udata/rdata/data_header are always fresh mallocs with memcpy’d contents (or NULL). That lets the caller free its own scratch the instant the call returns, and lets the error path (3.7) blindly free_and_init all three without double-free or freeing borrowed memory.
3.2 prior_lsa_alloc_and_copy_data: allocate, zero, dispatch
Section titled “3.2 prior_lsa_alloc_and_copy_data: allocate, zero, dispatch”The function mallocs the node, zeroes every field by hand (it is malloc’d, not calloc’d, so the three buffer pointers start as garbage), then switches on rec_type.
// prior_lsa_alloc_and_copy_data -- src/transaction/log_append.cppnode = (LOG_PRIOR_NODE *) malloc (sizeof (LOG_PRIOR_NODE));if (node == NULL) { er_set (... ER_OUT_OF_VIRTUAL_MEMORY ...); return NULL; } /* <- nothing to clean up yet */node->log_header.type = rec_type; /* <- only header field set pre-mutex */node->data_header_length = 0; node->data_header = NULL;node->tde_encrypted = false;node->ulength = 0; node->udata = NULL;node->rlength = 0; node->rdata = NULL;node->next = NULL;Setting the pointers NULL before the generator runs makes the unified error path (3.7) safe: buffers a failing generator never allocated are guaranteed NULL.
flowchart TD
A["enter with rec_type"] --> M["malloc node; if NULL return NULL"]
M --> Z["zero header.type, tde, lengths, pointers, next"]
Z --> SW{"switch (rec_type)"}
SW -->|"UNDOREDO / UNDO / REDO / MVCC_* family"| BAD["assert_release(false)<br/>error = ER_FAILED<br/>must use _crumbs path"]
SW -->|LOG_DBEXTERN_REDO_DATA| G1["prior_lsa_gen_dbout_redo_record"]
SW -->|LOG_POSTPONE| G2["assert ulength==0, udata==NULL<br/>prior_lsa_gen_postpone_record"]
SW -->|LOG_2PC_PREPARE| G3["assert addr==NULL<br/>prior_lsa_gen_2pc_prepare_record"]
SW -->|LOG_END_CHKPT| G4["assert addr==NULL<br/>prior_lsa_gen_end_chkpt_record"]
SW -->|"~25 small types: COMMIT, ABORT, SAVEPOINT, RUN_POSTPONE, ..."| G5["assert rlength==0, rdata==NULL<br/>prior_lsa_gen_record"]
SW -->|default| D["fall through, error stays NO_ERROR"]
BAD --> CK{"error_code == NO_ERROR?"}
G1 --> CK
G2 --> CK
G3 --> CK
G4 --> CK
G5 --> CK
D --> CK
CK -->|yes| RET["return node"]
CK -->|no| FREE["free data_header, udata, rdata, node<br/>return NULL"]
Figure 3-1. Branch-complete control flow of prior_lsa_alloc_and_copy_data.
The load-bearing branch fact: the undoredo/MVCC family is a hard error here — those nine rec_types hit assert_release (false) and set error_code = ER_FAILED, because only the crumbs path (Ch 4) carries the compression/diff machinery. The pre-dispatch asserts encode each remaining family’s API contract; the default branch returns a node with all buffers NULL — the path for header-only record types.
3.3 prior_lsa_gen_postpone_record: a redo-only page-addressed record
Section titled “3.3 prior_lsa_gen_postpone_record: a redo-only page-addressed record”LOG_POSTPONE records the address and recovery data of an operation deferred to commit. Its data header is a LOG_REC_REDO: a LOG_DATA (recovery location) plus its redo length; the redo payload is the recovery data.
// log_rec_redo -- src/transaction/log_record.hppstruct log_rec_redo { LOG_DATA data; /* Location of recovery data */ int length; /* Length of redo data */};| Field | Role | Why it exists |
|---|---|---|
data (LOG_DATA) | Recovery location rcvindex + (volid, pageid, offset), all written by the generator. | Recovery dispatches by rcvindex, re-applies at the page slot. |
length (int) | Byte length of the redo image. | Mirrored to node->rlength. |
// prior_lsa_gen_postpone_record -- src/transaction/log_append.cppnode->data_header_length = sizeof (LOG_REC_REDO);node->data_header = (char *) malloc (node->data_header_length);if (node->data_header == NULL) { ...; return ER_OUT_OF_VIRTUAL_MEMORY; }redo = (LOG_REC_REDO *) node->data_header;redo->data.rcvindex = rcvindex;if (addr->pgptr != NULL) /* <- page resident: read its VPID */ { vpid = pgbuf_get_vpid_ptr (addr->pgptr); redo->data.pageid = vpid->pageid; redo->data.volid = vpid->volid; }else /* <- no page: null location */ { redo->data.pageid = NULL_PAGEID; redo->data.volid = NULL_VOLID; }redo->data.offset = addr->offset;redo->length = length;error_code = prior_lsa_copy_redo_data_to_node (node, redo->length, data);return error_code;The only branch is addr->pgptr: page-held yields that page’s (volid, pageid), otherwise the location is nulled and only offset survives. Either way the redo data is copied via the helper in 3.6.
3.4 prior_lsa_gen_dbout_redo_record: redo to an external (non-page) destination
Section titled “3.4 prior_lsa_gen_dbout_redo_record: redo to an external (non-page) destination”LOG_DBEXTERN_REDO_DATA is for redo targeting something outside the page buffer (e.g. a file-level operation). Its header is the compact LOG_REC_DBOUT_REDO — recovery index and length, no page address.
// log_rec_dbout_redo -- src/transaction/log_record.hppstruct log_rec_dbout_redo { LOG_RCVINDEX rcvindex; /* Index to recovery function */ int length; /* Length of redo data */};| Field | Role | Why it exists |
|---|---|---|
rcvindex (LOG_RCVINDEX) | Index into the recovery-function table that replays this external redo. | No page address; the index alone identifies the target. |
length (int) | Byte length of the redo image. | Mirrored to node->rlength. |
// prior_lsa_gen_dbout_redo_record -- src/transaction/log_append.cppnode->data_header_length = sizeof (LOG_REC_DBOUT_REDO);node->data_header = (char *) malloc (node->data_header_length);if (node->data_header == NULL) { ...; return ER_OUT_OF_VIRTUAL_MEMORY; }dbout_redo = (LOG_REC_DBOUT_REDO *) node->data_header;dbout_redo->rcvindex = rcvindex;dbout_redo->length = length;error_code = prior_lsa_copy_redo_data_to_node (node, dbout_redo->length, data);return error_code;No branch beyond the malloc failure check: copy header, copy redo, done.
3.5 prior_lsa_gen_2pc_prepare_record and prior_lsa_gen_end_chkpt_record: dual-payload headers
Section titled “3.5 prior_lsa_gen_2pc_prepare_record and prior_lsa_gen_end_chkpt_record: dual-payload headers”These two share one shape: allocate a fixed header struct, then conditionally copy an undo-side and a redo-side buffer. Neither populates the header’s fields — the caller does that later; the generators only do the two copies.
// prior_lsa_gen_2pc_prepare_record -- src/transaction/log_append.cppnode->data_header_length = sizeof (LOG_REC_2PC_PREPCOMMIT);node->data_header = (char *) malloc (node->data_header_length); /* if NULL -> return ER_OUT_OF_VIRTUAL_MEMORY */if (gtran_length > 0) /* <- global-transaction info -> undo side */ error_code = prior_lsa_copy_undo_data_to_node (node, gtran_length, gtran_data);if (lock_length > 0) /* <- acquired-lock list -> redo side */ error_code = prior_lsa_copy_redo_data_to_node (node, lock_length, lock_data);return error_code;// prior_lsa_gen_end_chkpt_record -- src/transaction/log_append.cppnode->data_header_length = sizeof (LOG_REC_CHKPT);node->data_header = (char *) malloc (node->data_header_length); /* if NULL -> return ER_OUT_OF_VIRTUAL_MEMORY */if (tran_length > 0) /* <- active-transaction descriptors -> undo side */ error_code = prior_lsa_copy_undo_data_to_node (node, tran_length, tran_data);if (topop_length > 0) /* <- system-op descriptors -> redo side */ error_code = prior_lsa_copy_redo_data_to_node (node, topop_length, topop_data);return error_code;Branch caution — error_code is overwritten, not accumulated. The redo-side copy assigns error_code unconditionally, masking a failed undo-side copy if the redo-side then succeeds. In practice an OOM at udata makes the next rdata malloc almost certainly fail too, and the helpers no-op on empty length, so the error survives. A modifier adding a third payload must preserve “last copy wins” or switch to short-circuit.
The header structs are populated outside this chapter, but their field roles are part of the record contract:
// log_rec_2pc_prepcommit -- src/transaction/log_record.hppstruct log_rec_2pc_prepcommit { char user_name[DB_MAX_USER_LENGTH + 1]; int gtrid; int gtrinfo_length; unsigned int num_object_locks; unsigned int num_page_locks;};| Field | Role | Why it exists |
|---|---|---|
user_name | Client name of the global transaction. | Identifies the participant on 2PC reconstruction after restart. |
gtrid | Global transaction identifier. | Ties this local branch to the distributed transaction. |
gtrinfo_length | Byte length of the global-tran info blob (gtran_data copied to udata). | How much of the undo payload is global-tran info. |
num_object_locks | Count of update-type object locks held. | Recovery reacquires exactly those object locks. |
num_page_locks | Count of update-type page locks held. | Same for page locks; the list rides in the redo payload. |
// log_rec_chkpt -- src/transaction/log_record.hppstruct log_rec_chkpt { LOG_LSA redo_lsa; /* Oldest LSA of dirty data page in page buffers */ int ntrans; /* Number of active transactions */ int ntops; /* Total number of system operations */};| Field | Role | Why it exists |
|---|---|---|
redo_lsa | Oldest LSA among dirty data pages at checkpoint time. | Restart recovery starts its redo pass here. |
ntrans | Number of active transactions captured. | Count of LOG_INFO_CHKPT_TRANS in the undo payload. |
ntops | Total in-flight system operations captured. | Count of LOG_INFO_CHKPT_SYSOP in the redo payload. |
LOG_END_CHKPT is reachable two ways — the dedicated generator above (where 3.2’s dispatch routes it) and a case LOG_END_CHKPT arm in prior_lsa_gen_record that just sizes LOG_REC_CHKPT for the parameterless variant.
3.6 prior_lsa_gen_record: the catch-all and the copy helpers
Section titled “3.6 prior_lsa_gen_record: the catch-all and the copy helpers”prior_lsa_gen_record handles roughly two dozen small record types: a switch that only decides data_header_length, then a shared malloc-and-zero block, then one optional undo copy.
// prior_lsa_gen_record -- src/transaction/log_append.cppnode->data_header_length = 0;switch (rec_type) { case LOG_DUMMY_HEAD_POSTPONE: ... case LOG_SYSOP_ATOMIC_START: assert (length == 0 && data == NULL); /* <- pure marker records: no header */ break; case LOG_RUN_POSTPONE: node->data_header_length = sizeof (LOG_REC_RUN_POSTPONE); break; case LOG_COMPENSATE: node->data_header_length = sizeof (LOG_REC_COMPENSATE); break; case LOG_SAVEPOINT: node->data_header_length = sizeof (LOG_REC_SAVEPT); break; case LOG_COMMIT: case LOG_ABORT: assert (length == 0 && data == NULL); node->data_header_length = sizeof (LOG_REC_DONETIME); break; /* <- termination time record */ case LOG_SYSOP_END: node->data_header_length = sizeof (LOG_REC_SYSOP_END); break; case LOG_2PC_START: node->data_header_length = sizeof (LOG_REC_2PC_START); break; case LOG_END_CHKPT: node->data_header_length = sizeof (LOG_REC_CHKPT); break; /* ... condensed: COMMIT_WITH_POSTPONE[_OBSOLETE], SYSOP_START_POSTPONE, DUMMY_HA_SERVER_STATE, REPLICATION_{DATA,STATEMENT}, SUPPLEMENTAL_INFO ... */ default: break; /* <- length stays 0: header-less record */ }if (node->data_header_length > 0) { node->data_header = (char *) malloc (node->data_header_length); if (node->data_header == NULL) { ...; return ER_OUT_OF_VIRTUAL_MEMORY; }#if !defined (NDEBUG) memset (node->data_header, 0, node->data_header_length); /* <- silence valgrind */#endif }if (length > 0) error_code = prior_lsa_copy_undo_data_to_node (node, length, data); /* <- redo-side stays empty */return error_code;Two load-bearing facts. The switch only sizes the header — it never fills any field: every type-specific field (e.g. LOG_REC_DONETIME.at_time, the LOG_REC_SYSOP_END union) is written by the caller after the call returns, by casting node->data_header. And gen_record is undo-only: it calls prior_lsa_copy_undo_data_to_node exclusively, matching the dispatcher’s assert (rlength == 0 && rdata == NULL) guard. The marker asserts reject a payload on a payload-less type; release builds skip the assert but still reach if (length > 0), copying a stray payload harmlessly here yet wrongly for recovery.
The smallest meaningful header in this group is the commit/abort termination record:
// log_rec_donetime -- src/transaction/log_record.hppstruct log_rec_donetime { INT64 at_time; /* Database creation time. For safety reasons */};| Field | Role | Why it exists |
|---|---|---|
at_time | Wall-clock time the transaction terminated (commit or abort). | Point-in-time recovery; sanity check vs. db creation time on restart. |
The two payload-copy helpers are trivial and identical except for which node fields they touch:
// prior_lsa_copy_undo_data_to_node -- src/transaction/log_append.cppstatic int prior_lsa_copy_undo_data_to_node (LOG_PRIOR_NODE *node, int length, const char *data){ if (length <= 0 || data == NULL) return NO_ERROR; /* <- no-op guard: idempotent on empty */ node->udata = (char *) malloc (length); if (node->udata == NULL) { ...; return ER_OUT_OF_VIRTUAL_MEMORY; } memcpy (node->udata, data, length); node->ulength = length; /* <- length set only after successful malloc+memcpy */ return NO_ERROR;}prior_lsa_copy_redo_data_to_node is byte-for-byte the same with rdata/rlength substituted. The shared no-op guard is why callers in 3.5 can use if (gtran_length > 0) guards while 3.3/3.4 call the helper unconditionally — both are safe.
Invariant — ulength/rlength is non-zero iff the matching buffer is allocated. The length is set only after a successful malloc+memcpy; the no-op guard leaves both at their zeroed defaults. Nothing here can produce ulength > 0 with udata == NULL (crashes copy-out in Ch 7) or udata != NULL with ulength == 0 (silent leak). Any edit that sets the length before the copy succeeds breaks this.
3.7 The unified error path and the cross-cutting no-mutex invariant
Section titled “3.7 The unified error path and the cross-cutting no-mutex invariant”Back in prior_lsa_alloc_and_copy_data, every generator returns an int into the shared error_code. The tail is a single decision:
// prior_lsa_alloc_and_copy_data (tail) -- src/transaction/log_append.cppif (error_code == NO_ERROR) return node;else { if (node != NULL) { if (node->data_header != NULL) free_and_init (node->data_header); if (node->udata != NULL) free_and_init (node->udata); if (node->rdata != NULL) free_and_init (node->rdata); free_and_init (node); } return NULL; }This works precisely because of the hand-zeroing in 3.2 and the iff-invariant in 3.6: buffers a generator allocated are non-NULL and get freed; buffers it never reached are NULL and skipped. The NULL/non-NULL of each pointer is the bookkeeping — there is no separate progress flag.
Invariant (the chapter’s headline) — nothing on this path takes prior_lsa_mutex or reads/writes log_Gl.prior_info. Verify by inspection: prior_lsa_alloc_and_copy_data and all five generators reference only the local node, the caller’s arguments, and pgbuf_get_vpid_ptr (a page-buffer read). That makes node construction fully parallel across worker threads; the serialization point (the mutex) is deferred to attach time in Ch 5. An edit peeking at log_Gl.prior_info here would reintroduce hot-path contention — without the mutex, a data race.
The public append-API surface (log_append_undoredo_data, log_append_redo_data, log_append_postpone, the checkpoint and 2PC loggers) all funnel into this function or prior_lsa_alloc_and_copy_crumbs; the full catalogue lives in the high-level companion — see cubrid-prior-list.md, the append-API section.
3.8 Chapter summary — key takeaways
Section titled “3.8 Chapter summary — key takeaways”prior_lsa_alloc_and_copy_databuilds aLOG_PRIOR_NODEon the calling thread: onemalloc, hand-zeroing of every field (it ismalloc’d, notcalloc’d), aswitchonrec_typeto a per-type generator, and a single cleanup decision.- The undoredo/MVCC family is a hard
assert_release (false)here; those records must go throughprior_lsa_alloc_and_copy_crumbs(Chapter 4), which has the compression/diff encoding this path lacks. - Each generator
mallocs a type-specific header (LOG_REC_REDOpostpone,LOG_REC_DBOUT_REDOexternal redo,LOG_REC_2PC_PREPCOMMITprepare,LOG_REC_CHKPTend-checkpoint, one of ~15 sizes inprior_lsa_gen_record) and copies at most one undo and one redo payload. - The one real intra-generator branch is in postpone: page-resident
addryields a real(volid, pageid), otherwise the location is nulled. The 2PC and end-chkpt generators each do two conditional copies whose error codes overwrite — last copy wins. - The copy helpers are identical
malloc+memcpyroutines with alength <= 0 || data == NULLno-op guard, enforcing that a length field is non-zero iff its buffer is allocated. - The unified error path frees
data_header/udata/rdata/node relying on the NULL-or-allocated invariant, not a progress flag — which is why the up-front zeroing is load-bearing. - The whole path never touches
prior_lsa_mutexorlog_Gl.prior_info, so node construction is fully concurrent and serialization is deferred to attach time (Chapter 5).
Chapter 4: Building an Undoredo Node with Compression and Diff Encoding
Section titled “Chapter 4: Building an Undoredo Node with Compression and Diff Encoding”Chapter 3 copies bytes verbatim. UNDOREDO is the MVCC hot path — every heap insert, update, and overflow touch produces one — and the most expensive record to assemble: three transformations stack on the plain copy: diff-encoding of undo against redo, MVCC stamping with vacuum metadata, and LZ4 compression — all before the prior-list mutex. This chapter traces prior_lsa_alloc_and_copy_crumbs and prior_lsa_gen_undoredo_record_from_crumbs branch by branch. For the architecture and why pre-mutex work is central, see cubrid-prior-list.md (“The prior list”, “Producer side”).
4.1 The crumb interface and the entry switch
Section titled “4.1 The crumb interface and the entry switch”A crumb is a {length, pointer} pair. Callers describe an undo or redo image as an array of crumbs rather than one contiguous buffer, so a record’s bytes can come from scattered structures without the caller gluing them.
// log_crumb -- src/transaction/log_append.hppstruct log_crumb { int length; const void *data; };prior_lsa_alloc_and_copy_crumbs is the public entry. It mallocs a LOG_PRIOR_NODE, zeroes the payload pointers (data_header, udata, rdata) so the error path can free them unconditionally, then dispatches on rec_type: every UNDO/REDO/UNDOREDO type funnels into the single delegate, anything else trips assert_release (false). The two LOG_*_DIFF_UNDOREDO_DATA cases appear in the switch but are never passed by callers — diff promotion is internal (4.4), and the delegate even asserts the incoming type is not yet a DIFF type.
// prior_lsa_alloc_and_copy_crumbs -- src/transaction/log_append.cppnode->data_header = NULL; node->udata = NULL; node->rdata = NULL; /* <- err path frees iff non-NULL */switch (rec_type) { case LOG_UNDOREDO_DATA: case LOG_DIFF_UNDOREDO_DATA: case LOG_UNDO_DATA: case LOG_REDO_DATA: case LOG_MVCC_UNDOREDO_DATA: case LOG_MVCC_DIFF_UNDOREDO_DATA: case LOG_MVCC_UNDO_DATA: case LOG_MVCC_REDO_DATA: error = prior_lsa_gen_undoredo_record_from_crumbs (thread_p, node, rcvindex, addr, /* + crumb args */); break; default: assert_release (false); error = ER_FAILED; break; /* <- not a data record */}On success the node is returned. On error the wrapper frees the three payloads and the node; the delegate’s error: label frees the three payloads but not the node, so the cleanups compose without double-free.
Invariant — payload pointers are NULL until owned. Each malloced payload pointer is NULL or live, never stale; the entry zeroes all three before the switch, so both cleanups test != NULL and free safely. Enforcement: the unconditional NULL-init at entry plus the != NULL guard in each free site. What breaks if violated: the wrapper frees a payload the delegate already freed (double-free) or leaks one the delegate allocated. This invariant governs every error path below.
4.2 The data-header struct variants
Section titled “4.2 The data-header struct variants”The delegate selects one data header struct, copied later into the log page as the record’s fixed-size head. There are six record families and six matching header structs — LOG_REC_UNDOREDO, LOG_REC_UNDO, LOG_REC_REDO, and their three MVCC counterparts LOG_REC_MVCC_UNDOREDO, LOG_REC_MVCC_UNDO, LOG_REC_MVCC_REDO. All nest the same LOG_DATA locator and are filled by the same function. The non-MVCC trio plus the shared LOG_DATA/LOG_VACUUM_INFO building blocks are tabled below; the MVCC trio composes them: LOG_REC_MVCC_UNDOREDO is { undoredo, mvccid, vacuum_info }, LOG_REC_MVCC_UNDO is { undo, mvccid, vacuum_info }, and LOG_REC_MVCC_REDO is { redo, mvccid } — no vacuum_info, so MVCC redo is off vacuum’s chain (4.5).
LOG_DATA — the recovery locator nested in every variant:
| Field | Role | Why it exists |
|---|---|---|
rcvindex | Index of the recovery (RV) function | Selects which redo/undo routine replays the bytes |
pageid | Page id of the affected page | Names the target page; NULL_PAGEID for a logical record |
offset | Offset within the page | Where on the page the change lands |
volid | Volume id of the affected page | Names the volume; NULL_VOLID for a logical record |
LOG_REC_UNDOREDO — plain (non-MVCC) undoredo, the common heap/btree record:
| Field | Role | Why it exists |
|---|---|---|
data | LOG_DATA locator: rcvindex + (volid, pageid, offset) | Selects the page and RV function to replay against |
ulength | Undo image length; top bit = “zipped” via MAKE_ZIP_LEN | Bytes recovery reads for rollback; flag says inflate |
rlength | Redo image length; top bit = “zipped” | Forward replay; under DIFF redo is XOR-diffed vs undo |
LOG_REC_UNDO and LOG_REC_REDO — single-direction records, identical 2-field layout:
| Field | Role | Why it exists |
|---|---|---|
data | LOG_DATA locator, same as above | One direction needs one locator |
length | The image’s length; top bit = “zipped” via MAKE_ZIP_LEN | Bytes recovery reads; flag says inflate |
LOG_VACUUM_INFO — appended to MVCC undo-bearing records (UNDO, UNDOREDO) so vacuum can walk the chain:
| Field | Role | Why it exists |
|---|---|---|
prev_mvcc_op_log_lsa | LSA of prev MVCC op in this txn; NULL here, patched under the mutex | Vacuum follows it to find a txn’s versions; the LSA is not assigned pre-mutex (Ch 5) |
vfid | File id of the affected heap/btree file | Detects a dropped/reused file; object class (reusable vs referable) |
LOG_REC_MVCC_UNDOREDO — the heaviest of the three MVCC headers, field by field (LOG_REC_MVCC_UNDO is the same shape with LOG_REC_UNDO in place of undoredo; LOG_REC_MVCC_REDO drops the vacuum_info field):
| Field | Role | Why it exists |
|---|---|---|
undoredo | Embedded LOG_REC_UNDOREDO | Reuses the plain layout so recovery reads the head identically |
mvccid | The txn’s MVCC id (or innermost sub-txn id) | Vacuum and snapshot visibility need the producing MVCCID |
vacuum_info | Embedded LOG_VACUUM_INFO | Chains the record into vacuum’s per-txn worklist |
flowchart LR
subgraph mvcc["LOG_REC_MVCC_UNDOREDO"]
ur["undoredo: LOG_REC_UNDOREDO<br/>data + ulength + rlength"]
mid["mvccid"]
vi["vacuum_info<br/>prev_mvcc_op_log_lsa + vfid"]
end
ur --> ld["data: LOG_DATA<br/>rcvindex,pageid,offset,volid"]
Figure 4-1. The MVCC undoredo header nests the plain header, an MVCCID, and vacuum metadata; the non-MVCC variant is the undoredo box alone, and LOG_REC_MVCC_REDO drops the vacuum_info box.
4.3 The per-thread LOG_ZIP context and deciding can_zip
Section titled “4.3 The per-thread LOG_ZIP context and deciding can_zip”The delegate asserts a base type, totals both crumb arrays into ulength/rlength, grabs the per-thread zip contexts, and sets has_undo/has_redo/can_zip per record family.
// prior_lsa_gen_undoredo_record_from_crumbs -- src/transaction/log_append.cppassert (node->log_header.type != LOG_DIFF_UNDOREDO_DATA && node->log_header.type != LOG_MVCC_DIFF_UNDOREDO_DATA); /* <- DIFF arises internally only */zip_undo = log_append_get_zip_undo (thread_p); zip_redo = log_append_get_zip_redo (thread_p); /* lazy per-thread alloc */ulength = 0; for (i=0;i<num_ucrumbs;i++) ulength += ucrumbs[i].length; /* re-total both crumb arrays */rlength = 0; for (i=0;i<num_rcrumbs;i++) rlength += rcrumbs[i].length;if (LOG_IS_UNDOREDO_RECORD_TYPE (node->log_header.type)) { has_undo = has_redo = true; /* has both */ can_zip = log_Zip_support && (zip_undo != NULL || ulength == 0) && (zip_redo != NULL || rlength == 0); }else if (LOG_IS_REDO_RECORD_TYPE (node->log_header.type)) { has_redo = true; can_zip = log_Zip_support && zip_redo; }else /* UNDO type */ { has_undo = true; can_zip = log_Zip_support && zip_undo; }log_zip compresses into a LOG_ZIP context. Its three fields are the contract between the compressor and the copy step (4.5):
| Field | Role | Why it exists |
|---|---|---|
data_length | Length of the stored image, including the LOG_ZIP_SIZE_T length prefix | The byte count the copy step hands to prior_lsa_copy_*_data_to_node; the success test data_length < length lives on it |
buf_size | Capacity of log_data | Lets log_zip_realloc_if_needed grow lazily and skip realloc when the buffer already fits |
log_data | The compressed bytes, prefixed with the original length | The buffer copied into node->udata/node->rdata; the prefix lets the consumer size the unzip |
log_append_get_zip_undo/_redo return thread_p->log_zip_undo/_redo, allocating an IO_PAGESIZE LOG_ZIP on first use (the #if !defined(SERVER_MODE) build instead uses file-scope statics). This is the load-bearing line: the zip buffers are per-thread, so N producers compress in parallel outside the prior-list mutex; under the drain mutex it would serialise the costliest step. log_Zip_support is set by log_append_init_zip only when PRM_ID_LOG_COMPRESS is on; the size floor log_Zip_min_size_to_compress defaults to 255.
flowchart TD
S["total ulength, rlength"] --> Z{"can_zip and some len >= 255?"}
Z -- no --> RAW["raw crumb copy"]
Z -- yes --> R{"realloc data_ptr ok?"}
R -- no --> RAW
R -- yes --> B{"both >= 255?"}
B -- yes --> D["log_diff then zip both; is_diff if redo zipped"]
B -- no --> O["zip the large side only"]
Figure 4-2. The prior_lsa_gen_undoredo_record_from_crumbs compression decision; only when both directions clear 255 does diff-encoding run.
4.4 Staging, diff-encoding, and compression
Section titled “4.4 Staging, diff-encoding, and compression”When can_zip holds and one length reaches 255, the crumbs are staged into the per-thread data_ptr (grown by log_append_realloc_data_ptr). If that realloc fails, data_ptr stays NULL and the zip block is skipped — logging the record raw.
if (can_zip && (ulength >= MIN || rlength >= MIN)) { /* MIN = log_Zip_min_size_to_compress (255) */ total_length = (ulength>0?ulength:0) + (rlength>0?rlength:0); if (log_append_realloc_data_ptr (thread_p, total_length)) data_ptr = log_append_get_data_ptr (thread_p); if (data_ptr != NULL) { tmp_ptr = data_ptr; if (ulength >= MIN) { undo_data = data_ptr; /* flatten undo crumbs into data_ptr */ for(i=0;i<num_ucrumbs;i++){ memcpy(tmp_ptr,ucrumbs[i].data,ucrumbs[i].length); tmp_ptr+=ucrumbs[i].length; } } if (rlength >= MIN) { redo_data = tmp_ptr; /* same loop over rcrumbs after undo */ } if (ulength >= MIN && rlength >= MIN) { /* BOTH: diff then zip both */ (void) log_diff (ulength, undo_data, rlength, redo_data); /* redo ^= undo, in place */ is_undo_zip = log_zip (zip_undo, ulength, undo_data); is_redo_zip = log_zip (zip_redo, rlength, redo_data); if (is_redo_zip) is_diff = true; /* diff counts only if redo compressed */ } else { /* ONE side >= MIN: zip it only */ if (ulength >= MIN) is_undo_zip = log_zip (zip_undo, ulength, undo_data); if (rlength >= MIN) is_redo_zip = log_zip (zip_redo, rlength, redo_data); } } }log_diff XORs redo against undo in place over MIN(ulength, rlength) bytes; for an UPDATE the near-identical images yield mostly zeros that compress far better. log_zip LZ4-compresses into the thread’s LOG_ZIP; the resulting data_length includes a LOG_ZIP_SIZE_T length prefix, and the success test data_length < length (compressed output plus prefix smaller than the raw image) means a non-shrinking record is logged raw.
Invariant — is_diff is set only when redo compressed. log_diff mutates redo_data in place, but is_diff turns true only in the both-sides branch and only after is_redo_zip. Enforcement: the lone if (is_redo_zip) is_diff = true; inside the both-sides arm, plus the later assert (has_redo && has_undo) on the promotion. What breaks if violated: when undo compressed but redo did not, is_diff stays false and the un-zipped redo is recopied from the original crumbs (4.5), not the mutated buffer; setting is_diff with redo carried raw would replay corrupt redo. When set, the type is promoted to LOG_[MVCC_]DIFF_UNDOREDO_DATA.
4.5 Allocating the header, the fallthrough cascade, and the final copy
Section titled “4.5 Allocating the header, the fallthrough cascade, and the final copy”A switch sizes data_header_length to the chosen struct (LOG_REC_MVCC_UNDOREDO, LOG_REC_MVCC_UNDO, LOG_REC_MVCC_REDO, LOG_REC_UNDOREDO, or the narrower LOG_REC_UNDO/LOG_REC_REDO); the header is malloced, OOM jumps to error:. A second switch using [[fallthrough]] wires the bookkeeping pointers so one block fills each MVCC/non-MVCC pair:
case LOG_MVCC_UNDOREDO_DATA: case LOG_MVCC_DIFF_UNDOREDO_DATA: mvcc_undoredo_p = (LOG_REC_MVCC_UNDOREDO *) node->data_header; vacuum_info_p = &mvcc_undoredo_p->vacuum_info; mvccid_p = &mvcc_undoredo_p->mvccid; /* MVCC-only -> 4.6 */ [[fallthrough]];case LOG_UNDOREDO_DATA: case LOG_DIFF_UNDOREDO_DATA: undoredo_p = (type==LOG_UNDOREDO_DATA||type==LOG_DIFF_UNDOREDO_DATA ? (LOG_REC_UNDOREDO *) node->data_header : &mvcc_undoredo_p->undoredo); data_header_ulength_p = &undoredo_p->ulength; data_header_rlength_p = &undoredo_p->rlength; log_data_p = &undoredo_p->data; break;The MVCC case sets vacuum_info_p/mvccid_p then falls through, resolving undoredo_p to the top-level header (non-MVCC) or the nested undoredo (MVCC). The UNDO/REDO arms are asymmetric: LOG_MVCC_UNDO_DATA sets both vacuum_info_p and mvccid_p (then falls through to resolve undo_p), while LOG_MVCC_REDO_DATA sets mvccid_p only and never vacuum_info_p — because LOG_REC_MVCC_REDO has no such field. So MVCC undo/undoredo are on vacuum’s worklist; MVCC redo is not.
The locator is filled from addr (pgptr set ⇒ pgbuf_get_vpid_ptr gives the real (volid, pageid), else NULL_PAGEID/NULL_VOLID for a logical record). The final copy uses the four 4.4 flags:
if (is_undo_zip) { *data_header_ulength_p = MAKE_ZIP_LEN (zip_undo->data_length); /* zipped: top bit set */ error_code = prior_lsa_copy_undo_data_to_node (node, zip_undo->data_length, zip_undo->log_data); }else if (has_undo) { *data_header_ulength_p = ulength; /* raw */ error_code = prior_lsa_copy_undo_crumbs_to_node (node, num_ucrumbs, ucrumbs); }/* redo mirrors: is_redo_zip -> MAKE_ZIP_LEN + copy_redo_data; else has_redo -> copy_redo_crumbs */The raw helpers prior_lsa_copy_undo_crumbs_to_node/prior_lsa_copy_redo_crumbs_to_node re-total crumb lengths, malloc node->udata/node->rdata once, concatenate all crumbs (zero total allocates nothing), and assert the target is NULL first — so zip and raw are mutually exclusive per direction. The zipped helpers prior_lsa_copy_undo_data_to_node/prior_lsa_copy_redo_data_to_node no-op when length <= 0.
4.6 MVCC stamping and the deferred vacuum LSA
Section titled “4.6 MVCC stamping and the deferred vacuum LSA”If mvccid_p is non-NULL (MVCC types only), the MVCCID is stamped from the TDES, innermost sub-txn first; a NULL TDES or invalid id is assert_release (false):
tdes = LOG_FIND_CURRENT_TDES (thread_p);if (tdes == NULL || !MVCCID_IS_VALID (tdes->mvccinfo.id)) { assert_release(false); error_code = ER_FAILED; goto error; }*mvccid_p = tdes->mvccinfo.sub_ids.empty () ? tdes->mvccinfo.id : tdes->mvccinfo.sub_ids.back (); /* innermost */If vacuum_info_p is non-NULL (MVCC undo/undoredo/diff — not redo), vfid is copied from addr->vfid. RVES_NOTIFY_VACUUM legitimately carries a NULL vfid (VFID_SET_NULL); any other MVCC record with no vfid is a bug (assert_release (false)). The back-pointer is then LSA_SET_NULL (&vacuum_info_p->prev_mvcc_op_log_lsa).
Invariant — the previous-MVCC-op LSA cannot be filled pre-mutex. prev_mvcc_op_log_lsa needs this record’s own LSA, which is assigned only on attach under the prior-list mutex; the function writes NULL and defers the patch (cubrid-prior-list.md Chapter 5). Enforcement: the unconditional LSA_SET_NULL here, with the real value written during the mutexed attach. What breaks if violated: a stale or guessed LSA would chain vacuum to the wrong record, so vacuum either skips live versions or follows a dangling link. Any error_code from the copy helpers falls to the shared error: label, NULL-guarded per 4.1.
4.7 Chapter summary — key takeaways
Section titled “4.7 Chapter summary — key takeaways”- Crumbs decouple caller from layout. Bytes arrive as a
{length, data}array (LOG_CRUMB); the builder flattens them, so callers never pre-concatenate. - Three transformations distinguish UNDOREDO: XOR diff-encoding (
log_diff→LOG_*_DIFF_UNDOREDO_DATA), MVCC stamping (mvccid+LOG_VACUUM_INFO), and LZ4 compression (log_zip) — gated bylog_Zip_supportand the 255-byte floor. - Compression runs on a per-thread
LOG_ZIPbefore the mutex — the load-bearing choice: N producers compress in parallel; under the drain mutex it would serialise the costliest step.LOG_ZIPcarriesdata_length(output plus length-prefix),buf_size, and thelog_databuffer. - Six families, six header structs, chosen by final type:
LOG_REC_MVCC_UNDOREDOfor MVCC undoredo/diff,LOG_REC_MVCC_UNDO/LOG_REC_MVCC_REDOfor single-direction MVCC, plainLOG_REC_UNDOREDO/LOG_REC_UNDO/LOG_REC_REDOotherwise. OnlyLOG_REC_MVCC_REDOlacksvacuum_info, so MVCC redo is off vacuum’s chain. MAKE_ZIP_LENoverloads the length’s top bit (| 0x80000000) to flag a compressed image;is_diffis set only when redo compresses, and only then is redo carried asredo XOR undofor the consumer to reverse.- DIFF types are internal-only; cleanup splits two ways (delegate frees payloads, wrapper frees node — safe via “payload pointers NULL until owned”), and
prev_mvcc_op_log_lsastays NULL for the mutexed attach step (Chapter 5).
Chapter 5: Assigning the LSN and Attaching Under the Mutex
Section titled “Chapter 5: Assigning the LSN and Attaching Under the Mutex”A producer arrives holding a fully-built LOG_PRIOR_NODE (Chapters 3–4)
with a null start_lsa and no links. This chapter answers: once built,
how is a node’s LSN assigned, how is it spliced onto the tail, and what
does prior_lsa_mutex protect? It all happens inside
prior_lsa_next_record_internal: the producer pays O(1) — advance a
cursor, stamp a few LSAs, splice one pointer — and leaves; copy-and-flush
is the drain side’s job (Chapters 6–8). See the high-level companion’s
“prior LSA assignment” and “single global serialization point” sections
for rationale; backpressure is Chapter 9.
5.1 The two public wrappers and with_lock
Section titled “5.1 The two public wrappers and with_lock”Two shims differing only in the with_lock constant they forward:
// prior_lsa_next_record / _with_lock -- src/transaction/log_append.cppLOG_LSA prior_lsa_next_record (THREAD_ENTRY *thread_p, LOG_PRIOR_NODE *node, log_tdes *tdes){ return prior_lsa_next_record_internal (thread_p, node, tdes, LOG_PRIOR_LSA_WITHOUT_LOCK); } /* <- internal takes the mutex */LOG_LSA prior_lsa_next_record_with_lock (THREAD_ENTRY *thread_p, LOG_PRIOR_NODE *node, log_tdes *tdes){ return prior_lsa_next_record_internal (thread_p, node, tdes, LOG_PRIOR_LSA_WITH_LOCK); } /* <- caller already holds it */WITHOUT_LOCK takes prior_lsa_mutex on entry, releases it on exit, and
runs the backpressure block (Section 5.9). WITH_LOCK does none of these —
it is for callers that hold the mutex once and append several records
atomically (_with_lock repeatedly), then release it; backpressure must
run with the mutex not held.
5.2 Anatomy of the critical section
Section titled “5.2 Anatomy of the critical section”flowchart TB A["with_lock == WITHOUT_LOCK?"] -->|yes| B["prior_lsa_mutex.lock()"] A -->|no, caller holds mutex| D B --> D["prior_lsa_start_append: start_lsa, tran chain, back_lsa"] D --> E["copy start_lsa out, vacuum block-boundary check"] E --> G["type-dispatch stamping: MVCC / sysop / commit / abort"] G --> H["advance data_header, append udata then rdata"] H --> J["prior_lsa_end_append, splice tail, list_size += bytes"] J --> M["with_lock == WITHOUT_LOCK?"] M -->|yes| N["unlock + backpressure, Chapter 9"] M -->|no| P N --> P["num_log_records_written++, return start_lsa"]
Figure 5-1 — Control flow of prior_lsa_next_record_internal. Everything between lock and unlock is the critical section. Note num_log_records_written++ sits after the unlock guard — per-transaction state, bumped outside the critical section.
Invariant — prior_lsa_mutex totally orders LSN assignment and list
attachment. Every byte of logical-log address space is handed out by
advancing prior_lsa inside this mutex, and every node spliced onto
prior_list_tail inside it, giving (1) non-overlapping LSAs and (2) a
list ordered by ascending start_lsa = flush order. Advancing or splicing
outside the mutex would let two transactions claim the same byte range.
5.3 prior_lsa_start_append — start_lsa, the transaction chain, back_lsa
Section titled “5.3 prior_lsa_start_append — start_lsa, the transaction chain, back_lsa”The LSN is born here — start_lsa is the current prior_lsa:
// prior_lsa_start_append -- src/transaction/log_append.cpplog_prior_lsa_append_advance_when_doesnot_fit (sizeof (LOG_RECORD_HEADER)); /* <- header must not straddle a page */node->log_header.trid = tdes->trid;LSA_COPY (&node->start_lsa, &log_Gl.prior_info.prior_lsa); /* <- THE LSN */if (tdes->is_system_worker_transaction () && !tdes->is_under_sysop ()) { LSA_SET_NULL (&node->log_header.prev_tranlsa); /* <- no per-tran undo chain */ LSA_SET_NULL (&tdes->head_lsa); LSA_SET_NULL (&tdes->tail_lsa); }else { LSA_COPY (&node->log_header.prev_tranlsa, &tdes->tail_lsa); /* <- chain back to tran's last */ LSA_COPY (&tdes->tail_lsa, &log_Gl.prior_info.prior_lsa); /* <- record is now tran's tail */ if (LSA_ISNULL (&tdes->head_lsa)) { LSA_COPY (&tdes->head_lsa, &tdes->tail_lsa); } /* <- first record */ LSA_COPY (&tdes->undo_nxlsa, &log_Gl.prior_info.prior_lsa); } /* <- rollback resumes here */LSA_COPY (&node->log_header.back_lsa, &log_Gl.prior_info.prev_lsa); /* <- physical back link, every record */LSA_SET_NULL (&node->log_header.forw_lsa); /* <- filled by end_append */LSA_COPY (&log_Gl.prior_info.prev_lsa, &log_Gl.prior_info.prior_lsa);log_prior_lsa_append_add_align (sizeof (LOG_RECORD_HEADER)); /* <- reserve header, advance cursor */The physical chain (back_lsa/forw_lsa) is set in both branches so
recovery’s backward scan always works; the if/else diverges only on
the transaction chain, which the if branch (system worker outside a
sysop, no rollback) nulls. The leading advance_when_doesnot_fit runs
before the stamp, so start_lsa points at a page-contained header.
5.4 The vacuum block-boundary check
Section titled “5.4 The vacuum block-boundary check”After start_lsa is read:
// prior_lsa_next_record_internal -- src/transaction/log_append.cppif (LOG_ISRESTARTED () && log_Gl.hdr.does_block_need_vacuum) { assert (!LSA_ISNULL (&log_Gl.hdr.mvcc_op_log_lsa)); if (vacuum_get_log_blockid (log_Gl.hdr.mvcc_op_log_lsa.pageid) != vacuum_get_log_blockid (start_lsa.pageid)) { // ... assert block-id advances monotonically ... vacuum_produce_log_block_data (thread_p); } /* <- different block: hand the closed one to vacuum */ }Fires only when fully restarted and a prior MVCC record dirtied the block.
5.5 State stamping — the type dispatch
Section titled “5.5 State stamping — the type dispatch”An if/else-if chain on node->log_header.type — exactly one arm runs,
most records stamp nothing. These types record recovery state
atomically with the LSN assignment, which is why the stamps live
inside prior_lsa_mutex. The table enumerates every arm; MVCC and
LOG_SYSOP_END get excerpts for their non-obvious shape.
MVCC arm. The guard fires for the three MVCC data types or a
LOG_SYSOP_END of sub-type LOG_SYSOP_END_LOGICAL_MVCC_UNDO. A
three-way internal dispatch then picks the embedded struct:
// prior_lsa_next_record_internal -- src/transaction/log_append.cppif (node->log_header.type == LOG_MVCC_UNDO_DATA) { mvcc_undo = (LOG_REC_MVCC_UNDO *) node->data_header; } /* <- top-level undo cast */else if (node->log_header.type == LOG_SYSOP_END) { mvcc_undo = & ((LOG_REC_SYSOP_END *) node->data_header)->mvcc_undo; } /* <- NESTED member, not a cast */else /* UNDOREDO / DIFF_UNDOREDO */ { mvcc_undoredo = (LOG_REC_MVCC_UNDOREDO *) node->data_header; } /* <- undoredo cast */// vacuum_info and mvccid read from the selected struct, then:LSA_COPY (&vacuum_info->prev_mvcc_op_log_lsa, &log_Gl.hdr.mvcc_op_log_lsa); /* <- chain to prior MVCC op */prior_update_header_mvcc_info (start_lsa, mvccid); /* <- advance global MVCC head */Undo and undoredo/diff cast data_header directly, but LOG_SYSOP_END
reads a mvcc_undo embedded inside the record — not a top-level cast.
All three then link prev_mvcc_op_log_lsa and call the head update.
Sysop / commit / abort arms stamp recovery state into tdes under the
mutex so a concurrent checkpoint sees it consistent:
type | Action | Why under the mutex |
|---|---|---|
LOG_SYSOP_START_POSTPONE | set sysop_start_postpone_lsa = start_lsa; clear atomic_sysop_start_lsa if finished; set state = TRAN_UNACTIVE_TOPOPE_COMMITTED_WITH_POSTPONE | LSA + state seen together |
LOG_SYSOP_END | two independent guarded clears (code below): null atomic_sysop_start_lsa, then sysop_start_postpone_lsa, each only if its slot is set and lastparent_lsa < it | Unwind nesting atomically |
LOG_COMMIT_WITH_POSTPONE / _OBSOLETE | tran_start_postpone_lsa = start_lsa | Recovery resumes here |
LOG_SYSOP_ATOMIC_START | atomic_sysop_start_lsa = start_lsa | Atomic region boundary |
LOG_COMMIT / LOG_ABORT | commit_abort_lsa = start_lsa | The decisive WAL point |
The LOG_SYSOP_END arm is two separate ifs (not one combined test):
// prior_lsa_next_record_internal -- src/transaction/log_append.cppif (!LSA_ISNULL (&tdes->rcv.atomic_sysop_start_lsa) && LSA_LT (&sysop_end->lastparent_lsa, &tdes->rcv.atomic_sysop_start_lsa)) { LSA_SET_NULL (&tdes->rcv.atomic_sysop_start_lsa); } /* <- clear #1, guarded by its own slot */if (!LSA_ISNULL (&tdes->rcv.sysop_start_postpone_lsa) && LSA_LT (&sysop_end->lastparent_lsa, &tdes->rcv.sysop_start_postpone_lsa)) { LSA_SET_NULL (&tdes->rcv.sysop_start_postpone_lsa); } /* <- clear #2, independent guard */The asserts in the other arms (e.g. commit_abort_lsa.is_null ()) state
each slot is written once per phase; a double-write is a malformed
transaction.
5.6 prior_update_header_mvcc_info — the global MVCC head
Section titled “5.6 prior_update_header_mvcc_info — the global MVCC head”// prior_update_header_mvcc_info -- src/transaction/log_append.cppassert (MVCCID_IS_VALID (mvccid));if (!log_Gl.hdr.does_block_need_vacuum) /* <- first MVCC record of this block */ { log_Gl.hdr.oldest_visible_mvccid = log_Gl.mvcc_table.get_global_oldest_visible (); log_Gl.hdr.newest_block_mvccid = mvccid; }else /* <- same block: track max */ { /* ... sanity asserts ... */ if (log_Gl.hdr.newest_block_mvccid < mvccid) { log_Gl.hdr.newest_block_mvccid = mvccid; } }log_Gl.hdr.mvcc_op_log_lsa = record_lsa; /* <- this record is now latest MVCC op */log_Gl.hdr.does_block_need_vacuum = true; /* <- re-arms the flag Section 5.4 reads next */5.7 Advancing past data_header, then udata, then rdata
Section titled “5.7 Advancing past data_header, then udata, then rdata”The cursor advances over the three regions in on-disk order — the header
via the fit/align pair, then the variable regions via
prior_lsa_append_data:
// prior_lsa_next_record_internal -- src/transaction/log_append.cpplog_prior_lsa_append_advance_when_doesnot_fit (node->data_header_length);log_prior_lsa_append_add_align (node->data_header_length);if (node->ulength > 0) { prior_lsa_append_data (node->ulength); }if (node->rlength > 0) { prior_lsa_append_data (node->rlength); }The three primitives (prior_lsa = log_Gl.prior_info.prior_lsa):
// log_prior_lsa_append_align -- src/transaction/log_append.cppprior_lsa.offset = DB_ALIGN (prior_lsa.offset, DOUBLE_ALIGNMENT);if ((size_t) prior_lsa.offset >= (size_t) LOGAREA_SIZE) { prior_lsa.pageid++; prior_lsa.offset = 0; } /* <- align rolled page */// log_prior_lsa_append_advance_when_doesnot_fit -- src/transaction/log_append.cppif ((size_t) prior_lsa.offset + length >= (size_t) LOGAREA_SIZE) { prior_lsa.pageid++; prior_lsa.offset = 0; } /* <- push chunk forward */// log_prior_lsa_append_add_align -- src/transaction/log_append.cppprior_lsa.offset += (add); log_prior_lsa_append_align (); /* <- advance then re-align */LOG_PRIOR_LSA_LAST_APPEND_OFFSET returns LOGAREA_SIZE. Unlike the
header, variable payloads may span pages, so prior_lsa_append_data
carries a page-spanning loop:
// prior_lsa_append_data -- src/transaction/log_append.cppif (length == 0) { return; } /* <- nothing to advance */log_prior_lsa_append_align ();current_offset = prior_lsa.offset; last_offset = LOG_PRIOR_LSA_LAST_APPEND_OFFSET ();if ((current_offset + length) >= last_offset) /* <- crosses >=1 page boundary */ while (length > 0) { if (current_offset >= last_offset) /* <- at page end: roll, reset offsets */ { prior_lsa.pageid++; current_offset = 0; /* ... offset=0 ... */ } copy_length = (current_offset + length >= last_offset) ? CAST_BUFLEN (last_offset - current_offset) /* <- rest of page */ : length; /* <- final partial chunk */ current_offset += copy_length; length -= copy_length; prior_lsa.offset += copy_length; }else { prior_lsa.offset += length; } /* <- fits in page, single bump */log_prior_lsa_append_align ();Despite the name it copies no bytes — it only moves the cursor so
prior_lsa ends one past the last byte of rdata (the copy is Chapter 7).
5.8 prior_lsa_end_append, splicing the tail, and list_size
Section titled “5.8 prior_lsa_end_append, splicing the tail, and list_size”// prior_lsa_end_append -- src/transaction/log_append.cpplog_prior_lsa_append_align ();log_prior_lsa_append_advance_when_doesnot_fit (sizeof (LOG_RECORD_HEADER)); /* <- next record's header must fit */LSA_COPY (&node->log_header.forw_lsa, &log_Gl.prior_info.prior_lsa); /* <- forward link = next record's start */forw_lsa is the address the next record will get; advance_when_doesnot_fit
first means it accounts for any page roll the next header needs. Then the
splice and size bump:
// prior_lsa_next_record_internal -- src/transaction/log_append.cppif (log_Gl.prior_info.prior_list_tail == NULL) /* <- empty list */ { log_Gl.prior_info.prior_list_header = node; log_Gl.prior_info.prior_list_tail = node; }else /* <- append to tail */ { log_Gl.prior_info.prior_list_tail->next = node; log_Gl.prior_info.prior_list_tail = node; }log_Gl.prior_info.list_size += (sizeof (LOG_PRIOR_NODE) + node->data_header_length + node->ulength + node->rlength);node->next was already null from the build phase, so the node is
terminal in either branch. list_size accrues the in-memory footprint
(node struct + three buffers) — the figure the drain daemon and Section
5.9 compare against the memory ceiling.
5.9 Backpressure tail-block and the two consequences
Section titled “5.9 Backpressure tail-block and the two consequences”A guarded backpressure block runs at the end of the WITHOUT_LOCK path
after the mutex is released (Chapter 9); _with_lock callers skip it.
On return, two facts hold: (1) the returned start_lsa is the record’s
final on-disk LSN — assigned from the monotonic cursor under the mutex,
never changing, so callers store it as the durable address; and (2) the
producer has no further WAL work — copy and flush belong to the drain
side (Chapters 6–8).
5.10 Chapter summary — key takeaways
Section titled “5.10 Chapter summary — key takeaways”prior_lsa_next_record_internalis the single serialization point;prior_lsa_mutextotally orders LSN assignment and list attachment, giving non-overlapping LSAs and astart_lsa-ascending list = flush order.with_lockseparates the two wrappers —prior_lsa_next_recordmanages the mutex and runs backpressure;_with_lockassumes a caller-held mutex and skips both.prior_lsa_start_appendassigns the LSN and maintains two chains — the always-on physical chain (back_lsa/forw_lsa) and the transaction chain, nulled for non-sysop system workers, chained else.- The type dispatch stamps recovery state atomically with the LSN —
note the MVCC arm’s three-way dispatch (
LOG_SYSOP_ENDreads a nestedmvcc_undo) and that arm’s two independent guarded clears. - Cursor advance copies no bytes — fit/align/add-align and
prior_lsa_append_data’s page-spanning loop only moveprior_lsa; the copy is Chapter 7. prior_lsa_end_appendand the splice finalize the node —forw_lsacompletes the physical chain, the empty-vs-append branch adds it to the tail,list_sizeaccrues its footprint.- On return the LSN is final and the producer is done — flushing is Chapters 6–8, backpressure is Chapter 9.
Chapter 6: Detaching the List on the Drain Side
Section titled “Chapter 6: Detaching the List on the Drain Side”Chapters 3-5 followed a producer thread: it built a node outside the mutex,
then under prior_lsa_mutex stamped the node’s LSN and linked it onto the tail
of the shared singly-linked list rooted at log_Gl.prior_info. This chapter
crosses to the consumer side and answers one question: when the flush runs,
how does it take the producer-built list away from the producers without
copying a single byte and without racing an in-flight append?
The answer is a two-function pair that is deliberately tiny. The outer function
logpb_prior_lsa_append_all_list holds the coarse LOG_CS critical section,
briefly grabs prior_lsa_mutex, snapshots the byte count, and calls the inner
prior_lsa_remove_prior_list, which is nothing more than three pointer stores
and one integer store. After the swap the detached chain is a private,
single-owner list — no other thread can see it — so everything downstream
(Chapter 7’s copy into the authoritative log pages) runs lock-free. See
cubrid-prior-list §“Producer/consumer split” for
why the design splits work this way; this chapter traces the handover at the
pointer level.
6.1 The struct under contention (recap)
Section titled “6.1 The struct under contention (recap)”We introduced log_prior_lsa_info fully in Chapter 1 and do not re-derive it.
Only four of its fields participate in the drain. They are reproduced here
verbatim for local reference; the role matrix is the authoritative copy in
Chapter 1.
// log_prior_lsa_info -- src/transaction/log_append.hppstruct log_prior_lsa_info{ LOG_LSA prior_lsa; LOG_LSA prev_lsa;
/* list */ LOG_PRIOR_NODE *prior_list_header; LOG_PRIOR_NODE *prior_list_tail;
INT64 list_size; /* bytes */
/* flush list */ LOG_PRIOR_NODE *prior_flush_list_header;
std::mutex prior_lsa_mutex;
log_prior_lsa_info ();};| Field | Role in the drain | Why it exists |
|---|---|---|
prior_list_header | The value returned to the caller; reset to NULL so the next producer starts a fresh list. | The drain’s entire payload is this one pointer — the whole chain travels by reading it once. |
prior_list_tail | Reset to NULL so the next append (Ch.5) re-initializes both ends. | Without resetting it, a later prior_lsa_next_record would splice onto a node now owned by the flusher (use-after-handover). |
list_size | Snapshotted into current_size before the swap, then zeroed. | Drives the PSTAT_PRIOR_LSA_LIST_SIZE perfmon counter in kbytes; zeroing restarts the backpressure accounting (Ch.9). |
prior_lsa_mutex | Held only across the snapshot read plus the swap, not across the copy. | Keeps the producer/consumer hand-off atomic while keeping the lock-hold window O(1). |
Invariant (single-owner after detach). After
prior_lsa_remove_prior_listreturns, the chain reachable from the returnedprior_listpointer is reachable from nowhere else:prior_list_headerandprior_list_tailare bothNULL, so no producer can splice onto it and no second drain can observe it. The code enforces this by doing all three resets underprior_lsa_mutexin the same locked region that reads the head. If violated — e.g. ifprior_list_tailwere left pointing into the detached chain — a concurrentprior_lsa_next_recordwould link a new node onto a list the flusher is already walking, corrupting the log stream.
6.2 logpb_prior_lsa_append_all_list — the drain entry point
Section titled “6.2 logpb_prior_lsa_append_all_list — the drain entry point”This is the only public symbol on the drain side (extern in
log_impl.h). It is short enough to quote whole:
// logpb_prior_lsa_append_all_list -- src/transaction/log_page_buffer.cintlogpb_prior_lsa_append_all_list (THREAD_ENTRY * thread_p){ LOG_PRIOR_NODE *prior_list; INT64 current_size;
assert (LOG_CS_OWN_WRITE_MODE (thread_p));
log_Gl.prior_info.prior_lsa_mutex.lock (); current_size = log_Gl.prior_info.list_size; prior_list = prior_lsa_remove_prior_list (thread_p); log_Gl.prior_info.prior_lsa_mutex.unlock ();
if (prior_list != NULL) { perfmon_add_stat (thread_p, PSTAT_PRIOR_LSA_LIST_SIZE, (unsigned int) current_size / ONE_K); /* kbytes */ perfmon_inc_stat (thread_p, PSTAT_PRIOR_LSA_LIST_REMOVED);
logpb_append_prior_lsa_list (thread_p, prior_list); }
return NO_ERROR;}Branch-complete walkthrough — there are only two branches, but both matter:
-
Precondition assert.
LOG_CS_OWN_WRITE_MODE (thread_p)(declared inlog_manager.h) asserts the caller already holds the coarseLOG_CScritical section in write mode. This is a debug-build guard, not a lock acquisition: the function never takesLOG_CSitself, it trusts the caller. Every one of the callers in §6.4 wraps the call inLOG_CS_ENTER … LOG_CS_EXIT. -
Inner lock + snapshot + detach.
prior_lsa_mutex.lock()is taken,list_sizeis copied into the localcurrent_sizewhile the lock is held (so the snapshot is consistent with the chain being detached), thenprior_lsa_remove_prior_listperforms the swap and returns the old head. The mutex is released immediately after — the lock-hold window is exactly the snapshot read plus four stores, never the byte copy. -
Empty-list branch (
prior_list == NULL). If no producer appended anything since the last drain, the detached head isNULL. The function skips both perfmon updates and the copy entirely and returnsNO_ERROR. This is the common case for an idle server and keeps the flush daemon’s wakeups cheap. -
Non-empty branch (
prior_list != NULL). Two perfmon statistics fire, then the copy.PSTAT_PRIOR_LSA_LIST_SIZEaccumulatescurrent_size / ONE_K— the snapshot converted to kilobytes (integer division, so a sub-1 KB drain records0).PSTAT_PRIOR_LSA_LIST_REMOVEDis a simple counter of how many non-empty drains have occurred. Only after the counters are updated does control pass tologpb_append_prior_lsa_list(Chapter 7), which walks the now-private chain and copies each node into the authoritative log pages. The return value of that call is ignored here —logpb_prior_lsa_append_all_listalways returnsNO_ERROR; copy-side failures escalate throughlogpb_fatal_errorinside the copy path, not via this return code.
flowchart TD
A["logpb_prior_lsa_append_all_list"] --> B["assert LOG_CS_OWN_WRITE_MODE"]
B --> C["prior_lsa_mutex.lock"]
C --> D["current_size = list_size (snapshot)"]
D --> E["prior_list = prior_lsa_remove_prior_list"]
E --> F["prior_lsa_mutex.unlock"]
F --> G{"prior_list != NULL ?"}
G -- "no, idle" --> H["return NO_ERROR"]
G -- "yes" --> I["perfmon_add PRIOR_LSA_LIST_SIZE = current_size/ONE_K kB"]
I --> J["perfmon_inc PRIOR_LSA_LIST_REMOVED"]
J --> K["logpb_append_prior_lsa_list -> Ch.7 copy"]
K --> H
Figure 6-1. Control flow of the drain entry point. The inner lock spans only C-F; the copy at K runs unlocked.
6.3 prior_lsa_remove_prior_list — the minimal swap
Section titled “6.3 prior_lsa_remove_prior_list — the minimal swap”The inner function does the actual handover. It is static and assumes both
locks are already held — it asserts the outer one and silently relies on the
caller holding the inner one:
// prior_lsa_remove_prior_list -- src/transaction/log_page_buffer.cstatic LOG_PRIOR_NODE *prior_lsa_remove_prior_list (THREAD_ENTRY * thread_p){ LOG_PRIOR_NODE *prior_list;
assert (LOG_CS_OWN_WRITE_MODE (thread_p));
prior_list = log_Gl.prior_info.prior_list_header;
log_Gl.prior_info.prior_list_header = NULL; log_Gl.prior_info.prior_list_tail = NULL; log_Gl.prior_info.list_size = 0;
return prior_list;}There is no loop and no branch — the function has a single path. It reads
prior_list_header into a local, then performs exactly four stores:
prior_list_header = NULL— the next producer’s append will see aNULLhead and re-initialize the list (Chapter 5’sif (prior_list_tail == NULL)branch fires).prior_list_tail = NULL— re-armed for the same re-init.list_size = 0— restarts byte accounting from zero.
Counting the stores precisely matters for the design claim: three pointer
stores and one INT64 store, zero bytes of payload moved. The entire log
record chain — which may be megabytes — changes ownership by reassigning a
single head pointer. The thread_p argument exists only for the
LOG_CS_OWN_WRITE_MODE assert; it is otherwise unused. The prev_lsa,
prior_lsa, and prior_flush_list_header fields of log_prior_lsa_info are
deliberately not touched here — prior_lsa/prev_lsa track the running LSN
cursors owned by the producers (Chapter 5) and must survive across drains;
prior_flush_list_header belongs to a different list and is irrelevant to this
swap.
stateDiagram-v2
[*] --> Shared
Shared --> Detached: drain takes prior_lsa_mutex \n reads header, then 4 stores
note right of Shared
header -> n1 -> n2 -> ... -> tail
list_size = sum(bytes)
visible to all producers
end note
note right of Detached
header = NULL, tail = NULL, size = 0
returned prior_list -> n1 -> ... -> tail
visible to flusher ONLY
end note
Detached --> Shared: next producer append re-inits header and tail
Figure 6-2. The list’s ownership state before and after the swap. The chain’s internal links are never rewritten — only the roots in log_prior_lsa_info move.
6.4 The two-lock ordering and the three drain callers
Section titled “6.4 The two-lock ordering and the three drain callers”Invariant (lock ordering: outer LOG_CS first, inner prior_lsa_mutex
second, briefly). Every path that reaches the drain holds LOG_CS in write
mode before entering, and the drain takes prior_lsa_mutex inside that
region and releases it before the copy. The nesting is always
LOG_CS then prior_lsa_mutex, never the reverse. If violated — if some
path took prior_lsa_mutex and then tried to enter LOG_CS — it would invert
the order against the producer-then-flusher discipline and risk deadlock with a
thread holding LOG_CS waiting on prior_lsa_mutex. The producer side
(Chapter 5) only ever holds prior_lsa_mutex and never reaches up for
LOG_CS while holding it, so the partial order is total.
This split is also why detach is kept distinct from copy: prior_lsa_mutex is
contended by every committing transaction on the producer side (Ch.5 takes it
to stamp each record’s LSN). Holding it across the multi-page byte copy would
stall every producer; detaching first releases it in O(1), so producers
immediately resume building a fresh list while the flusher copies the old,
now-private one with no lock at all.
Three drain paths matter for this chapter, and each is wrapped
in its own LOG_CS_ENTER/LOG_CS_EXIT (or asserts ownership). Pointer-level
enumeration:
-
Log-flush daemon.
log_flush_execute(log_manager.c) callsLOG_CS_ENTER, thenlogpb_flush_pages_direct(log_page_buffer.c), which in turn callslogpb_prior_lsa_append_all_list. This is the steady-state path: a producer raiseslog_Flush_has_been_requested, wakeslog_Flush_daemon, and the daemon drains then flushes. The daemon body itself — the wait loop, the group-commit broadcast after the flush — is Chapter 8; here we only note that it is the entry that calls the drain.// log_flush_execute -- src/transaction/log_manager.cLOG_CS_ENTER (&thread_ref);logpb_flush_pages_direct (&thread_ref);LOG_CS_EXIT (&thread_ref); -
Direct flush.
logpb_flush_pages_direct(log_page_buffer.c) is also called synchronously by transactions that cannot wait for the daemon — single-process mode, server not yet restarted, daemon unavailable, or an explicitlogpb_flush_pagesfallback. Its first statement after theLOG_CS_OWN_WRITE_MODEassert is the drain:// logpb_flush_pages_direct -- src/transaction/log_page_buffer.cassert (LOG_CS_OWN_WRITE_MODE (thread_p));logpb_prior_lsa_append_all_list (thread_p);(void) logpb_flush_all_append_pages (thread_p);Both caller 1 and caller 2 funnel through this same function — the daemon just reaches it via
log_flush_execute. Direct callers oflogpb_flush_pages_directalready holdLOG_CS. -
Backpressure / partial-record self-help flush. Two producer-side paths reach the drain directly rather than through
logpb_flush_pages_direct:-
prior_lsa_next_record_internal(log_append.cpp): when the list grows pastlogpb_get_memsize()and the daemon path is unavailable (crash recovery, orSA_MODE), the producer self-drains —LOG_CS_ENTER, thenlogpb_prior_lsa_append_all_list, thenLOG_CS_EXIT. This is the backpressure valve detailed in Chapter 9.// prior_lsa_next_record_internal -- src/transaction/log_append.cppLOG_CS_ENTER (thread_p);logpb_prior_lsa_append_all_list (thread_p);LOG_CS_EXIT (thread_p); -
heap_get_visible_version_from_log(heap_file.c): when reading a prior MVCC version whoseprev_version_lsahas not yet been flushed out of the prior list, the reader forces a drain so the version becomes fetchable —LOG_CS_ENTER, thenlogpb_prior_lsa_append_all_list, thenLOG_CS_EXIT, then re-reads the append LSA and asserts the version is now durable.
-
All three honor the same nesting, and all three reach the identical swap in
prior_lsa_remove_prior_list. The drain is intentionally caller-agnostic: it
neither knows nor cares why it was invoked.
Footnote (out of scope). A fourth direct call site exists beyond the three above:
logpb_fetch_page(log_page_buffer.c) self-drains inside its ownLOG_CS_ENTER/LOG_CS_EXITregion before fetching a log page, ensuring the requested page reflects any not-yet-copied prior records. It reaches the identical swap but is deferred as out of this chapter’s scope.
6.5 Chapter summary — key takeaways
Section titled “6.5 Chapter summary — key takeaways”- The drain is two functions.
logpb_prior_lsa_append_all_listis the public entry (assertsLOG_CS, takesprior_lsa_mutex, snapshotslist_size, calls the swap, releases the mutex, does perfmon + copy);prior_lsa_remove_prior_listis the swap itself. - The swap moves no bytes. It is exactly three pointer stores
(
prior_list_header,prior_list_tailset toNULL) and one INT64 store (list_size = 0), returning the old head. A megabyte chain changes owner by reassigning one pointer. - Two branches only in the entry function. Empty list skips perfmon and
copy and returns
NO_ERROR; non-empty recordsPRIOR_LSA_LIST_SIZE(kB) andPRIOR_LSA_LIST_REMOVED, then hands tologpb_append_prior_lsa_list. - Single-owner invariant. After the swap the detached chain is reachable
from nowhere in
log_prior_lsa_info, so the downstream copy (Chapter 7) runs lock-free and no producer can splice onto it. - Lock ordering is
LOG_CSthenprior_lsa_mutex, always. The drain trusts the caller to holdLOG_CS(debug-asserted) and takes the inner mutex only for the O(1) swap, never across the copy — keeping producer stalls minimal. - Three callers, one swap. The log-flush daemon (via
log_flush_executethenlogpb_flush_pages_direct), the direct/synchronous flush (logpb_flush_pages_direct), and the self-help backpressure / partial-record paths (prior_lsa_next_record_internal,heap_get_visible_version_from_log) all converge on the same detach. The daemon’s body and the copy/flush they trigger are Chapters 8 and 7.
Chapter 7: Copying Nodes into the Authoritative Log Pages
Section titled “Chapter 7: Copying Nodes into the Authoritative Log Pages”The drain side has detached the producer queue under prior_lsa_mutex
(Chapter 6) and holds a private singly-linked list of LOG_PRIOR_NODEs
no other thread can see. What remains is the step producers were
forbidden from doing: moving record bytes out of the malloc’d nodes into
the authoritative LOG_PAGE ring buffer, in LSN order, under
LOG_CS_OWN_WRITE_MODE. This is the queue-to-page half of the two-stage
split in the companion (cubrid-prior-list.md §“Flush — making it
durable”); page-to-disk is Chapter 8.
The reader question: how do the bytes in the detached nodes finally
land in the real LOG_PAGE buffer, and when is each node freed? We
trace logpb_append_prior_lsa_list (walk-and-free) and
logpb_append_next_record (per-node copy), then its primitives
logpb_start_append / logpb_append_data / logpb_end_append, and the
page-boundary crossing logpb_next_append_page.
7.1 The walk-and-free loop — logpb_append_prior_lsa_list
Section titled “7.1 The walk-and-free loop — logpb_append_prior_lsa_list”The detached list is handed to logpb_append_prior_lsa_list, which
stages it at a second global, prior_flush_list_header, and consumes
it node by node:
// logpb_append_prior_lsa_list -- src/transaction/log_page_buffer.cstatic intlogpb_append_prior_lsa_list (THREAD_ENTRY * thread_p, LOG_PRIOR_NODE * list){ LOG_PRIOR_NODE *node; assert (LOG_CS_OWN_WRITE_MODE (thread_p)); /* <- outer log CS held */
/* append prior_flush_list */ assert (log_Gl.prior_info.prior_flush_list_header == NULL); /* <- single-drain guard */ log_Gl.prior_info.prior_flush_list_header = list; /* <- stage under a named global */
while (log_Gl.prior_info.prior_flush_list_header != NULL) { node = log_Gl.prior_info.prior_flush_list_header; log_Gl.prior_info.prior_flush_list_header = node->next; /* <- advance BEFORE copy */
logpb_append_next_record (thread_p, node); /* <- copy bytes into LOG_PAGE */
if (node->data_header != NULL) { free_and_init (node->data_header); } if (node->udata != NULL) { free_and_init (node->udata); } if (node->rdata != NULL) { free_and_init (node->rdata); } free_and_init (node); /* <- node and its three buffers reclaimed here */ } return NO_ERROR;}prior_flush_list_header is a contract/debug marker, not merely a
local walk cursor. A full-tree grep finds no concurrent reader (only
the declaration, the initializer, and the five uses inside this
function). The global instead buys (a) the entry
assert(prior_flush_list_header == NULL), which checks the single-drain
contract, and (b) visibility — the mid-drain list is named in log_Gl,
so a core dump shows which records were being copied at the time. The
header advances to node->next before the copy.
Invariant — at most one drain runs at a time. The entry
assert (prior_flush_list_header == NULL)is the in-process enforcement point; the invariant is upheld structurally byLOG_CS_OWN_WRITE_MODE— only the write-mode holder of the log critical section reaches this function (daemon, self-help drain, or forced flush; seecubrid-prior-list.md§“The drain side”). If violated, the second assignment would overwrite the header, orphan the first list, and double-drive the page cursor, tripping the §7.2start_lsa == append_lsaassertion.
Each node is freed immediately after its bytes are copied. No node
pool; the three payload buffers and the node go to the C allocator via
free_and_init, each guarded by a non-NULL check mirroring the
conditional copy in §7.2. The cross-thread malloc/free asymmetry
(producer mallocs, drain frees) is Open Question 3 in the companion.
flowchart TD
A["enter: list = detached prior list\nassert prior_flush_list_header == NULL"] --> B["prior_flush_list_header = list"]
B --> C{"prior_flush_list_header != NULL ?"}
C -- no --> Z["return NO_ERROR"]
C -- yes --> D["node = prior_flush_list_header\nprior_flush_list_header = node->next"]
D --> E["logpb_append_next_record(node)\ncopy bytes into LOG_PAGE"]
E --> F["free data_header / udata / rdata\nif non-NULL"]
F --> G["free_and_init(node)"]
G --> C
Figure 7-1 — The walk-and-free loop. The staged-list header is a named global for the single-drain assert and debugger visibility; each node is freed the instant its bytes are in the page buffer.
7.2 Copying one node — logpb_append_next_record
Section titled “7.2 Copying one node — logpb_append_next_record”Where node bytes meet the authoritative pages — short, but every branch matters here:
// logpb_append_next_record -- src/transaction/log_page_buffer.cstatic intlogpb_append_next_record (THREAD_ENTRY * thread_p, LOG_PRIOR_NODE * node){ if (!LSA_EQ (&node->start_lsa, &log_Gl.hdr.append_lsa)) /* <- order sanity, fatal */ { logpb_fatal_error (thread_p, true, ARG_FILE_LINE, "logpb_append_next_record"); } /* forcing flush in the middle of log record append is a complicated business. */ if (log_Gl.flush_info.num_toflush + 1 >= log_Gl.flush_info.max_toflush) /* <- pre-flush guard */ { logpb_flush_all_append_pages (thread_p); } /* <- empty the toflush array first */
log_Gl.append.appending_page_tde_encrypted = prior_is_tde_encrypted (node); /* <- stamp TDE intent */ logpb_start_append (thread_p, &node->log_header); /* <- write the LOG_RECORD_HEADER */
if (node->data_header != NULL) { LOG_APPEND_ADVANCE_WHEN_DOESNOT_FIT (thread_p, node->data_header_length); /* <- keep header whole */ logpb_append_data (thread_p, node->data_header_length, node->data_header); } if (node->udata != NULL) { logpb_append_data (thread_p, node->ulength, node->udata); } if (node->rdata != NULL) { logpb_append_data (thread_p, node->rlength, node->rdata); }
logpb_end_append (thread_p, &node->log_header); /* <- finalize, validate forw_lsa */ log_Gl.append.appending_page_tde_encrypted = false; /* <- reset TDE intent */ return NO_ERROR;}The start_lsa == append_lsa assertion is the load-bearing check of
the whole drain. node->start_lsa was stamped by the producer under
prior_lsa_mutex (Chapter 5); log_Gl.hdr.append_lsa is the page-side
cursor where the next byte lands. A mismatch — queue and page buffer
desynchronised — fires logpb_fatal_error; there is no recovery branch.
Invariant — page cursor tracks prior-list LSNs exactly. For every node,
node->start_lsa == log_Gl.hdr.append_lsaon entry andheader->forw_lsa == log_Gl.hdr.append_lsaon exit (§7.3); the two asserts bracket the copy in lock-step. Maintained because the producer computedstart_lsa/forw_lsawith the same{pageid, offset}arithmetic the page cursor uses (log_prior_lsa_append_*inlog_append.cpp) and the drain processes nodes in attach order. If violated, an on-disk record’s self-described LSA would not match its physical position, breaking recovery’sforw_lsa/back_lsachains.
The pre-flush guard (num_toflush + 1 >= max_toflush) flushes
toflush[] now, before starting this record (logpb_flush_all_append_pages,
Chapter 8). Forcing a flush mid-record is “a complicated business” — it
triggers the LOGPB_APPENDREC_STATUS partial-record dance; draining
between records keeps the common case free of it.
The remaining branches: appending_page_tde_encrypted is stamped so a
page created mid-record (§7.5) is encrypted, reset at the end. The three
logpb_append_data calls fire only for non-NULL buffers in on-disk
order data_header → udata → rdata, and only the data header gets the
LOG_APPEND_ADVANCE_WHEN_DOESNOT_FIT pre-advance keeping it whole on one
page (logpb_append_data itself spans udata/rdata, §7.4). The function
always returns NO_ERROR; data-path failures escalate to
logpb_fatal_error.
7.3 Writing the header and closing the record
Section titled “7.3 Writing the header and closing the record”logpb_start_append writes the fixed LOG_RECORD_HEADER into the page
and opens the record:
// logpb_start_append -- src/transaction/log_page_buffer.cstatic voidlogpb_start_append (THREAD_ENTRY * thread_p, LOG_RECORD_HEADER * header){ LOG_RECORD_HEADER *log_rec; /* ... condensed: assert LOG_CS write mode, perfmon_inc_stat ... */ LOG_APPEND_ADVANCE_WHEN_DOESNOT_FIT (thread_p, sizeof (LOG_RECORD_HEADER)); /* <- header stays whole */ if (!LSA_EQ (&header->back_lsa, &log_Gl.append.prev_lsa)) /* <- back-link sanity, fatal */ { logpb_fatal_error (thread_p, true, ARG_FILE_LINE, "logpb_start_append"); } /* ... condensed: assert log_pgptr != NULL, set TDE algorithm on page if appending_page_tde_encrypted ... */
log_rec = (LOG_RECORD_HEADER *) LOG_APPEND_PTR (); /* <- page ptr at append_lsa.offset */ *log_rec = *header; /* <- struct copy header into page */ if (log_Gl.append.log_pgptr->hdr.offset == NULL_OFFSET) /* <- first record on page? */ { log_Gl.append.log_pgptr->hdr.offset = (PGLENGTH) log_Gl.hdr.append_lsa.offset; }
if (log_rec->type == LOG_END_OF_LOG) /* <- EOF marker, flush only; no IN_PROGRESS */ { /* this comes from logpb_flush_all_append_pages */ assert (log_Pb.partial_append.status == LOGPB_APPENDREC_SUCCESS || log_Pb.partial_append.status == LOGPB_APPENDREC_PARTIAL_ENDED); /* <- two legal prestates */ LSA_COPY (&log_Gl.hdr.eof_lsa, &log_Gl.hdr.append_lsa); logpb_set_dirty (thread_p, log_Gl.append.log_pgptr); } else { assert (log_Pb.partial_append.status == LOGPB_APPENDREC_SUCCESS); /* <- no record open */ LSA_COPY (&log_Gl.append.prev_lsa, &log_Gl.hdr.append_lsa); /* <- this LSA -> prev */ LOG_APPEND_SETDIRTY_ADD_ALIGN (thread_p, sizeof (LOG_RECORD_HEADER)); /* <- past header */ log_Pb.partial_append.status = LOGPB_APPENDREC_IN_PROGRESS; /* <- open record */ }}Branches: the advance macro crosses to a fresh page (§7.5) so the header
never splits; the fatal back_lsa == prev_lsa check ties header->back_lsa
to the physically preceding record’s LSA; first-record-on-page records
hdr.offset so recovery can locate where records begin. The
LOG_END_OF_LOG branch — the EOF marker written only by the flusher
(Chapter 8) — legitimately follows either LOGPB_APPENDREC_SUCCESS
(a completed record) or LOGPB_APPENDREC_PARTIAL_ENDED (the
post-mid-record-flush path), then copies append_lsa into eof_lsa and
dirties the page without opening a record. The normal branch records
this LSA as prev_lsa, advances past the header, and opens IN_PROGRESS.
logpb_end_append closes the record:
// logpb_end_append -- src/transaction/log_page_buffer.cstatic voidlogpb_end_append (THREAD_ENTRY * thread_p, LOG_RECORD_HEADER * header){ LOG_APPEND_ALIGN (thread_p, LOG_DONT_SET_DIRTY); LOG_APPEND_ADVANCE_WHEN_DOESNOT_FIT (thread_p, sizeof (LOG_RECORD_HEADER)); /* <- next hdr fits */ assert (LSA_EQ (&header->forw_lsa, &log_Gl.hdr.append_lsa)); /* <- forw_lsa points to next record */ if (!LSA_EQ (&log_Gl.append.prev_lsa, &log_Gl.hdr.append_lsa)) { logpb_set_dirty (thread_p, log_Gl.append.log_pgptr); }
if (log_Pb.partial_append.status == LOGPB_APPENDREC_IN_PROGRESS) { /* normal: fall through */ } else if (log_Pb.partial_append.status == LOGPB_APPENDREC_PARTIAL_FLUSHED_END_OF_LOG) { log_Pb.partial_append.status = LOGPB_APPENDREC_PARTIAL_ENDED; logpb_flush_all_append_pages (thread_p); /* <- re-flush the now-complete record */ assert (log_Pb.partial_append.status == LOGPB_APPENDREC_PARTIAL_FLUSHED_ORIGINAL); /* <- re-flush left it here */ } else { assert_release (false); } /* <- invalid state */ log_Pb.partial_append.status = LOGPB_APPENDREC_SUCCESS;}It ensures the next header would fit, so append_lsa points where the
next record begins. The forw_lsa == append_lsa assertion closes the
lock-step bracket opened by the §7.2 start_lsa check. The status switch
is normally IN_PROGRESS; PARTIAL_FLUSHED_END_OF_LOG is reached only
after a mid-record forced flush (Chapter 8) and re-flushes the completed
record; anything else is assert_release(false). Status returns to
SUCCESS.
7.4 Copying payload bytes — logpb_append_data
Section titled “7.4 Copying payload bytes — logpb_append_data”logpb_append_data is the byte mover — the only place in the drain that
spans a payload across page boundaries:
// logpb_append_data -- src/transaction/log_page_buffer.cstatic voidlogpb_append_data (THREAD_ENTRY * thread_p, int length, const char *data){ int copy_length; char *ptr, *last_ptr;
if (length == 0 || data == NULL) { return; } /* <- nothing to copy */
LOG_APPEND_ALIGN (thread_p, LOG_DONT_SET_DIRTY); ptr = LOG_APPEND_PTR (); /* <- write cursor in page */ last_ptr = LOG_LAST_APPEND_PTR (); /* <- end of usable area */
if ((ptr + length) >= last_ptr) /* <- payload spans page boundary */ { while (length > 0) { if (ptr >= last_ptr) /* <- page exhausted: cross, dirty old page */ { logpb_next_append_page (thread_p, LOG_SET_DIRTY); ptr = LOG_APPEND_PTR (); last_ptr = LOG_LAST_APPEND_PTR (); } copy_length = (ptr + length >= last_ptr) ? CAST_BUFLEN (last_ptr - ptr) : length; memcpy (ptr, data, copy_length); ptr += copy_length; data += copy_length; length -= copy_length; log_Gl.hdr.append_lsa.offset += copy_length; } } else /* <- fits in one page */ { memcpy (ptr, data, length); log_Gl.hdr.append_lsa.offset += length; } LOG_APPEND_ALIGN (thread_p, LOG_SET_DIRTY); /* <- align next, dirty page */}Three branches: the empty guard returns at once; the else branch
is the common case — one memcpy, one offset bump; the while branch
fires when the payload reaches last_ptr, copying what fits, crossing via
logpb_next_append_page (§7.5), and repeating until exhausted. Because
LOG_APPEND_PTR() resolves to log_pgptr->area + append_lsa.offset,
these memcpys land directly in the live LOG_PAGE frames with no
staging buffer — the work producers were forbidden from doing under
prior_lsa_mutex, run only on the single drain under LOG_CS.
7.5 Crossing a page boundary — logpb_next_append_page
Section titled “7.5 Crossing a page boundary — logpb_next_append_page”Every page-boundary cross — from the advance/align macros or the spanning
loop in logpb_append_data — funnels through logpb_next_append_page: it
dirties the page just finished, allocates the next, and registers it in
flush_info.toflush[]:
// logpb_next_append_page -- src/transaction/log_page_buffer.cstatic voidlogpb_next_append_page (THREAD_ENTRY * thread_p, LOG_SETDIRTY current_setdirty){ LOG_FLUSH_INFO *flush_info = &log_Gl.flush_info; bool need_flush;
if (current_setdirty == LOG_SET_DIRTY) { logpb_set_dirty (thread_p, log_Gl.append.log_pgptr); } /* <- dirty the page we are leaving */ log_Gl.append.log_pgptr = NULL; log_Gl.hdr.append_lsa.pageid++; /* <- next logical page */ log_Gl.hdr.append_lsa.offset = 0;
if (LOGPB_AT_NEXT_ARCHIVE_PAGE_ID (log_Gl.hdr.append_lsa.pageid)) { logpb_archive_active_log (thread_p); } if (LOGPB_IS_FIRST_PHYSICAL_PAGE (log_Gl.hdr.append_lsa.pageid)) { /* ring wrap: bump fpageid, flush header */ }
log_Gl.append.log_pgptr = logpb_create_page (thread_p, log_Gl.hdr.append_lsa.pageid); if (log_Gl.append.log_pgptr == NULL) { logpb_fatal_error (thread_p, true, ARG_FILE_LINE, "log_next_append_page"); return; } /* ... condensed: if appending_page_tde_encrypted, set TDE algorithm on new page ... */
rv = pthread_mutex_lock (&flush_info->flush_mutex); flush_info->toflush[flush_info->num_toflush] = log_Gl.append.log_pgptr; /* <- register page to flush */ flush_info->num_toflush++; need_flush = (flush_info->num_toflush >= flush_info->max_toflush); pthread_mutex_unlock (&flush_info->flush_mutex);
if (need_flush) { logpb_flush_all_append_pages (thread_p); } /* <- toflush array full */}Branch by branch: dirty-on-leave — LOG_SET_DIRTY (the spanning
loop) dirties the page being left, LOG_DONT_SET_DIRTY means the caller
already did. Archive / ring-wrap — colliding with the next archive
page archives first; the first physical page bumps fpageid and flushes
the header (log-ring housekeeping, log-manager companion).
logpb_create_page failure is fatal. TDE — a page created while
appending_page_tde_encrypted is set gets a TDE algorithm. Register and
maybe flush — the frame is appended to toflush[] under flush_mutex;
reaching max_toflush forces a flush here, the threshold the §7.2 guard
stays ahead of.
7.6 Chapter summary — key takeaways
Section titled “7.6 Chapter summary — key takeaways”-
logpb_append_prior_lsa_listwalks the detached list and frees each node the instant its bytes are copied. It stages the list at the globalprior_flush_list_headeras a single-drain contract marker (entryassert(... == NULL)plus debugger visibility); the serialization allowing only one drain isLOG_CSin write mode. -
logpb_append_next_recordopens with a fatalstart_lsa == append_lsacheck — the invariant proving the prior list’s LSNs and the page cursor are in lock-step. A mismatch aborts the server; no recovery branch. -
The pre-flush guard (
num_toflush + 1 >= max_toflush) drainstoflush[]between records to dodge the costly mid-record forced flush, whose partial-record handling is theLOGPB_APPENDREC_*state machine inlogpb_start_append/logpb_end_append. -
The node’s three buffers are appended only if non-
NULL, in on-disk order data_header → udata → rdata; the data header gets a pre-advance so it never splits, whilelogpb_append_dataspans udata/rdata across pages. -
logpb_start_appendstruct-copies the header and opens the record (IN_PROGRESS);logpb_end_appendvalidatesforw_lsa == append_lsaand closes it (SUCCESS). The EOF branch legitimately follows either a succeeded record or aPARTIAL_ENDEDstate. -
logpb_next_append_pageis the single funnel for every page cross — it dirties the page left, allocates the next, propagates TDE intent, and registers the new frame inflush_info.toflush[](forcing a flush if it fills). The drain copies into the liveLOG_PAGEframes direct viaLOG_APPEND_PTR()with no staging buffer, sotoflush[]plus the per-page dirty flag is the only handoff to the Chapter 8 disk writer.
Chapter 8: Flushing to Disk and Waking Commit Waiters
Section titled “Chapter 8: Flushing to Disk and Waking Commit Waiters”This chapter answers: once nodes are in the page buffer, how are pages
made durable, how does nxio_lsa advance, and how does a parked committer
learn its record is on disk? Ch.6/7 left each node’s payload in a
LOG_PAGE dirty in memory but not on disk; nxio_lsa (the durable
watermark) still points where the last write left off. The high-level
companion cubrid-prior-list.md carries the policy in its §“Commit
waiters” four-quadrant table; we assume it and trace the branches here.
For where this flush sits in the wider WAL pipeline (checkpoint, archive,
recovery scan), cross-link cubrid-log-manager-detail.md.
8.1 The drain-then-write sequencer: logpb_flush_pages_direct
Section titled “8.1 The drain-then-write sequencer: logpb_flush_pages_direct”Every durable-flush path funnels through one sequencer:
// logpb_flush_pages_direct -- src/transaction/log_page_buffer.cassert (LOG_CS_OWN_WRITE_MODE (thread_p)); /* <- caller already holds the log CS */logpb_prior_lsa_append_all_list (thread_p); /* <- Ch.6/7: drain queue into LOG_PAGE buffer */(void) logpb_flush_all_append_pages (thread_p); /* <- this chapter: write pages, advance nxio_lsa */Invariant: drain precedes write, and (non-HA path) the caller owns
LOG_CS write mode for the whole sequence. Drain must run first, else
nxio_lsa advances past a record left in the prior list and a committer
is woken on it — a lost commit. The “CS held throughout” half is non-HA
only: the HA hand-off (§8.2.6) demotes the CS around the write loop.
logpb_force_flush_pages wraps the CS for callers lacking it
(LOG_CS_ENTER → direct → LOG_CS_EXIT).
8.2 logpb_flush_all_append_pages — the write-to-disk half
Section titled “8.2 logpb_flush_all_append_pages — the write-to-disk half”The largest function: write the dirty flush_info->toflush[] pages to
the active log, fsync, advance nxio_lsa. The complexity is crash-safety
bookkeeping; Figure 8-1 is the authoritative branch map.
flowchart TB
B{"num_toflush<1 or\nlone clean page?"} -->|yes| R0["return 0"]
B -->|need_flush| E{"partial_append.status?"}
E -->|IN_PROGRESS| E1["EOF into page copy, write copy,\n-> PARTIAL_FLUSHED_END_OF_LOG"]
E -->|PARTIAL_FLUSHED_EOL| E2["no new marker"]
E -->|PARTIAL_ENDED or SUCCESS| E3["logpb_start_append EOF in buffer"]
E -->|other| ERR["assert_release, error"]
E1 --> HA["HA on? LOG_CS_DEMOTE, wake LOGWR"]
E2 --> HA
E3 --> HA
HA --> F["loop: skip clean+nxio, collect\ndirty run, writev_append_pages"]
F --> G{"flush nxio page?\nSUCCESS or nxio != prev_lsa"}
G -->|yes| G1["write nxio page, need_sync=true"]
G -->|no| G2["skip: nxio is incomplete header"]
G1 --> H{"need_sync?"}
G2 --> H
H -->|yes| H1["fileio_synchronize\nper SUPPRESS_FSYNC"]
H -->|no| BA["bg archiving? to_archive"]
H1 --> BA
BA --> I{"status?"}
I -->|PARTIAL_ENDED| J1["restore header, rewrite+fsync,\nnxio=append_lsa, -> FLUSHED_ORIGINAL"]
I -->|PARTIAL_FLUSHED_EOL| J2["nxio = append.prev_lsa"]
I -->|SUCCESS| J3["nxio = hdr.append_lsa"]
I -->|other| ERR
J1 --> K["num_toflush=0, re-add log_pgptr,\nHA on? notify writers, LOG_CS_PROMOTE"]
J2 --> K
J3 --> K
K --> R1["return 1"]
Figure 8-1 — Branch map of logpb_flush_all_append_pages. The two
partial_append.status switches bracket the write loop and the optional
HA hand-off.
8.2.1 Early exits. Under flush_info->flush_mutex: if
num_toflush < 1, or it equals 1 and that lone page is not dirty
(logpb_is_dirty), return 0 — avoiding a needless marker rewrite on an
idle tick. Return convention: 1 = flushed, 0 = no flush needed,
< 0 = error; callers (void)-cast it because durability lives in
nxio_lsa, not the return code.
8.2.2 The end-of-log marker — top half of the partial dance. The log
must end with a LOG_END_OF_LOG marker so recovery’s forward scan knows
where the durable log stops. The marker depends on
log_Pb.partial_append.status:
status | Meaning | Marker action |
|---|---|---|
..._IN_PROGRESS | record mid-append when flush forced | copy header page, overwrite the record with LOG_END_OF_LOG (forw_lsa nulled), write the copy, set hdr.eof_lsa = append.prev_lsa, → PARTIAL_FLUSHED_END_OF_LOG |
..._PARTIAL_FLUSHED_END_OF_LOG | temp EOF already on disk | nothing |
..._PARTIAL_ENDED / ..._SUCCESS | record fully appended | logpb_start_append a fresh LOG_END_OF_LOG into the buffer (no log-address advance) |
| else | unexpected | assert_release (false); goto error |
The IN_PROGRESS branch is the heart of crash safety for oversized
records:
// logpb_flush_all_append_pages -- src/transaction/log_page_buffer.c (IN_PROGRESS, condensed)memcpy (log_Pb.partial_append.log_page_record_header, bufptr->logpage, LOG_PAGESIZE);bufptr->dirty = false; /* <- keep fake EOF off the real page */LSA_SET_NULL (&log_Pb.partial_append.record_header_p->forw_lsa); /* <- no forward pointer past EOF */log_Pb.partial_append.record_header_p->type = LOG_END_OF_LOG; /* (original header saved for §8.2.5) */Only the disk copy carries the fake EOF; the buffer page stays the real record, marked clean so the write loop skips it.
8.2.3 The two-step write loop — nxio page last.
Invariant: the nxio_lsa page is written last. Recovery validates
the new end-of-log only when its page lands, so a crash mid-flush leaves
the old end-of-log (nxio page never written) or the new one (written
after everything it points to) — never a page whose forw_lsa points
into a record body that did not reach disk.
The loop (Figure 8-1 node F) skips clean pages and the nxio page, then
extends a run while pages stay dirty and consecutive in both logical and
physical pageid. Four conditions break a run (clean, nxio, logical gap,
physical gap); the gap checks let one logpb_writev_append_pages do a
single vectored write per run. The nxio page is then written alone,
unless it is the incomplete record’s header page (status != SUCCESS
and nxio page == log_Gl.append.prev_lsa.pageid) — already carrying the
fake EOF, so left for §8.2.5. On SUCCESS it holds the fresh end-of-log
and is written last to validate the new tail.
8.2.4 The fsync and background archiving. If need_sync and
(PRM_ID_SUPPRESS_FSYNC == 0 or total_sync_count % PRM_ID_SUPPRESS_FSYNC == 0), call fileio_synchronize (thread_p, log_Gl.append.vdes, log_Name_active, false) — SUPPRESS_FSYNC = 0 syncs every flush, non-zero
every Nth; this is where durability becomes physical. Then, if
PRM_ID_LOG_BACKGROUND_ARCHIVING, logpb_write_toflush_pages_to_archive
mirrors the flushed pages into the pre-allocated archive volume (so
archive-log creation is a rename) — a side write off the nxio_lsa path.
8.2.5 Advancing nxio_lsa — bottom half of the partial dance. The
value set here is the writer-to-waiter contract: what a committer (§8.4)
observes.
// logpb_flush_all_append_pages -- src/transaction/log_page_buffer.c (nxio advance, condensed)if (log_Pb.partial_append.status == LOGPB_APPENDREC_PARTIAL_ENDED) { /* oversized record now complete */ *log_Pb.partial_append.record_header_p = log_Pb.partial_append.original_record_header; /* restore real header */ logpb_write_page_to_disk (...); fileio_synchronize (...); /* rewrite + sync again */ log_Gl.append.set_nxio_lsa (log_Gl.hdr.append_lsa); /* <- now safe: real record durable */ log_Pb.partial_append.status = LOGPB_APPENDREC_PARTIAL_FLUSHED_ORIGINAL;} else if (log_Pb.partial_append.status == LOGPB_APPENDREC_PARTIAL_FLUSHED_END_OF_LOG) log_Gl.append.set_nxio_lsa (log_Gl.append.prev_lsa); /* <- cannot pass incomplete record yet */else if (log_Pb.partial_append.status == LOGPB_APPENDREC_SUCCESS) log_Gl.append.set_nxio_lsa (log_Gl.hdr.append_lsa); /* <- normal: watermark = append cursor */else { assert_release (false); goto error; }set_nxio_lsa is an atomic store (std::atomic<LOG_LSA> in
log_append_info), so committers reading via get_nxio_lsa without the
CS see a consistent value. The PARTIAL_ENDED double write — fake EOF in
§8.2.2, real header restored and re-synced here — makes a dangling
forw_lsa impossible: the watermark reaches append_lsa only after the
real record is durable.
8.2.6 HA writer hand-off. In server mode with HA enabled and writer
flush not suppressed (!HA_DISABLED () && !writer_info->skip_flush), the
function demotes the log CS to read mode around the write loop, flips
each waiting LWT entry to LOGWR_STATUS_FETCH, and wakes the thread:
// logpb_flush_all_append_pages -- src/transaction/log_page_buffer.c (HA hand-off, condensed)LOG_CS_DEMOTE (thread_p); /* <- write -> read: LOGWR threads read the log concurrently */entry->status = LOGWR_STATUS_FETCH; /* <- tell each waiting LWT: go fetch */thread_wakeup_already_had_mutex (wait_thread_p, THREAD_LOGWR_RESUMED);The demote happens after the marker is chosen (§8.2.2) but before the
write loop. The paired completion block (Figure 8-1 node K) broadcasts
writer_info->flush_wait_cond, waits until every LOGWR_STATUS_FETCH
entry drains, then LOG_CS_PROMOTEs back — the exception to §8.1’s “CS
held throughout”. LOG_CS_DEMOTE/LOG_CS_PROMOTE are functions in
log_manager.c, not macros; with HA disabled the pair is skipped.
8.3 The daemon body: log_flush_execute
Section titled “8.3 The daemon body: log_flush_execute”In server mode the flush is normally done by the log-flush daemon; its
body is small and every line is a gate:
// log_flush_execute -- src/transaction/log_manager.cif (!BO_IS_SERVER_RESTARTED () || !log_Flush_has_been_requested) return; /* <- short-circuit: empty tick does no work */LOG_CS_ENTER (&thread_ref);logpb_flush_pages_direct (&thread_ref); /* <- drain + write + advance nxio_lsa */LOG_CS_EXIT (&thread_ref);pthread_mutex_lock (&group_commit_info.gc_mutex);pthread_cond_broadcast (&group_commit_info.gc_cond); /* <- wake ALL committers */log_Flush_has_been_requested = false; /* <- clear the request flag */pthread_mutex_unlock (&group_commit_info.gc_mutex);Branches: (1) !BO_IS_SERVER_RESTARTED () — boot/recovery not yet
open, return (recovery self-flushes). (2) !log_Flush_has_been_requested
— the looper ticks on a timed period (§8.5) even unprompted; with no
request it returns without taking LOG_CS (so the high-level doc’s “INF,
woken by commits” is imprecise — it is a timed looper short-circuiting
empty ticks). (3) flush under LOG_CS. (4) after CS release,
broadcast and clear the flag under gc_mutex, paired with the §8.4 set so
no interleaved request is missed; the atomic nxio_lsa store inside the
CS makes the broadcast the release fence.
Invariant: the broadcast covers every committer waiting when the flush
completed. pthread_cond_broadcast (not signal) wakes all waiters
to re-evaluate nxio_lsa < flush_lsa: one fsync advances nxio_lsa
once, one broadcast releases the whole covered cohort.
8.4 Waking the daemon: log_wakeup_log_flush_daemon and friends
Section titled “8.4 Waking the daemon: log_wakeup_log_flush_daemon and friends”A committer (or a backpressure producer, Ch.9) requests a flush via
log_wakeup_log_flush_daemon: if log_is_log_flush_daemon_available ()
(log_Flush_daemon != NULL — false in SA mode and before init), it sets
log_Flush_has_been_requested = true then calls
log_Flush_daemon->wakeup (). Setting the flag (std::atomic_bool)
before wakeup() closes a race: the period function (§8.5) also reads
it and collapses its sleep to zero when pending, so a lost wakeup()
cannot delay the flush.
8.4.1 The committer wait loop: logpb_flush_pages.
logpb_flush_pages(flush_lsa) is the committer’s entry. Branch-complete
walkthrough: (0) the SA build (#if !defined(SERVER_MODE)) and the
two server-mode runtime bypasses — !BO_IS_SERVER_RESTARTED () || flush_lsa == NULL || LSA_ISNULL, and !log_is_log_flush_daemon_available () — all route into the §8.4.2 direct flush. (1) else set
need_wait = (PRM_ID_LOG_ASYNC_COMMIT == false) and need_wakeup_LFT = (LOG_IS_GROUP_COMMIT_ACTIVE () == false) —
the four (async, group) quadrants of the high-level doc’s matrix; only
the two need_wait (synchronous) quadrants enter the CV loop, the async
pair returns immediately (after a daemon wakeup when !group). (2) the
CV loop spins while (LSA_LT (&nxio_lsa, flush_lsa)): lock gc_mutex,
re-read get_nxio_lsa (), conditionally log_wakeup_log_flush_daemon,
pthread_cond_timedwait (&gc_cond, &gc_mutex, ...) 1 s, unlock,
wakeup_LFT=true. The recheck under gc_mutex before sleeping closes the
lost-wakeup race against the §8.3 broadcast. need_wakeup_LFT is the
group-commit lever: in sync+group it starts false, so the first
iteration parks without waking the daemon, letting the timed tick (§8.5)
batch arrivals. Permanent data-page latches (pgbuf_has_perm_pages_fixed)
or any post-timeout iteration force a wakeup.
Invariant: a synchronous committer returns only when
nxio_lsa >= flush_lsa. With §8.2.5’s rule that nxio_lsa advances
only after fileio_synchronize, this guarantees no transaction sees a
commit ack before its LOG_COMMIT is on stable storage. The async
quadrants break it (need_wait = false) — why PRM_ID_LOG_ASYNC_COMMIT
is unsafe across crashes.
8.4.2 The non-server / not-restarted bypasses. The SA build compiles
to a bare LOG_CS_ENTER → logpb_flush_pages_direct → LOG_CS_EXIT.
In server mode, two runtime guards route around the CV loop into a direct
flush: !BO_IS_SERVER_RESTARTED () || flush_lsa == NULL || LSA_ISNULL
(boot/recovery — §8.3 short-circuits anyway), and
!log_is_log_flush_daemon_available (). The load-bearing
assert (!LOG_CS_OWN_WRITE_MODE (thread_p)) sits between the two —
after the restart bypass returns and before the daemon-availability
bypass — guarding the path that actually parks: a committer must not
hold the log CS when it waits, else it deadlocks the daemon.
8.5 Daemon registration: log_flush_daemon_init
Section titled “8.5 Daemon registration: log_flush_daemon_init”log_flush_daemon_init builds a cubthread::looper over the period
function log_get_log_group_commit_interval (not a fixed interval) and
create_daemons it with log_flush_execute as the body, named
"log-flush":
// log_get_log_group_commit_interval -- src/transaction/log_manager.c (condensed)if (log_Flush_has_been_requested) { period = milliseconds (0); return; } /* <- pending: tick now */period = milliseconds (msec == 0 ? 1000 : msec); /* <- 0 means default 1s cap */Because the period is a function pointer, changing
PRM_ID_LOG_GROUP_COMMIT_INTERVAL_MSECS propagates on the next cycle
without a restart, and a pending request collapses it to 0 ms (defeating a
lost wakeup()). At the 0 default the idle cap is 1 s, bounding the
group-commit batch window.
8.6 Chapter summary — key takeaways
Section titled “8.6 Chapter summary — key takeaways”logpb_flush_pages_directis the one sequencer: assert write-mode CS, drain the prior list, then write — drain-before-write is hard, ornxio_lsacould pass a record still in volatile memory.nxio_lsais the durable watermark, advanced only insidelogpb_flush_all_append_pagesafterfileio_synchronizeby atomicset_nxio_lsa:SUCCESSjumps it toappend_lsa; the two partial states pin/restore it so it never crosses an incomplete record.- The two-step page write is a crash-safety dance: the
nxio_lsapage (the fresh end-of-log onSUCCESS) is written last, so a crash leaves only the old or new end-of-log, never a danglingforw_lsa. Oversized records add the fake-EOF / real-header double-write tracked bypartial_append.status. - HA replication brackets the write loop with
LOG_CS_DEMOTE/LOG_CS_PROMOTE(flip LOGWR entries toLOGWR_STATUS_FETCH, drain, promote); background archiving mirrors flushed pages afterfsync. log_flush_executeis the daemon body: short-circuit unless restarted andlog_Flush_has_been_requested; flush underLOG_CS; broadcastgc_condand clear the flag undergc_mutex— the broadcast after the atomicnxio_lsastore is the release fence.- A committer parks in
logpb_flush_pagesunder the four-quadrant(async, group)policy; only the synchronous quadrants enter thegc_condCV loop, returning only whennxio_lsa >= flush_lsa— a commit ack thus implies a durableLOG_COMMIT. - The daemon’s period is a function
(
log_get_log_group_commit_interval): 0 ms when a request is pending (defeating lost wakeups), else the configured interval capped at 1 s.
Chapter 9: Backpressure and the Self Help Drain
Section titled “Chapter 9: Backpressure and the Self Help Drain”The reader question this chapter answers: what happens when producers fill the prior list faster than the flush daemon can drain it?
Every call to prior_lsa_next_record_internal that takes the lock-free producer path (Chapters 3–5) ends with the same tail block. After it has attached its node and bumped list_size, it checks whether the in-memory prior list has grown past the size of the log page buffer. If it has, the producer applies soft backpressure: it either nudges the flush daemon and yields its timeslice, or — when there is no daemon — drains the list itself. This is the only place in the producer path that reacts to the aggregate depth of the queue rather than to its own single record.
This chapter dissects that tail block branch by branch. It assumes the high-level companion’s treatment of why the prior list exists and how flush works — see cubrid-prior-list.md §“The producer/consumer split” and §“Write-ahead logging”. It introduces no new structs.
9.1 The soft cap: logpb_get_memsize()
Section titled “9.1 The soft cap: logpb_get_memsize()”The threshold is computed fresh on every check by logpb_get_memsize:
// logpb_get_memsize -- src/transaction/log_page_buffer.csize_tlogpb_get_memsize (){ return (size_t) log_Pb.num_buffers * (size_t) LOG_PAGESIZE;}log_Pb.num_buffers is set once at boot from the log_max_buffers system parameter (PRM_ID_LOG_NBUFFERS) in logpb_initialize_pool:
// logpb_initialize_pool -- src/transaction/log_page_buffer.clog_Pb.num_buffers = prm_get_integer_value (PRM_ID_LOG_NBUFFERS);So logpb_get_memsize() is the total byte capacity of the authoritative in-memory log page buffer — the same pool that Chapter 7 copies nodes into. The design ties the prior-list soft cap to that pool deliberately: there is no point letting the pending prior list grow much larger than the buffer it will be copied into, since the copy + flush pipeline downstream is what ultimately bounds memory. The cap is expressed in the same currency as list_size: bytes. Because list_size (Chapter 5) also counts per-node LOG_PRIOR_NODE overhead the page buffer does not, it crosses the threshold somewhat before the data would actually fill the buffer — intentional conservatism so the producer reacts early.
Invariant — the cap is soft, never a hard producer block. Crossing
logpb_get_memsize()never blocks a producer from attaching its node. The node was already attached andlist_sizealready bumped (Chapter 5) before this check runs. The check only decides whether the producer helps drain or yields. A producer that ignored the signal entirely would still be correct — it would just let the list grow. The real, hard flow-control bound on the system is the WAL invariant on data pages (a dirty data page cannot be flushed past the log; seecubrid-prior-list.md§“Write-ahead logging”): that is what eventually stalls transactions, not this soft cap. This block is a pressure-relief valve, not a gate.
flowchart LR
subgraph prod["producer (no mutex held here)"]
A["list_size += node bytes<br/>(under mutex, Ch.5)"] --> B["unlock prior_lsa_mutex"]
B --> C{"list_size >=<br/>logpb_get_memsize() ?"}
end
C -->|no| Z["return start_lsa"]
C -->|yes| D["PSTAT_PRIOR_LSA_LIST_MAXED++"]
D --> E["dispatch by build/runtime mode<br/>(Figure 9-2)"]
E --> Z
Figure 9-1. Where the backpressure check sits relative to the mutex. The check runs only on the WITHOUT_LOCK producer path, and only after the mutex is released.
9.2 The operator-visible signal: PSTAT_PRIOR_LSA_LIST_MAXED
Section titled “9.2 The operator-visible signal: PSTAT_PRIOR_LSA_LIST_MAXED”The first thing the tail block does, unconditionally once the threshold is crossed, is bump a performance counter:
// prior_lsa_next_record_internal -- src/transaction/log_append.cppif (log_Gl.prior_info.list_size >= (INT64) logpb_get_memsize ()) { perfmon_inc_stat (thread_p, PSTAT_PRIOR_LSA_LIST_MAXED); /* <- counts every "maxed" event */ // ... mode dispatch below ... }PSTAT_PRIOR_LSA_LIST_MAXED is the operator-visible symptom of producer/consumer imbalance. It is incremented once per maxed event, before any of the three mode branches run, so it counts both the daemon-nudge path and the self-help-drain path. A rising Num_prior_lsa_list_maxed in cubrid statdump means producers are outrunning the flush daemon often enough to trip the soft cap — a signal to investigate log device throughput, log_max_buffers sizing, or flush daemon scheduling. Contrast it with PSTAT_PRIOR_LSA_LIST_REMOVED / PSTAT_PRIOR_LSA_LIST_SIZE, which the drain side emits in logpb_prior_lsa_append_all_list (§9.4) to report how much was actually drained.
9.3 The three-way dispatch
Section titled “9.3 The three-way dispatch”The (INT64) cast on logpb_get_memsize() matters: list_size is a signed INT64 and the comparison must be signed-vs-signed, so the size_t return is narrowed deliberately. Past the increment, the block splits three ways across two compile-time #if arms and one runtime branch inside the SERVER_MODE arm:
// prior_lsa_next_record_internal -- src/transaction/log_append.cpp#if defined(SERVER_MODE) if (!log_is_in_crash_recovery ()) /* <- runtime split inside SERVER_MODE */ { log_wakeup_log_flush_daemon (); /* (a) nudge the daemon ... */ thread_sleep (1); /* ... and yield 1 msec */ } else { LOG_CS_ENTER (thread_p); /* (b) crash recovery: no daemon yet */ logpb_prior_lsa_append_all_list (thread_p); /* so drain it ourselves */ LOG_CS_EXIT (thread_p); }#else LOG_CS_ENTER (thread_p); /* (c) standalone: never a daemon */ logpb_prior_lsa_append_all_list (thread_p); LOG_CS_EXIT (thread_p);#endifstateDiagram-v2 [*] --> Maxed: list_size >= memsize Maxed --> ServerMode: compiled SERVER_MODE Maxed --> Standalone: not SERVER_MODE ServerMode --> Normal: not in crash recovery ServerMode --> Recovery: in crash recovery Normal --> Nudge: wakeup daemon then sleep 1ms Recovery --> SelfHelp: LOG_CS then append_all_list then LOG_CS_EXIT Standalone --> SelfHelp: LOG_CS then append_all_list then LOG_CS_EXIT Nudge --> [*] SelfHelp --> [*]
Figure 9-2. The three backpressure outcomes. Two are compile-time selected; the SERVER_MODE arm splits once more at runtime on crash-recovery state.
(a) SERVER_MODE, normal operation — nudge and yield. This is the steady-state production path. A dedicated flush daemon exists (log_Flush_daemon), so the producer does not touch the log critical section at all. It calls log_wakeup_log_flush_daemon, which sets the request flag and wakes the daemon:
// log_wakeup_log_flush_daemon -- src/transaction/log_manager.cvoidlog_wakeup_log_flush_daemon (){ if (log_is_log_flush_daemon_available ()) /* <- false if log_Flush_daemon == NULL */ {#if defined (SERVER_MODE) log_Flush_has_been_requested = true; /* <- daemon checks this to avoid spurious wakeups */ log_Flush_daemon->wakeup ();#endif /* SERVER_MODE */ }}Then the producer calls thread_sleep (1) — yields for one millisecond. The sleep is the actual backpressure: the producing thread voluntarily stalls, giving the daemon CPU time and the disk a chance to catch up before this thread can enqueue more. It is cooperative, not enforced — the producer chose to sleep; nothing prevented it from continuing. Note log_wakeup_log_flush_daemon is internally guarded by log_is_log_flush_daemon_available(), so even on this arm a missing daemon makes the wakeup a no-op (the producer would then sleep 1 ms and re-check on its next record) — but in normal SERVER_MODE the daemon is always present.
(b) SERVER_MODE, during crash recovery — self-help drain. log_is_in_crash_recovery() returns true during restart recovery, before the flush daemon has been started. With no daemon to nudge, the nudge-and-sleep strategy would stall forever — the list would never drain. So the producer drains it inline: it enters the log critical section (LOG_CS_ENTER), calls logpb_prior_lsa_append_all_list to copy-and-detach the whole list (§9.4), and exits. This is the “self-help” path: the producer becomes the consumer for one drain cycle.
(c) Standalone (non-SERVER_MODE) — always self-help. In standalone/SA builds there is never a flush daemon (log_is_log_flush_daemon_available() is hard-coded false). The #else arm is therefore identical to (b) and unconditional: every maxed event self-drains under LOG_CS. There is no log_is_in_crash_recovery() test here because the answer does not matter — with no daemon, self-help is the only option in or out of recovery.
Invariant —
prior_lsa_mutexis released beforeLOG_CSis taken, so self-help cannot deadlock or recurse. Look at Figure 9-1: the threshold check, and therefore all of (a)/(b)/(c), runs afterlog_Gl.prior_info.prior_lsa_mutex.unlock(). This matters for two reasons. (1) Lock ordering:logpb_prior_lsa_append_all_listre-acquiresprior_lsa_mutexinternally (§9.4) while holdingLOG_CS. Releasing first fixes the producer’s order toLOG_CSthenprior_lsa_mutex, matching the drain side; holdingprior_lsa_mutexacross theLOG_CS_ENTERwould self-deadlock on that re-acquire. (2) No recursion:logpb_prior_lsa_append_all_listnever calls back intoprior_lsa_next_record_internal, and the producer is not holdingprior_lsa_mutex, so the self-help drain is a flat call, not a re-entrant one.
9.4 The drain primitive reused by self-help: logpb_prior_lsa_append_all_list
Section titled “9.4 The drain primitive reused by self-help: logpb_prior_lsa_append_all_list”Both self-help arms call the same function the flush daemon uses to drain. It is the natural seam between this chapter and Chapters 6–7 (detach + copy):
// logpb_prior_lsa_append_all_list -- src/transaction/log_page_buffer.cintlogpb_prior_lsa_append_all_list (THREAD_ENTRY * thread_p){ LOG_PRIOR_NODE *prior_list; INT64 current_size;
assert (LOG_CS_OWN_WRITE_MODE (thread_p)); /* <- caller MUST hold LOG_CS in write mode */
log_Gl.prior_info.prior_lsa_mutex.lock (); /* <- the re-acquire the §9.3 invariant guards */ current_size = log_Gl.prior_info.list_size; prior_list = prior_lsa_remove_prior_list (thread_p); /* detach whole list, reset size to 0 (Ch.6) */ log_Gl.prior_info.prior_lsa_mutex.unlock ();
if (prior_list != NULL) /* <- empty-list guard: nothing to copy */ { perfmon_add_stat (thread_p, PSTAT_PRIOR_LSA_LIST_SIZE, (unsigned int) current_size / ONE_K); /* kbytes drained */ perfmon_inc_stat (thread_p, PSTAT_PRIOR_LSA_LIST_REMOVED);
logpb_append_prior_lsa_list (thread_p, prior_list); /* copy nodes into log pages (Ch.7) */ }
return NO_ERROR;}Branch-complete reading of this function from the self-help caller’s perspective:
assert (LOG_CS_OWN_WRITE_MODE)— both self-help arms satisfy this because they wrapped the call inLOG_CS_ENTER/LOG_CS_EXIT. This is why the producer must takeLOG_CSitself before calling; the function does not take it.- Snapshot + detach under
prior_lsa_mutex.current_sizeis read, thenprior_lsa_remove_prior_listunhooks the entire list (head/tail → NULL,list_size→ 0; see Chapter 6). The mutex protects the detach against concurrent producers and is released immediately. prior_list != NULLbranch (non-empty): reportPSTAT_PRIOR_LSA_LIST_SIZEin kilobytes and bumpPSTAT_PRIOR_LSA_LIST_REMOVED, then hand the detached chain tologpb_append_prior_lsa_list(Chapter 7) which copies every node into the authoritative log pages. After this returns,list_sizeis back at 0 and the producer’s next-record check (§9.1) will pass cleanly.prior_list == NULLbranch (empty): another drainer (the daemon, or a racing self-help producer) detached the list between this producer’s threshold check and itsLOG_CS_ENTER. The function silently returnsNO_ERRORwith no copy and no perf stats. This is the benign race the empty-list guard exists for — the producer that lost the race did redundantLOG_CSwork but caused no harm.
Invariant — after a successful self-help drain,
list_size == 0and the detached chain is fully owned by the draining thread.prior_lsa_remove_prior_listresetslist_sizeto 0 and nulls both head and tail pointers underprior_lsa_mutexatomically, so a concurrent producer can never observe a half-detached list (head moved but size not reset, or vice versa). The returnedprior_listis then private to the draining thread — no other thread can reach those nodes — which is what makes the subsequentlogpb_append_prior_lsa_listcopy safe to run outsideprior_lsa_mutex(it still holdsLOG_CS). Violating the atomicity — say, resetting size before nulling head — would let a producer append onto a chain that is about to be copied and freed.
9.5 Why nudge-and-sleep in production but self-help in recovery/standalone
Section titled “9.5 Why nudge-and-sleep in production but self-help in recovery/standalone”The asymmetry follows from who else can drain. In normal SERVER_MODE the flush daemon is the designated drainer, so producers stay out of LOG_CS and apply pressure purely by yielding — having them also self-drain would contend with the daemon under exactly the load that trips the cap. In crash recovery the daemon does not exist yet (recovery replays log before background services start), and in standalone it never exists; with no specialist, the producer must drain or the list grows unbounded. The single shared primitive logpb_prior_lsa_append_all_list funnels recovery, standalone, and the daemon through identical copy-and-flush logic; only who calls it and whether it is wrapped in a yield differs.
9.6 Chapter summary — key takeaways
Section titled “9.6 Chapter summary — key takeaways”- The check is
list_size >= logpb_get_memsize(), wherelogpb_get_memsize()isnum_buffers * LOG_PAGESIZE— the byte capacity of the in-memory log page buffer. Both sides are bytes;list_sizeincludes per-node overhead, so it trips slightly early by design. - The cap is soft. The node is already attached and counted before the check; crossing the threshold never blocks the producer from enqueuing. Hard flow control comes from the WAL invariant on data pages, not from this block.
PSTAT_PRIOR_LSA_LIST_MAXEDis bumped once per maxed event, before any mode branch — it is the operator-visible signal that producers are outrunning the flush daemon.- Three outcomes, two compile-time plus one runtime: (a) SERVER_MODE normal →
log_wakeup_log_flush_daemon+thread_sleep(1)(nudge and yield, never touchesLOG_CS); (b) SERVER_MODE during crash recovery → self-help drain underLOG_CS; (c) standalone → always self-help drain underLOG_CS. - Self-help exists because there is no flush daemon during recovery or in standalone builds. The producer temporarily becomes the consumer, reusing the daemon’s own drain primitive
logpb_prior_lsa_append_all_list. prior_lsa_mutexis released beforeLOG_CSis taken, fixing the lock order toLOG_CSthenprior_lsa_mutexand preventing both self-deadlock and re-entrancy when the self-help path re-acquiresprior_lsa_mutexinside the drain.- The drain is race-tolerant:
logpb_prior_lsa_append_all_listdetaches and zeroeslist_sizeatomically underprior_lsa_mutex, then guards onprior_list != NULLso a producer that lost the drain race to the daemon simply does no work and returns cleanly.
Chapter 10: Special and Edge Paths
Section titled “Chapter 10: Special and Edge Paths”Chapters 3-9 traced one record through the steady-state lifecycle
(build outside the mutex, assign LSN and link, detach, copy into the
LOG_PAGE ring, flush, backpressure). This chapter collects the paths
that do not fit that shape: checkpoint’s across-generation mutex
hold, TDE flag propagation, the node-less LSA stamp, the shutdown drain
assertion, crash with a non-empty list, and the non-commit force-flush
entry points — all downstream of one rule:
Master invariant (commit durability). A transaction never sees a commit ack unless its
LOG_COMMITrecord is durable. Every edge path below either upholds this directly or is a no-op with respect to it.
10.1 The checkpoint path — holding the mutex across record generation
Section titled “10.1 The checkpoint path — holding the mutex across record generation”The normal attach (prior_lsa_next_record, Ch 5) takes
prior_lsa_mutex internally for one node. Checkpoint needs more: its
LOG_END_CHKPT payload (tran table + system top-op array) must reflect
a state no other record mutates while it is stamped — a worker LSN
assigned between snapshot and attach would leave the recorded redo LSA
stale. So logpb_checkpoint locks once, generates the record while
holding it, attaches via the with-lock variant, and unlocks:
// logpb_checkpoint (END_CHKPT span) -- src/transaction/log_page_buffer.c (condensed)log_Gl.prior_info.prior_lsa_mutex.lock (); /* <- held across the whole span */// ... snapshot tran table + system topops, build node ...node = prior_lsa_alloc_and_copy_data (thread_p, LOG_END_CHKPT, RV_NOT_DEFINED, NULL, ...);if (node == NULL) { log_Gl.prior_info.prior_lsa_mutex.unlock (); /* <- error path must release once */ goto error_cannot_chkpt;}// ... *chkpt = tmp_chkpt ...prior_lsa_next_record_with_lock (thread_p, node, tdes); /* <- attach WITHOUT re-locking */log_Gl.prior_info.prior_lsa_mutex.unlock (); /* <- single matching release */prior_lsa_next_record_with_lock wraps
prior_lsa_next_record_internal (..., LOG_PRIOR_LSA_WITH_LOCK). Per
Ch 5 the internal function locks only on the WITHOUT_LOCK arm; the
WITH_LOCK arm assumes the caller holds it and also skips the
backpressure tail (the list_size >= logpb_get_memsize() self-help
of Ch 9), so checkpoint relies on its own logpb_flush_pages_direct
after the unlock.
Invariant (single acquisition —
std::mutexis non-recursive). Every_with_lockcaller must holdprior_lsa_mutexexactly once and pair onelock()with oneunlock()on every path including the errorgoto. Enforced only by audit: the internal function never locks on the with-lock arm. Breaks if violated — a caller that both pre-locks and calls the plainprior_lsa_next_record(which locks again) self-deadlocks.
The other with-lock site is the replication-and-commit path: the
orchestrator log_append_repl_info_and_commit_log locks once and
sequences log_append_repl_info_with_lock then
log_append_commit_log_with_lock (which also covers the
log_append_donetime_internal done-time record), so a foreign commit
cannot land between a tx’s replication records and its LOG_COMMIT —
its header comment warns that would “break consistencies of
slaves/replicas”. The inner log_append_repl_info_internal branches on
with_lock: the LOG_PRIOR_LSA_WITH_LOCK arm calls
prior_lsa_next_record_with_lock inside the held span, else plain
prior_lsa_next_record.
flowchart TD
A["caller needs N records\nwith no interleave"] --> B["prior_lsa_mutex.lock() ONCE"]
B --> C["build record\nprior_lsa_alloc_and_copy_*"]
C --> D{"alloc ok?"}
D -->|no| E["unlock() ONCE\ngoto error"]
D -->|yes| F["prior_lsa_next_record_with_lock\nno re-lock, no backpressure"]
F --> G{"more records?"}
G -->|yes| C
G -->|no| H["prior_lsa_mutex.unlock() ONCE"]
H --> I["logpb_flush_pages_direct\nown durability"]
Figure 10-1 — The with-lock atomic-group write. The mutex spans every record’s generation and attach; the error path at D releases the single lock, and durability (I) is the caller’s own responsibility.
10.2 TDE — propagating tde_encrypted from node to page
Section titled “10.2 TDE — propagating tde_encrypted from node to page”The prior node carries one bool, node->tde_encrypted, set by
prior_set_tde_encrypted (producer) and read by
prior_is_tde_encrypted (drain):
// prior_set_tde_encrypted -- src/transaction/log_append.cppintprior_set_tde_encrypted (log_prior_node *node, LOG_RCVINDEX recvindex){ if (!tde_is_loaded()) /* <- master key must be loaded */ { er_set (ER_ERROR_SEVERITY, ARG_FILE_LINE, ER_TDE_CIPHER_IS_NOT_LOADED, 0); return ER_TDE_CIPHER_IS_NOT_LOADED; /* <- caller asserts(false) on this */ } node->tde_encrypted = true; return NO_ERROR; /* <- success write */}// prior_is_tde_encrypted -- src/transaction/log_append.cppbool prior_is_tde_encrypted (const log_prior_node *node) { return node->tde_encrypted; } /* <- pure read */The setter has two branches: the cipher-not-loaded error (every caller
treats it as assert(false) — a should-be-encrypted record that cannot
be is a logic error) and the success write. Public append APIs call it
only when the recvindex targets a TDE-class object. On the drain side
logpb_append_next_record reads the flag into a page-side global before
copying any bytes, then clears it:
// logpb_append_next_record -- src/transaction/log_page_buffer.clog_Gl.append.appending_page_tde_encrypted = prior_is_tde_encrypted (node); /* <- node flag -> page flag *//* ... logpb_start_append, logpb_append_data x3, logpb_end_append ... */log_Gl.append.appending_page_tde_encrypted = false; /* <- cleared after the record */appending_page_tde_encrypted is a member of log_append_info (the
page-side cursor, Ch 1), not the node or queue. While true, two
consumers brand the destination page:
// logpb_next_append_page -- src/transaction/log_page_buffer.c (page allocation)if (log_Gl.append.appending_page_tde_encrypted) /* <- on a brand-new page: stamp unconditionally */ { TDE_ALGORITHM tde_algo = (TDE_ALGORITHM) prm_get_integer_value (PRM_ID_TDE_DEFAULT_ALGORITHM); logpb_set_tde_algorithm (thread_p, log_Gl.append.log_pgptr, tde_algo); /* <- stamp the new page */ logpb_set_dirty (thread_p, log_Gl.append.log_pgptr); }logpb_start_append runs the identical stamp on an existing page but
idempotently — guarded on !LOG_IS_PAGE_TDE_ENCRYPTED (...) so it
only brands a page logpb_next_append_page did not already brand.
Either way the brand is honoured at write time: the flush path encrypts
pages carrying the TDE algorithm flag. The bool thus rides the node to
the drain, transiently brands the page, and is cleared per record.
Hazard (master-key rotation between build and drain). A node is flagged under the master-key generation loaded on the producer thread, but encryption happens later on the drain/write path — possibly after the operator rotated the key. The node carries no IV or key-generation bookkeeping, only the bool, so a rotation in that window leaves the page branded under the current generation while the intent was formed under the old one.
tde_is_loaded()gating and the administrative nature of rotation are the only mitigations. This is Open Question 6 in the companion.
10.3 The direct LSA-stamp-under-mutex path
Section titled “10.3 The direct LSA-stamp-under-mutex path”log_skip_logging_set_lsa is the one prior_lsa-mutex acquisition
that attaches no node and does not advance prior_lsa — it only
reads prior_lsa to stamp a coherent value onto the data page via
pgbuf_set_lsa. It serves the case where a data page is modified but
intentionally not logged (e.g. log_no_logging mode), yet the page
still needs an LSA so the page buffer’s WAL check has something
monotonic to compare against:
// log_skip_logging_set_lsa -- src/transaction/log_manager.clog_Gl.prior_info.prior_lsa_mutex.lock ();(void) pgbuf_set_lsa (thread_p, addr->pgptr, &log_Gl.prior_info.prior_lsa); /* <- stamp current prior_lsa, do NOT advance it */log_Gl.prior_info.prior_lsa_mutex.unlock ();Reading without the lock could observe a torn value while a concurrent
attach advances prior_lsa; the mutex gives the page a coherent LSA
<= any record subsequently appended — exactly what the WAL invariant
needs (the page’s LSA must never lead the durable log). No record means
no commit ack, so it is a no-op w.r.t. the master invariant.
10.4 The shutdown drain assertion
Section titled “10.4 The shutdown drain assertion”log_prior_has_worker_log_records asks “does the prior list still hold
any non-system (user) records not yet drained to pages?” Its sole
caller is the server stop sequence in server_support.c: after workers
and vacuum are stopped and logpb_force_flush_pages has run, it is
wrapped in a debug-only
assert (!log_prior_has_worker_log_records (thread_p)) confirming no
worker work is left un-drained before the log writers stop — a
shutdown-time sanity check (relevant to HA, since the log writer must
ship all user log first). The probe walks the live (undrained) list
under the lock:
// log_prior_has_worker_log_records -- src/transaction/log_append.cpp (condensed)LOG_CS_ENTER (thread_p); /* <- outer lock first */std::unique_lock<std::mutex> ulock (log_Gl.prior_info.prior_lsa_mutex); /* <- inner lock second */LOG_LSA nxio_lsa = log_Gl.append.get_nxio_lsa ();if (!LSA_EQ (&nxio_lsa, &log_Gl.prior_info.prior_lsa)) /* <- durable cursor behind producer cursor? */ { assert (LSA_LT (&nxio_lsa, &log_Gl.prior_info.prior_lsa)); /* <- nxio can only lag, never lead */ for (node = log_Gl.prior_info.prior_list_header; node != NULL; node = node->next) if (node->log_header.trid != LOG_SYSTEM_TRANID) /* <- a USER record is pending */ { ulock.unlock (); LOG_CS_EXIT (thread_p); return true; } /* <- early exit on first user node */ }ulock.unlock (); LOG_CS_EXIT (thread_p);return false; /* <- no user records pending */Branch-complete reading: if nxio_lsa == prior_lsa the queue is fully
drained and flushed — skip the loop, return false. If
nxio_lsa < prior_lsa (undrained/unflushed work, assert confirms the
cursor can only lag), walk the list: a trid != LOG_SYSTEM_TRANID node
(real worker tx) returns true immediately — the only early-return;
LOG_SYSTEM_TRANID nodes (vacuum, checkpoint housekeeping) are skipped;
reaching the end with only system nodes falls through to return false.
Lock ordering matches the global discipline — LOG_CS (outer) before
prior_lsa_mutex (inner) — and this is the only reader that walks
the live prior_list_header chain under the mutex; the drain detaches
the chain (Ch 6) before walking it, so the two never race the same list.
Invariant (cursor ordering —
nxio_lsa <= prior_lsa). The durable I/O cursor can equal or lag the producer cursor, never exceed it. Enforced by construction:prior_lsaadvances only at attach (under the mutex);nxio_lsaadvances only at flush (after the named bytes are on disk). Breaks if violated: durability would be claimed for LSNs no record produced.
10.5 Force-flush wrappers and the WAL-invariant consumer
Section titled “10.5 Force-flush wrappers and the WAL-invariant consumer”Two thin wrappers serve callers needing durability now, off the
group-commit path. Both wrap logpb_flush_pages_direct (drain + write,
Ch 7-8) inside LOG_CS_ENTER/LOG_CS_EXIT. logpb_force_flush_pages
is the bare “make everything durable” call, used by server shutdown
(server_support.c), heap / disk / btree bulk operations, the page
buffer, and vacuum. Checkpoint does not use this wrapper — it calls
logpb_flush_pages_direct directly after the END_CHKPT attach (§10.1).
logpb_force_flush_header_and_pages adds one logpb_flush_header call
after the pages: the header records the last checkpoint and
end-of-log location, so writing it before the pages it references would
point past durable data. Pages first, then header.
The WAL-invariant consumer is the page buffer’s contract enforcer:
// logpb_flush_log_for_wal -- src/transaction/log_page_buffer.c (condensed)if (logpb_need_wal (lsa_ptr)) /* <- 1st check: data page ahead of durable log? */ { LOG_CS_ENTER (thread_p); if (logpb_need_wal (lsa_ptr)) /* <- 2nd check (double-checked under CS) */ logpb_flush_pages_direct (thread_p); /* <- force the log forward */ LOG_CS_EXIT (thread_p); assert (LSA_ISNULL (lsa_ptr) || !logpb_need_wal (lsa_ptr)); /* <- WAL rule now holds */ }The page-buffer manager calls this before writing any dirty data page.
It is double-checked, three branches: (1) outer !logpb_need_wal —
the durable log already covers lsa_ptr, return with no CS (hot path);
(2) outer true, inner false — a concurrent flush advanced nxio_lsa
past the page’s LSA in the window, do nothing; (3) both true — flush
forward, then the post-condition assert confirms the page is safe to
write. This is where the prior-list drain is forced on behalf of a
page flush, making it a participant in page-durability, not just the
commit path.
10.6 Crash with a non-empty prior list
Section titled “10.6 Crash with a non-empty prior list”The most important edge has the least code: there is no special
handling, and that is correct. At crash time the prior list may hold
any number of malloc’d LOG_PRIOR_NODEs whose bytes were never copied
into a log page, let alone fsync’d. These nodes are memory-only and were
never acked — any committing owner would still be parked on gc_cond
in logpb_flush_pages awaiting nxio_lsa >= commit_lsa, so no client
believes it committed. Recovery sees no LOG_COMMIT, marks the
transaction active-at-crash, and undoes its durable records cleanly
(data pages obey the WAL invariant, §10.5). The un-drained nodes are
simply leaked; the OS reclaims the heap at process exit, with no pool
to corrupt and no on-disk structure to repair.
flowchart TD A["crash, non-empty prior list"] --> B["nodes malloc'd,\nnever on disk, never acked"] B --> F["recovery: no LOG_COMMIT\n-> mark active -> undo"] F --> G["WAL-safe pages -> undo rolls back;\nleaked nodes -> OS reclaims at exit"]
Figure 10-3 — Crash with a non-empty prior list. The durability gate already made the only dangerous case — an acked-but-not-durable commit — impossible, so no crash-time cleanup is needed.
10.7 Chapter summary — key takeaways
Section titled “10.7 Chapter summary — key takeaways”- Checkpoint holds
prior_lsa_mutexacross record generation, not just attach, viaprior_lsa_next_record_with_lock.std::mutexis non-recursive: every with-lock caller locks once, releases on every path (including errorgotos), and gets no backpressure self-help. - TDE is one bool on the node.
prior_set_tde_encrypted(gated ontde_is_loaded()) sets it; the drain reads it viaprior_is_tde_encryptedintoappending_page_tde_encrypted, which brands the page idempotently acrosslogpb_next_append_pageandlogpb_start_append, cleared per record. Master-key rotation in that window is unguarded — no key-generation/IV bookkeeping (Open Q 6). log_skip_logging_set_lsais the onlyprior_lsa-mutex acquisition with no record — it reads a coherentprior_lsaand stamps it viapgbuf_set_lsa, without advancing or linking.log_prior_has_worker_log_recordsis a shutdown-time drain assertion (sole caller: the server stop sequence), returning true on the first non-LOG_SYSTEM_TRANIDnode whennxio_lsa < prior_lsa.- The force-flush wrappers and
logpb_flush_log_for_walare the non-commit drain entry points._force_flush_pagesis bare drain+write (not checkpoint);_header_and_pagesflushes the header after the pages;logpb_flush_log_for_waldouble-checks then forces the log forward before a dirty data page is home-written. - Crash with a non-empty list needs no handling. Un-drained nodes are memory-only and leaked at exit; the owner was never acked (master invariant), so recovery correctly undoes the uncommitted work.
Position hints as of this revision
Section titled “Position hints as of this revision”The following are line numbers as observed on 2026-06-19; symbols are the canonical anchor and line numbers are hints that decay.
| Symbol | File | Line |
|---|---|---|
heap_get_visible_version_from_log | src/storage/heap_file.c | 25329 |
pgbuf_has_perm_pages_fixed | src/storage/page_buffer.c | 11424 |
log_zip_undo | src/thread/thread_entry.hpp | 259 |
log_data_ptr | src/thread/thread_entry.hpp | 261 |
log_Zip_support | src/transaction/log_append.cpp | 40 |
log_Zip_min_size_to_compress | src/transaction/log_append.cpp | 41 |
LOG_PRIOR_LSA_LAST_APPEND_OFFSET | src/transaction/log_append.cpp | 44 |
log_append_info::get_nxio_lsa | src/transaction/log_append.cpp | 106 |
log_append_info::set_nxio_lsa | src/transaction/log_append.cpp | 112 |
log_prior_lsa_info::log_prior_lsa_info | src/transaction/log_append.cpp | 117 |
LOG_RESET_APPEND_LSA | src/transaction/log_append.cpp | 129 |
LOG_RESET_PREV_LSA | src/transaction/log_append.cpp | 137 |
LOG_APPEND_PTR | src/transaction/log_append.cpp | 145 |
log_prior_has_worker_log_records | src/transaction/log_append.cpp | 152 |
log_append_init_zip | src/transaction/log_append.cpp | 185 |
log_append_final_zip | src/transaction/log_append.cpp | 232 |
prior_lsa_alloc_and_copy_data | src/transaction/log_append.cpp | 273 |
prior_lsa_alloc_and_copy_crumbs | src/transaction/log_append.cpp | 409 |
prior_lsa_copy_undo_data_to_node | src/transaction/log_append.cpp | 492 |
prior_lsa_copy_undo_data_to_node | src/transaction/log_append.cpp | 493 |
prior_lsa_copy_redo_data_to_node | src/transaction/log_append.cpp | 523 |
prior_lsa_copy_redo_data_to_node | src/transaction/log_append.cpp | 524 |
prior_lsa_copy_undo_crumbs_to_node | src/transaction/log_append.cpp | 554 |
prior_lsa_copy_redo_crumbs_to_node | src/transaction/log_append.cpp | 599 |
prior_lsa_gen_undoredo_record_from_crumbs | src/transaction/log_append.cpp | 650 |
prior_lsa_gen_undoredo_record_from_crumbs | src/transaction/log_append.cpp | 651 |
prior_lsa_gen_postpone_record | src/transaction/log_append.cpp | 1062 |
prior_lsa_gen_dbout_redo_record | src/transaction/log_append.cpp | 1109 |
prior_lsa_gen_2pc_prepare_record | src/transaction/log_append.cpp | 1144 |
prior_lsa_gen_end_chkpt_record | src/transaction/log_append.cpp | 1181 |
prior_lsa_gen_record | src/transaction/log_append.cpp | 1217 |
prior_update_header_mvcc_info | src/transaction/log_append.cpp | 1320 |
prior_lsa_next_record_internal | src/transaction/log_append.cpp | 1357 |
PSTAT_PRIOR_LSA_LIST_MAXED | src/transaction/log_append.cpp | 1524 |
thread_sleep | src/transaction/log_append.cpp | 1531 |
prior_lsa_next_record | src/transaction/log_append.cpp | 1553 |
prior_lsa_next_record_with_lock | src/transaction/log_append.cpp | 1559 |
prior_set_tde_encrypted | src/transaction/log_append.cpp | 1565 |
prior_is_tde_encrypted | src/transaction/log_append.cpp | 1581 |
prior_lsa_start_append | src/transaction/log_append.cpp | 1593 |
prior_lsa_end_append | src/transaction/log_append.cpp | 1652 |
prior_lsa_append_data | src/transaction/log_append.cpp | 1661 |
log_append_get_zip_undo | src/transaction/log_append.cpp | 1725 |
log_append_get_zip_redo | src/transaction/log_append.cpp | 1751 |
log_append_realloc_data_ptr | src/transaction/log_append.cpp | 1787 |
log_append_get_data_ptr | src/transaction/log_append.cpp | 1858 |
log_prior_lsa_append_align | src/transaction/log_append.cpp | 1892 |
log_prior_lsa_append_advance_when_doesnot_fit | src/transaction/log_append.cpp | 1905 |
log_prior_lsa_append_add_align | src/transaction/log_append.cpp | 1917 |
log_crumb | src/transaction/log_append.hpp | 46 |
log_data_addr | src/transaction/log_append.hpp | 53 |
LOG_DATA_ADDR_INITIALIZER | src/transaction/log_append.hpp | 64 |
LOG_PRIOR_LSA_LOCK | src/transaction/log_append.hpp | 66 |
LOG_PRIOR_LSA_WITHOUT_LOCK | src/transaction/log_append.hpp | 68 |
LOG_PRIOR_LSA_WITH_LOCK | src/transaction/log_append.hpp | 69 |
log_append_info | src/transaction/log_append.hpp | 73 |
appending_page_tde_encrypted | src/transaction/log_append.hpp | 81 |
log_prior_node | src/transaction/log_append.hpp | 91 |
log_prior_lsa_info | src/transaction/log_append.hpp | 112 |
prior_flush_list_header | src/transaction/log_append.hpp | 124 |
LOG_RV_RECORD_SET_MODIFY_MODE | src/transaction/log_append.hpp | 204 |
log_zip | src/transaction/log_compress.c | 45 |
log_diff | src/transaction/log_compress.c | 176 |
MAKE_ZIP_LEN | src/transaction/log_compress.h | 33 |
log_zip | src/transaction/log_compress.h | 53 |
LOG_IS_GROUP_COMMIT_ACTIVE | src/transaction/log_impl.h | 124 |
log_Flush_has_been_requested | src/transaction/log_manager.c | 364 |
log_is_in_crash_recovery | src/transaction/log_manager.c | 518 |
LOG_RESET_PREV_LSA | src/transaction/log_manager.c | 887 |
log_initialize_internal | src/transaction/log_manager.c | 1100 |
log_skip_logging_set_lsa | src/transaction/log_manager.c | 3225 |
log_append_repl_info_internal | src/transaction/log_manager.c | 4555 |
log_append_repl_info_and_commit_log | src/transaction/log_manager.c | 4647 |
log_append_donetime_internal | src/transaction/log_manager.c | 4679 |
log_get_log_group_commit_interval | src/transaction/log_manager.c | 10044 |
log_wakeup_log_flush_daemon | src/transaction/log_manager.c | 10126 |
log_is_log_flush_daemon_available | src/transaction/log_manager.c | 10141 |
log_flush_execute | src/transaction/log_manager.c | 10377 |
log_flush_daemon_init | src/transaction/log_manager.c | 10493 |
LOG_CS_DEMOTE | src/transaction/log_manager.c | 15246 |
LOG_CS_PROMOTE | src/transaction/log_manager.c | 15257 |
LOG_CS_OWN_WRITE_MODE | src/transaction/log_manager.h | 281 |
LOG_LAST_APPEND_PTR | src/transaction/log_page_buffer.c | 162 |
LOG_APPEND_ALIGN | src/transaction/log_page_buffer.c | 164 |
LOG_APPEND_ADVANCE_WHEN_DOESNOT_FIT | src/transaction/log_page_buffer.c | 177 |
LOG_APPEND_SETDIRTY_ADD_ALIGN | src/transaction/log_page_buffer.c | 185 |
LOGPB_APPENDREC_STATUS | src/transaction/log_page_buffer.c | 217 |
logpb_initialize_pool | src/transaction/log_page_buffer.c | 553 |
logpb_next_append_page | src/transaction/log_page_buffer.c | 2630 |
logpb_writev_append_pages | src/transaction/log_page_buffer.c | 2780 |
logpb_write_toflush_pages_to_archive | src/transaction/log_page_buffer.c | 2868 |
logpb_append_next_record | src/transaction/log_page_buffer.c | 2981 |
logpb_append_prior_lsa_list | src/transaction/log_page_buffer.c | 3040 |
prior_lsa_remove_prior_list | src/transaction/log_page_buffer.c | 3084 |
logpb_prior_lsa_append_all_list | src/transaction/log_page_buffer.c | 3106 |
logpb_flush_all_append_pages | src/transaction/log_page_buffer.c | 3232 |
logpb_flush_pages_direct | src/transaction/log_page_buffer.c | 3952 |
logpb_flush_pages | src/transaction/log_page_buffer.c | 3980 |
logpb_force_flush_pages | src/transaction/log_page_buffer.c | 4096 |
logpb_force_flush_header_and_pages | src/transaction/log_page_buffer.c | 4104 |
logpb_flush_log_for_wal | src/transaction/log_page_buffer.c | 4162 |
logpb_start_append | src/transaction/log_page_buffer.c | 4207 |
logpb_append_data | src/transaction/log_page_buffer.c | 4290 |
logpb_end_append | src/transaction/log_page_buffer.c | 4455 |
logpb_checkpoint | src/transaction/log_page_buffer.c | 6877 |
logpb_get_memsize | src/transaction/log_page_buffer.c | 11553 |
log_rectype | src/transaction/log_record.hpp | 35 |
LOG_DIFF_UNDOREDO_DATA | src/transaction/log_record.hpp | 122 |
log_rec_header | src/transaction/log_record.hpp | 146 |
log_data | src/transaction/log_record.hpp | 157 |
log_rec_undoredo | src/transaction/log_record.hpp | 167 |
log_rec_undo | src/transaction/log_record.hpp | 176 |
log_rec_redo | src/transaction/log_record.hpp | 184 |
log_vacuum_info | src/transaction/log_record.hpp | 192 |
log_rec_mvcc_undoredo | src/transaction/log_record.hpp | 202 |
log_rec_mvcc_undo | src/transaction/log_record.hpp | 211 |
log_rec_mvcc_redo | src/transaction/log_record.hpp | 220 |
log_rec_donetime | src/transaction/log_record.hpp | 237 |
log_rec_dbout_redo | src/transaction/log_record.hpp | 254 |
log_rec_chkpt | src/transaction/log_record.hpp | 345 |
log_rec_2pc_prepcommit | src/transaction/log_record.hpp | 388 |
LOG_IS_UNDOREDO_RECORD_TYPE | src/transaction/log_record.hpp | 455 |
Sources
Section titled “Sources”cubrid-prior-list.md— the high-level companion. See alsocubrid-log-manager-detail.md(the surrounding append pipeline; this doc zooms into its producer side).- Code:
src/transaction/log_append.{cpp,hpp}, drain side insrc/transaction/log_manager.c, record shapes insrc/transaction/log_record.hpp. - Methodology:
knowledge/methodology/code-analysis-detail-doc.md.