CUBRID Error Management — Per-Thread Error Context, Stack, Message Catalog, and Wire Propagation
Contents:
- Theoretical Background
- Common DBMS Design
- CUBRID’s Approach
- Source Walkthrough
- Cross-check Notes
- Open Questions
- Sources
Theoretical Background
Section titled “Theoretical Background”Every database engine spends a non-trivial fraction of its source lines reporting errors. Buffer pool can’t allocate a frame, B-tree splits a page the wrong way, the network layer reads a short header, the parser sees a keyword in the wrong place — each of these has to turn into a value the caller can branch on, a human-readable string an operator can search for in a manual, a log line with enough context for a post-mortem, and a wire payload a client driver can re-raise as a SQLException. The textbook problem is small but the engineering surface is large, because every subsystem in the engine touches it.
Database System Concepts (Silberschatz, Korth, Sudarshan) does not spend a chapter on error reporting per se, but the constraint shows up implicitly throughout the recovery and concurrency-control chapters: a transaction must be able to unilaterally abort on a dirty read, a deadlock victim must be told why it lost, the WAL writer must distinguish a transient I/O retry from a fatal media failure. Each of those is a return value plus an error code plus a message. The text never names the abstraction, but the implementations all need one.
The implementation literature is more direct. The four constraints that shape every real error subsystem are:
-
Per-thread error state. A relational engine is multi-threaded. The “current error” is not a property of the process — it is a property of whoever just executed a system call. Two threads running queries in parallel must each see only their own most recent error, not each other’s. Implementations therefore park the current-error record in a per-thread slot — TLS in the simple case, a member of the per-thread context struct in the fancy case. CUBRID does the latter.
-
Structured error codes. Returning a textual message as the only signal is a non-starter — callers have to branch, and string comparison is both slow and locale-fragile. Every DBMS has a stable, integer-keyed error-code namespace: PostgreSQL’s SQLSTATE plus ErrCode, Oracle’s
ORA-NNNNN, MySQL’sER_*constants, CUBRID’sER_*macros inerror_code.h. The integer is the protocol; the string is the user interface. -
Localised messages. A Korean DBA reads Korean error messages; an English DBA reads English. The integer code is shared; only the string varies. The standard mechanism is a per-locale message catalog file built by something like
gencat, opened at process start, looked up by(set_id, msg_id)to fetch aprintf-style format string. CUBRID inherits this mechanism from the NetBSD/FreeBSDnl_catdfamily. -
Wire propagation. When the client and server are in different processes, the error must cross the wire. The two workable shapes are: (a) flatten the error to a packed buffer on the server side, ship it inside the response, unflatten on the client; (b) ship a pre-rendered message string and let the client treat it as opaque. CUBRID does (a) with three packed integers (errid, severity, length) followed by the rendered message bytes — and re-raises the error on the client through the same
er_setmachinery so client-side handlers see it the same way they see a local error.
Two further design questions appear once the basic shape is in
place. Severity — is this a warning, an error, or a fatal? A
warning may not even need to be logged; an error must be
remembered and returned; a fatal may require the engine to stop.
Stack — when a cleanup routine called from an error path fails
itself, do we overwrite the original error (and lose the cause) or
nest the new one (and risk leaking entries)? CUBRID’s answer is a
per-thread std::stack<er_message> with explicit er_stack_push /
er_stack_pop calls and a pop_and_keep_error variant that
preserves whichever error survived.
Common DBMS Design
Section titled “Common DBMS Design”The four constraints listed above pin down the shape of every real DBMS error subsystem; the variation lies in which abstractions are exposed to the caller and how aggressive the catalog is.
PostgreSQL — ereport(elevel, ...) macro
Section titled “PostgreSQL — ereport(elevel, ...) macro”PostgreSQL builds errors out of fields (errcode, errmsg,
errdetail, errhint, errcontext, errposition, …) attached to
an in-flight error record by chained calls. The entry point is the
ereport(level, (errcode(ERRCODE_FOO), errmsg("..."), ...))
macro: level selects severity (LOG, WARNING, ERROR,
FATAL, PANIC); the body fills in fields. errmsg accepts a
gettext-translated format string. When the level is ERROR or
above, control non-locally returns to the nearest PG_TRY/PG_CATCH
block via siglongjmp — Postgres uses this catch-throw model to
unwind a backend’s allocations on every transaction abort. A
backend has a single per-process ErrorData stack;
elog_start/elog_finish push and pop entries on it.
MySQL — my_error(error_code, MYF(0), ...) and THD::raise_error_*
Section titled “MySQL — my_error(error_code, MYF(0), ...) and THD::raise_error_*”MySQL’s older C interface is my_error(int err, myf MyFlags, ...),
which formats a message via my_get_err_msg() from the system error
catalog errmsg.sys and prints it through my_message(). The
modern C++ interface goes through THD::raise_error_printf /
THD::raise_warning_printf which writes into the per-THD
diagnostic area (Diagnostics_area). The diagnostic area follows
the SQL-standard model — a stack of conditions accessible through
GET DIAGNOSTICS. Localisation goes through errmsg-utf8.txt
compiled to errmsg.sys per locale.
Oracle — ORA-NNNNN numbers and dbms_utility.format_error_*
Section titled “Oracle — ORA-NNNNN numbers and dbms_utility.format_error_*”Oracle exposes errors as five-digit ORA-NNNNN numbers. Internally
the kernel raises errors through the kge error layer (kernel
generic error). Error definitions live in oraus.msg and locale
catalogs; PL/SQL surfaces them as exceptions named in
STANDARD.sql. Server-side stored procedures can use
DBMS_UTILITY.FORMAT_ERROR_BACKTRACE to recover the call site.
Severity is roughly inferred from the number range. Wire
propagation is via OCI/Net’s structured error packet.
CUBRID — er_set(severity, file, line, code, n_args, ...)
Section titled “CUBRID — er_set(severity, file, line, code, n_args, ...)”CUBRID sits between the PostgreSQL and MySQL designs. The macro is
er_set (and the convenience macros ERROR0..ERROR5,
ERROR_SET_ERROR_*, ERROR_SET_WARNING_*); the integer code
namespace is ER_* from error_code.h; the catalog is a
NetBSD/FreeBSD nl_catd file (cubrid.msg, csql.msg,
utils.msg) per locale; the per-thread state is
cuberr::context carrying a base er_message plus a
std::stack<er_message> for explicit save/restore. There is no
catch-throw — the engine is C-style, errors return through ordinary
return values and live in the thread context until cleared. The
wire format is the smallest interesting one: three packed OR_INT
fields plus the rendered message string. The next section walks
each piece.
CUBRID’s Approach
Section titled “CUBRID’s Approach”CUBRID’s error subsystem is shared between the client and the
server. The same error_manager.c compiles into the cub_server
binary (SERVER_MODE), the cubridcs client library (CS_MODE),
and the cubridsa standalone library (SA_MODE); the same
er_set calls happen on both ends; the wire format is what bridges
them. Three preprocessor guards select the per-mode behaviour:
SERVER_MODE— the per-threadcuberr::contextis owned bycubthread::entry(one context per worker, one per daemon), ander_setwrites into the context bound to the callingTHREAD_ENTRY *. Cross-reference:cubrid-thread-worker-pool.md.CS_MODE— the client process has one singleton context,er_Singleton_context_p, registered as the thread-local context for the main thread. Helper threads (broker connection threads, HA copy/applylogdb workers) register their own context by hand.SA_MODE— same asSERVER_MODEbut the entry pool size is small; thread-context lookup falls back to the singleton when the manager is not yet initialised.
The first thing to understand is the data layout: what state lives where.
// cuberr::er_message — base/error_context.hppstruct er_message{ int err_id; // most recent error code, or NO_ERROR int severity; // FATAL / ERROR / SYNTAX / WARNING / NOTIFICATION const char *file_name; // file of the er_set call site int line_no; // line of the er_set call site std::size_t msg_area_size; char *msg_area; // rendered formatted message, owned by this struct er_va_arg *args; // captured printf args, for re-rendering int nargs; char msg_buffer[ER_EMERGENCY_BUF_SIZE]; // 256-byte SBO buffer // ... condensed ...};
class context{ er_message m_base_level; // the "current" error std::stack<er_message> m_stack; // pushed errors, top() is current if non-empty bool m_automatic_registration; // ... condensed ...};Three observations frame the rest of this section.
First, er_message carries an inline 256-byte buffer
(msg_buffer) with msg_area pointing into it for short
messages. Only when a message exceeds 256 bytes does
reserve_message_area allocate a heap buffer. This keeps the
common case allocation-free.
Second, the captured arguments (args, nargs) live alongside
the rendered text. After er_set returns, both the rendered
string in msg_area and the raw argument values in args
are accessible — though the typical readers (er_msg, the wire
packer) only consume the rendered string.
Third, context::get_current_error_level() returns
m_stack.top() if the stack is non-empty, else m_base_level.
The “current” error is whichever level is on top, and
er_stack_push / er_stack_pop change which level is active by
pushing or popping. This is structurally cleaner than the
PostgreSQL chain-of-fields model — at any moment exactly one
er_message is “live” — at the cost of having to remember to
push/pop in cleanup paths.
Error code enum
Section titled “Error code enum”error_code.h is a flat list of #define ER_xxx -N macros, one
per error, with NO_ERROR = 0 and ER_FAILED = -1 reserved at
the top:
// NO_ERROR / ER_FAILED — base/error_code.h#define NO_ERROR 0#define ER_FAILED -1
#define ER_GENERIC_ERROR -2#define ER_OUT_OF_VIRTUAL_MEMORY -3#define ER_INTERRUPTED -4// ... condensed ...#define ER_LK_UNILATERALLY_ABORTED -72#define ER_LK_OBJECT_TIMEOUT_SIMPLE_MSG -73// ... condensed ...#define ER_LAST_ERROR -1371Two design choices are worth calling out.
All non-zero error codes are negative. The code is the
difference from the success value, and er_find_fmt uses
-err_id as an array index into er_Fmt_list. Severity is not
encoded in the code; the same ER_INTERRUPTED (-4) can be raised
at WARNING severity from a cleanup path or at ERROR severity from
a user query.
The codes are grouped by subsystem prefix. The prefix encodes
which module owns the error: ER_LK_* for lock manager, ER_LOG_*
for the log writer/recoverer, ER_BO_* for boot, ER_BTREE_*
for B-tree, ER_PB_* for page buffer, ER_HEAP_* for heap files,
ER_DISK_* for disk manager, ER_NET_* for network, ER_AU_*
for auth, ER_SM_* for schema manager, ER_QPROC_* for query
processing, ER_TR_* for triggers, ER_LDR_* for the bulk
loader, ER_FK_* for foreign keys, ER_TP_* for type coercion,
ER_LC_* for the locator, ER_IO_* for raw I/O, ER_FILE_*
for file system, ER_CT_* for catalog, ER_OBJ_* for object
manager, ER_REGU_* for the regu/expression evaluator, and so
on. Adding a new error means picking a prefix, the next free
slot below ER_LAST_ERROR, and bumping ER_LAST_ERROR. The
header itself reminds the editor of the six-step ritual:
// caution comment — base/error_code.h, top of file/* * CAUTION! * * When an entry is added here please ensure that the msg/<locale>/cubrid.msg * files are updated with matching error strings. See message_catalog.c for * details. * The error codes must also be added to compat/dbi_compat.h * ER_LAST_ERROR must also be updated. * In case of common, * cci repository source (src/cci/base_error_code.h) must be updated, * because CCI source and Engine source have been separated. */The codes that matter most to the rest of the engine — the ones
called out by macro and inspected by callers — are these, taken
verbatim from error_manager.h:
// lock-manager fingerprints — base/error_manager.h#define ER_IS_LOCK_TIMEOUT_ERROR(err) \ ((err) == ER_LK_UNILATERALLY_ABORTED \ || (err) == ER_LK_OBJECT_TIMEOUT_SIMPLE_MSG \ || (err) == ER_LK_OBJECT_TIMEOUT_CLASS_MSG \ || (err) == ER_LK_OBJECT_TIMEOUT_CLASSOF_MSG \ || (err) == ER_LK_OBJECT_DL_TIMEOUT_SIMPLE_MSG \ || (err) == ER_LK_OBJECT_DL_TIMEOUT_CLASS_MSG \ || (err) == ER_LK_OBJECT_DL_TIMEOUT_CLASSOF_MSG)
#define ER_IS_ABORTED_DUE_TO_DEADLOCK(err) \ ((err) == ER_LK_UNILATERALLY_ABORTED \ || (err) == ER_TM_SERVER_DOWN_UNILATERALLY_ABORTED)
#define ER_IS_SERVER_DOWN_ERROR(err) \ ((err) == ER_TM_SERVER_DOWN_UNILATERALLY_ABORTED \ || (err) == ER_NET_SERVER_CRASHED \ || (err) == ER_OBJ_NO_CONNECT \ || (err) == ER_BO_CONNECT_FAILED \ || (err) == ER_NET_CANT_CONNECT_SERVER)These macros are the authoritative answer to “is this error a
deadlock?” / “is this a server-down condition?” and are read by
the broker, the JDBC driver shim, the HA copy daemons, and any
user-mode utility that needs to retry. The prefix discipline is
not enough by itself — the lock manager has both real lock
timeouts (ER_LK_OBJECT_TIMEOUT_*) and deadlock-victim
abort-tickets (ER_LK_UNILATERALLY_ABORTED), and the macro is
how callers tell the categories apart.
Severity
Section titled “Severity”// er_severity — base/error_manager.henum er_severity{ ER_FATAL_ERROR_SEVERITY, // 0 ER_ERROR_SEVERITY, // 1 ER_SYNTAX_ERROR_SEVERITY, // 2 ER_WARNING_SEVERITY, // 3 ER_NOTIFICATION_SEVERITY, // 4 ER_MAX_SEVERITY = ER_NOTIFICATION_SEVERITY};Severity is a separate axis from the error code: the same code can be raised at any severity, and the severity dictates downstream behaviour rather than the error’s identity. Five levels are defined.
ER_FATAL_ERROR_SEVERITY triggers the abort/exit logic in
er_log (governed by er_Exit_ask: ER_ABORT calls abort(),
ER_EXIT_DONT_ASK calls er_final and exit(EXIT_FAILURE),
ER_EXIT_ASK prompts on stdin in non-debug builds, ER_NEVER_EXIT
does nothing). Fatal severity also unconditionally dumps the C
call stack into the log.
ER_ERROR_SEVERITY is the default — what ASSERT_ERROR() /
ASSERT_NO_ERROR() care about, what er_has_error() and
er_errid_if_has_error() count as a real error. The convenience
macro family ERROR_SET_ERROR_* produces this severity.
ER_SYNTAX_ERROR_SEVERITY is the parser’s bucket — semantically
the same as ERROR for er_has_error purposes (the helper
er_is_error_severity returns true for FATAL, ERROR, and
SYNTAX) but routed differently through the broker.
ER_WARNING_SEVERITY does not register as “an error” in
er_has_error(). The ERROR0..ERROR5 macros default to this
severity. A warning is logged (subject to
PRM_ID_ER_LOG_WARNING) but the caller’s er_errid() will
still return NO_ERROR from the perspective of the
er_has_error predicate — though er_errid() itself returns
the warning’s code.
ER_NOTIFICATION_SEVERITY is the most curious. er_set_internal
treats notifications specially: if a real error was already set,
the notification is stacked (tl_context.push_error_stack ())
so the original error is preserved, and after logging the
notification entry, the stack is popped and destroyed (pop_error_stack_and_destroy).
The net effect is that notifications never overwrite a real
error — they pass through the logging pipe and disappear.
// er_set_internal — base/error_manager.c (severity stacking, condensed)if ((severity == ER_NOTIFICATION_SEVERITY && prev_err.err_id != NO_ERROR) || (prev_err.err_id == ER_INTERRUPTED)) { tl_context.push_error_stack (); need_stack_pop = true; }// ... later ...if (severity == ER_NOTIFICATION_SEVERITY) { crt_error.clear_error (); }end: if (need_stack_pop) { tl_context.pop_error_stack_and_destroy (); }The same protection applies when the current error is
ER_INTERRUPTED — a thread that has been told “you’re being
interrupted, exit cleanly” should not lose that status to a
subsequent error from a cleanup routine. The push/pop guard
makes the cleanup error visible-then-discarded.
Set entry — er_set, er_set_with_oserror, er_set_with_file
Section titled “Set entry — er_set, er_set_with_oserror, er_set_with_file”The user-facing entry point is er_set. The full signature with
its three friends is:
// er_set family — base/error_manager.hextern void er_set (int severity, const char *file_name, const int line_no, int err_id, int num_args, ...);extern void er_set_with_file (int severity, const char *file_name, const int line_no, int err_id, FILE * fp, int num_args, ...);extern void er_set_with_oserror (int severity, const char *file_name, const int line_no, int err_id, int num_args, ...);er_set_with_oserror appends strerror(errno) to the rendered
message — used by I/O paths to mention the OS-level cause.
er_set_with_file accepts a FILE * whose contents are spliced
into the log alongside the error — used by the loader and the
parser to dump the offending input file. All three converge on
er_set_internal. The ARG_FILE_LINE macro
(#define ARG_FILE_LINE __FILE__, __LINE__) is how callers fill
the file/line arguments without typing them by hand:
// convenience macros — base/error_manager.h#define ERROR0(error, code) \ do { error = code; \ er_set (ER_WARNING_SEVERITY, ARG_FILE_LINE, code, 0); } while (0)#define ERROR1(error, code, arg1) \ do { error = code; \ er_set (ER_WARNING_SEVERITY, ARG_FILE_LINE, code, 1, arg1); } while (0)// ... condensed ...#define ERROR_SET_ERROR(error, code) \ do { (error) = (code); \ er_set (ER_ERROR_SEVERITY, ARG_FILE_LINE, (code), 0); } while (0)#define ERROR_SET_ERROR_1ARG(error, code, arg1) \ do { (error) = (code); \ er_set (ER_ERROR_SEVERITY, ARG_FILE_LINE, (code), 1, (arg1)); } while (0)Note that ERROR0..ERROR5 raise warnings by default — a small
trap. New code should prefer ERROR_SET_ERROR_* when an error is
intended.
The er_set_internal flow has nine steps. They are worth following
in order because the error-flow FSM only makes sense in light of
this body:
- Refuse if uninitialised.
er_Hasalready_initiatedmust be true. If not, seter_Errid_not_initialized = err_id, assert, returnER_FAILED. The HAapplylogdb/copylogdbworkaround underCS_MODElets uninit slide to silently drop the call. - Capture errno. If the caller is
er_set_with_oserror, snapshotstrerror(errno)before anything else might clobbererrno. - Decide whether to stack. If the new severity is
NOTIFICATION and a real error already lives in the current
level, or if the current error is
ER_INTERRUPTED, push a new stack frame so the original is preserved. - Initialise the new error level.
crt_error.set_error(err_id, severity, file, line)stamps the new error’s identifying tuple. Any priormsg_areais not freed yet — it will be reused if its buffer is large enough. - Look up the format.
er_find_fmt(err_id, num_args)returns the cachedER_FMT(or builds one on first use from the message catalog). The format describes the printf conversion specs in the localised message string. - Estimate the size.
er_estimate_size(fmt, ap)walks the captured spec array and theva_listto compute an upper bound on the rendered string length.MAX_INT_WIDTH(20) for integers,MAX_DOUBLE_WIDTH(32) for floats,strlenof the actual string for%s. - Reserve space.
crt_error.reserve_message_area(new_size + 1)grows themsg_areabuffer, doubling until the requested size fits. The 256-byte SBO is reused if the message is short. - Render.
er_vsprintf(&crt_error, fmt, ap)walks the format spec array and the simplified, non-positional conversion specs stored inER_FMT::specto callsprintfonce per spec into the message area. This sidestepsvsprintf’s sometimes-broken%<num>$<code>positional support on legacy platforms — seeer_study_specfor the parser. Ifos_errorwas captured,"... " + strerror(errno)is appended. - Log. If
severity ≤ PRM_ID_ER_LOG_LEVEL(and warnings are not suppressed byPRM_ID_ER_LOG_WARNING), caller_Fnlog[severity]— which iser_logfor all severities — underer_Log_file_mutex. Then optionally dump a call stack (PRM_ID_CALL_STACK_DUMP_ON_ERROR/PRM_ID_CALL_STACK_DUMP_ACTIVATION/PRM_ID_CALL_STACK_DUMP_DEACTIVATIONlists), notify the event handler if the error appears inPRM_ID_EVENT_ACTIVATION, and ifer_Print_to_consoleis on, also write the message to stderr.
Step 9 is followed by the PRM_ID_ER_STOP_ON_ERROR debug knob —
when set to a specific error code, the engine prompts on stdin
and may exit on that code. The notification stack-pop is the
final action.
stateDiagram-v2
[*] --> NoError : er_clear / context init
NoError --> Error : er_set (ERROR/SYNTAX/FATAL)
NoError --> Warning : er_set (WARNING)
NoError --> Notification : er_set (NOTIFICATION)
Error --> NotifPushed : er_set (NOTIFICATION)\npush_error_stack
NotifPushed --> Error : pop_error_stack_and_destroy
Error --> Stacked : er_stack_push
Stacked --> Error : er_stack_pop
Stacked --> NewError : er_set (ERROR) on top
NewError --> Stacked : er_stack_pop_and_keep_error\n(if top err_id != NO_ERROR)
Error --> NoError : er_clear / er_clearid
Warning --> NoError : er_clear
Notification --> NoError : implicit clear after log
Error --> ProcessExit : ER_FATAL_ERROR_SEVERITY\n· ER_EXIT_DONT_ASK
Per-thread context — cuberr::context
Section titled “Per-thread context — cuberr::context”cuberr::context lives in error_context.cpp / .hpp. The
thread-local pointer that connects an arbitrary call site to the
context is a single C++11 thread_local:
// thread_local pointer — base/error_context.cppnamespace cuberr{ thread_local context *tl_Context_p = NULL;}context registration is explicit. There are two patterns:
- Automatic.
context (true, false)constructs a context that registers itself astl_Context_pon construction and deregisters on destruction. Used in clienter_initfor the singleton, and in tests where a context is scoped to a function. - Manual.
context (false, false)constructs the context without registering; the caller callsregister_thread_local()/deregister_thread_local()explicitly. Used bycubthread::entrybecause the entry decides when to register its embeddedm_errormember as the current thread’s context (typically when the entry is claimed by a worker, before any task code runs).
// context::get_thread_local_context — base/error_context.cppcuberr::context &context::get_thread_local_context (void){ if (tl_Context_p == NULL) { assert (false); static context emergency_context (false, false);#if defined (SERVER_MODE) if (cubthread::get_manager () != NULL) { return cubthread::get_entry ().get_error_context (); }#endif // SERVER_MODE return emergency_context; } return *tl_Context_p;}The fallback chain is: assert (we should never get here in
production); if in SERVER_MODE and the manager is up, look up
the calling thread’s cubthread::entry and return the embedded
context; otherwise fall back to a static emergency context.
The static emergency context is a safety net of last resort —
it is shared across all threads that hit it, so concurrent
writes are racy. Hitting the emergency context indicates a bug
in registration, not a normal code path.
The error stack is a std::stack<er_message>:
// context fields — base/error_context.hppclass context{ er_message m_base_level; std::stack<er_message> m_stack; // ... condensed ...};get_current_error_level() returns m_stack.top() if non-empty,
else m_base_level. push_error_stack() emplaces a fresh
default-constructed er_message on top — a clean slate the next
er_set will fill in. pop_error_stack(er_message &out) swaps
the top frame into the caller-provided destination and pops
(letting the caller decide whether to keep or destroy).
pop_error_stack_and_destroy() is the common case: pop into a
local and let it go out of scope.
// push_error_stack — base/error_context.cppvoid context::push_error_stack (void){ m_stack.emplace (m_logging);}
void context::pop_error_stack (er_message &popped){ if (m_stack.empty ()) { assert (false); return; } popped.swap (m_stack.top ()); m_stack.pop ();}er_message::swap is non-trivial because of the SBO buffer: if
both messages live inside their msg_buffer, the buffers’ bytes
are exchanged; if both have heap-allocated areas, the pointers
are swapped; if one is SBO and one is heap, the SBO’s bytes are
copied and the heap pointer is moved. After the swap, the
invariant (msg_area_size == sizeof(msg_buffer)) == (msg_area == msg_buffer)
holds. The careful code keeps the SBO contiguous with the
struct after a swap, which is what std::stack’s value-semantics
require.
The four callable patterns on top of push/pop are:
er_stack_push/er_stack_pop— straight save/restore. Used when a function will set a new error and the caller wants the old one back. Symmetric.er_stack_push_if_exists/er_restore_last_error— conditional save. Push only if there is an existing error to protect; restore picks whichever is non-empty (current or pushed). Used in cleanup paths that may or may not have an error to set.er_stack_pop_and_keep_error— pop the top frame, but if it carried a real error, swap it into the now-current level. Used to bubble whichever level won.er_stack_clearall— drain the stack down to the base level, keeping whichever frame had a real error along the way.
Cross-reference: the per-thread context lives inside
cubthread::entry (m_error member); see
cubrid-thread-worker-pool.md for how entry is dispatched to
workers and daemons. When a worker claims an entry, the entry’s
get_error_context() is what tl_Context_p resolves to. When
the worker finishes a task and the entry is retired, the context
is reset (clear_all_levels) so the next worker to claim the
entry sees a fresh NO_ERROR.
Message catalog — cubrid.msg, csql.msg, utils.msg
Section titled “Message catalog — cubrid.msg, csql.msg, utils.msg”The catalog format is a verbatim copy of NetBSD’s nl_catd
binary format, renamed to avoid header collisions:
// catalog header — base/message_catalog.c#define NLS_MAGIC 0xff88ff89
struct nls_cat_hdr{ INT32 _magic; INT32 _nsets; INT32 _mem; INT32 _msg_hdr_offset; INT32 _msg_txt_offset;};
struct nls_set_hdr{ INT32 _setno; /* set number: 0 < x <= NL_SETMAX */ INT32 _nmsgs; /* number of messages in the set */ INT32 _index; /* index of first msg_hdr in msg_hdr table */};
struct nls_msg_hdr{ INT32 _msgno; /* msg number: 0 < x <= NL_MSGMAX */ INT32 _msglen; INT32 _offset;};All integers are big-endian on disk (ntohl on read). The file
is a header, then a sorted set-header table, then a sorted
message-header table, then the contiguous text region. Lookup
binary-searches the set table by set_id, then binary-searches
the message table within that set by msg_id, then returns a
pointer into the text region. The file is mmaped read-only;
no allocation happens per lookup.
Three system catalogs are loaded at startup:
// msgcat_System — base/message_catalog.cstruct msgcat_def msgcat_System[] = { {MSGCAT_CATALOG_CUBRID /* 0 */ , "cubrid.cat", NULL}, {MSGCAT_CATALOG_CSQL /* 1 */ , "csql.cat", NULL}, {MSGCAT_CATALOG_UTILS /* 2 */ , "utils.cat", NULL}};cubrid.cat is the engine’s main error and notification catalog;
csql.cat is the interactive SQL shell; utils.cat is for
utilities like cubrid backupdb. The .cat files are built by
gencat (or its CUBRID equivalent) from .msg source under
msg/<locale>/. Locales shipped with the source tree are
en_US.utf8 and ko_KR.utf8; others (Chinese, Japanese, …)
are loaded from msg/<locale>/ if present.
// msgcat_open — base/message_catalog.cMSG_CATDmsgcat_open (const char *name){ /* $CUBRID/msg/$CUBRID_MSG_LANG/'name' */ envvar_localedir_file (path, PATH_MAX, lang_get_msg_Loc_name (), name); catd = cub_catopen (path, 0); if (catd == NULL) { /* try once more as default language */ envvar_localedir_file (path, PATH_MAX, LANG_NAME_DEFAULT, name); catd = cub_catopen (path, 0); if (catd == NULL) { return NULL; } } // ... condensed ...}The locale resolution order is: $CUBRID/msg/<system_param_msg_lang>/<name>,
then $CUBRID/msg/<LANG_NAME_DEFAULT>/<name>. The system
parameter is intl_mbs_conv / intl_collation style — see
language_support.c. If neither file exists, msgcat_open returns
NULL and the caller (msgcat_init) records ER_FAILED. The
engine still boots — er_set falls back to the built-in
emergency strings when a catalog lookup misses, so the system
remains operable even with no catalog.
Within cubrid.msg, two sets matter: MSGCAT_SET_INTERNAL
(set 2) carries the strings used by error_manager.c itself
(“No error message available”, “Can’t allocate %d bytes”,
“\n*** The previous error message is the last one. ***\n\n”,
…) and MSGCAT_SET_ERROR (set 1) carries the per-error-code
format strings indexed by -err_id. The internal-set strings
are cached at init by er_init into er_Cached_msg:
// er_init message caching — base/error_manager.c (condensed)for (i = 1; i < (int) DIM (er_Cached_msg); i++) { msg = msgcat_message (MSGCAT_CATALOG_CUBRID, MSGCAT_SET_INTERNAL, i); if (msg && *msg) { tmp = (char *) malloc (std::strlen (msg) + 1); if (tmp) { strcpy (tmp, msg); er_Cached_msg[i] = tmp; } } }er_Is_cached_msg = true;If the catalog is missing or a particular code is absent, the
slot keeps the built-in default from er_Builtin_msg[]. The
defaults are the same English strings, hard-coded in
error_manager.c, that the catalog would otherwise provide —
the engine literally cannot lose its own internal messages
because they are baked into the binary.
Per-error formats (MSGCAT_SET_ERROR) are not cached at init —
they are looked up lazily the first time each error fires
through er_find_fmt:
// er_find_fmt — base/error_manager.cstatic ER_FMT *er_find_fmt (int err_id, int num_args){ ER_FMT *fmt = &er_Fmt_list[-err_id]; if (er_Fmt_msg_fail_count > 0) log_msg_cache.lock (); // er_Message_cache_mutex
if (fmt->fmt == NULL) { msg = msgcat_message (MSGCAT_CATALOG_CUBRID, MSGCAT_SET_ERROR, -err_id); if (msg == NULL || msg[0] == '\0') { msg = er_Cached_msg[ER_ER_MISSING_MSG]; } fmt = er_create_fmt_msg (fmt, err_id, msg); if (fmt != NULL && fmt->nspecs != num_args) { /* arg-count mismatch — replace with substitute msg */ er_internal_msg (fmt, err_id, ER_ER_SUBSTITUTE_MSG); } er_Fmt_msg_fail_count--; } return fmt;}er_create_fmt_msg calls er_study_fmt, which scans the format
string for % conversion specs and fills in
ER_FMT::spec[] — one entry per spec, recording the position
specifier (e.g. %2$s), the simplified format (without the
position), the field width, and the va-class ('i', 'p',
'f', 's', SPEC_CODE_LONGLONG, SPEC_CODE_SIZE_T). This
“compile once, reuse” approach is the reason er_set is cheap
on the second and subsequent firings of the same error: the
spec array is built once, kept in er_Fmt_list[-err_id], and
referenced thereafter. The arg-count check (fmt->nspecs == num_args)
is the critical safety: if a caller passes the wrong number of
arguments, the engine substitutes a generic “No message
available; original message format in error” string rather than
walking the va_list past its end.
sequenceDiagram
participant Caller as Caller code
participant ErSet as er_set
participant Ctx as cuberr::context (TLS)
participant Fmt as er_find_fmt
participant Cat as msgcat_message
participant Log as er_log
participant File as cubrid_*.err
Caller->>ErSet: er_set(ER_ERROR_SEVERITY, file, line, ER_BTREE_DUPLICATE_OID, 1, key)
ErSet->>Ctx: get_thread_local_context()
ErSet->>Ctx: get_current_error_level()
alt notification on top of error\n or current is INTERRUPTED
ErSet->>Ctx: push_error_stack()
end
ErSet->>Ctx: crt_error.set_error(code, sev, file, line)
ErSet->>Fmt: er_find_fmt(code, n_args)
alt fmt->fmt == NULL (first use of this code)
Fmt->>Cat: msgcat_message(CUBRID, SET_ERROR, -code)
Cat-->>Fmt: localised printf-format string
Fmt->>Fmt: er_create_fmt_msg → er_study_fmt
end
Fmt-->>ErSet: ER_FMT * with spec[]
ErSet->>ErSet: er_estimate_size + reserve_message_area
ErSet->>ErSet: er_vsprintf into msg_area
ErSet->>Log: er_Fnlog[severity](err_id)\n(under er_Log_file_mutex)
Log->>File: timestamped line via er_Cached_msg[ER_LOG_MSG_WRAPPER_D]
Log->>File: optional call-stack dump
alt notification path
ErSet->>Ctx: pop_error_stack_and_destroy()
end
Error log file — cubrid_*.err
Section titled “Error log file — cubrid_*.err”The error log file is opened in er_init and tied to one of two
filenames:
er_Msglog_filename— the main error log, suffix.err. The filename is eithermsglog_filenamepassed toer_init, resolved relative to$CUBRID/log/, orPRM_ID_ER_LOG_FILEfrom system parameters, or NULL (in which casestderris used).er_Accesslog_filename— the access log for client-connect events, suffix.access. Built by stripping.errfromer_Msglog_filename(or appending.accessif there is no.err). Only error codeER_BO_CLIENT_CONNECTEDis routed to this file; everything else goes to the main log.
In production mode (PRM_ID_ER_PRODUCTION_MODE = true) the file
is opened as-is; in non-production mode the pid is appended
(server.log.<pid>) so multiple server processes do not clobber
each other’s log. On Unix and SERVER_MODE, a symlink
<dirname>/<dbname>_latest<suffix> is created/refreshed
pointing at the current log — so an operator can tail -f log/server/foo_latest.err and follow the current file across
backup events.
// er_file_create_link_to_current_log_file — base/error_manager.cvoider_file_create_link_to_current_log_file (const char *log_file_path, const char *suffix){ /* $CUBRID/log/server/{db_name}_latest{suffix} */ cub_dirname_r (log_file_path, link_dir_path, PATH_MAX); snprintf (link_path, PATH_MAX, "%s%c%s_latest%s", link_dir_path, PATH_SEPARATOR, db_name, suffix); (void) unlink (link_path); symlink (log_file_path, link_path);}Rotation is size-based, not time-based. Inside er_log:
// er_log size-based rotation — base/error_manager.c (condensed)if (*log_fh != stderr && *log_fh != stdout && ftell (*log_fh) > (int) prm_get_integer_value (PRM_ID_ER_LOG_SIZE)) { fflush (*log_fh); fprintf (*log_fh, "%s", er_Cached_msg[ER_LOG_WRAPAROUND]); if (!er_Isa_null_device) { *log_fh = er_file_backup (*log_fh, log_file_name); if (*log_fh == NULL) { *log_fh = stderr; /* warn */ } else { er_file_create_link_to_current_log_file (...); } } else { /* /dev/null: just rewind to suppress repeated checks */ fseek (*log_fh, 0L, SEEK_SET); } }er_file_backup closes the current file, renames it to *.bak
(unlinking any prior .bak), and opens a fresh one. There is
one level of backup history — the newest backup overwrites the
previous one. This is a deliberate trade-off: log files do not
grow without bound, but operators who want long retention must
copy the .bak away before the next rotation. The
PRM_ID_ER_LOG_SIZE parameter (typically 4 MiB) controls the
trigger.
The format of a single log entry is ER_LOG_MSG_WRAPPER_D from
the internal-message catalog, with the built-in default:
\nTime: %s - %s *** file %s, line %d %s CODE = %d, Tran = %d%s\n%s\nSubstituting in: timestamp; severity name ("ERROR", "FATAL ERROR", …);
file_name; line_no; severity-classifier (“ERROR” or empty for
WARNING/NOTIFICATION); err_id; transaction index;
optional , CLIENT = host:prog(pid), EID = N suffix; rendered
message. EID is a per-process monotonic event identifier
(er_Eid) used to correlate errors that may have been raised
across both client and server logs. After every line, the
engine writes ER_LOG_LAST_MSG ("\n*** The previous error message is the last one. ***\n\n"), flushes, and
fseek(-wsz, SEEK_CUR)s back over it — so the file always ends
in the LAST_MSG marker, but the marker is overwritten by the
next entry. A reader at any moment sees a complete tail.
Wire propagation — er_get_area_error / er_set_area_error
Section titled “Wire propagation — er_get_area_error / er_set_area_error”When a server-side er_set happens, the error needs to make it
back to the client. The packing function flattens the current
thread-local error to three packed integers plus the rendered
message:
// er_get_area_error — base/error_manager.cchar *er_get_area_error (char *buffer, int *length){ er_message &crt_error = context::get_thread_local_error ();
msg = strlen (crt_error.msg_area) != 0 ? crt_error.msg_area : "(null)"; len = (OR_INT_SIZE * 3) + strlen (msg) + 1; len = MIN (len, *length); *length = (int) len; max_msglen = len - (OR_INT_SIZE * 3) - 1;
ptr = buffer; ASSERT_ALIGN (ptr, INT_ALIGNMENT);
OR_PUT_INT (ptr, (int) crt_error.err_id); ptr += OR_INT_SIZE; OR_PUT_INT (ptr, (int) crt_error.severity); ptr += OR_INT_SIZE; OR_PUT_INT (ptr, len); ptr += OR_INT_SIZE;
strncpy (ptr, msg, max_msglen); *(ptr + max_msglen) = '\0'; return buffer;}The wire layout is therefore [errid:int32][severity:int32][length:int32][msg:bytes][\0],
all integers in network byte order via OR_PUT_INT. Note that
the file_name and line_no of the original er_set site are
not shipped — those are server-internal. The client sees the
error code, the severity, and the localised message; that is
enough to recreate a meaningful er_set on the client side.
The unpacking side runs in CS_MODE:
// er_set_area_error — base/error_manager.cinter_set_area_error (char *server_area){ if (server_area == NULL) { er_clear (); return NO_ERROR; }
er_message &crt_error = context::get_thread_local_error ();
ptr = server_area; err_id = OR_GET_INT (ptr); ptr += OR_INT_SIZE; severity = OR_GET_INT (ptr); ptr += OR_INT_SIZE; length = OR_GET_INT (ptr); ptr += OR_INT_SIZE;
crt_error.err_id = ((err_id >= 0 || err_id <= ER_LAST_ERROR) ? -1 : err_id); crt_error.severity = severity; crt_error.file_name = "Unknown from server"; crt_error.line_no = -1;
length = strlen (ptr) + 1; crt_error.reserve_message_area (length); memcpy (crt_error.msg_area, ptr, length);
/* fire the local logging path so the client log records it too */ if (severity <= prm_get_integer_value (PRM_ID_ER_LOG_LEVEL) ...) { std::unique_lock<std::mutex> log_file_lock (er_Log_file_mutex); (*er_Fnlog[severity]) (err_id); // ... } return crt_error.err_id;}The client overwrites its current thread-local error with the
server-supplied tuple, sets the file/line to the placeholder
"Unknown from server" / -1, copies the message bytes
verbatim, and runs the local logging pipe so the client process
also records the event in its own cubrid_*.err. The function
returns crt_error.err_id so the network layer can simply
return er_set_area_error(buf) and propagate the code as a
function-return value.
The complementary helper er_get_ermsg_from_area_error(buffer)
is a one-line return buffer + (OR_INT_SIZE * 3); — it gives the
network-stat layer access to the message text inside an
already-packed area without re-flattening.
The bracketing happens in the network-interface layer
(network_interface_sr.c / network_interface_cl.c) — see
cubrid-network-protocol.md for how request/response packets
are framed. The relevant pattern is: every server-side request
handler returns either success (NO_ERROR) or, on error, packs
both the result and er_get_area_error(...) into the response
buffer; the client’s net_client_request_* family inspects the
response for the error area and calls er_set_area_error if
one is present, then returns the unpacked code as the function
result.
sequenceDiagram
participant App as Client app
participant NetCl as network_interface_cl
participant NetSr as network_interface_sr
participant Engine as Server engine path
participant SrvCtx as server thread context
participant CliCtx as client thread context
App->>NetCl: db_query_execute(...)
NetCl->>NetSr: TCP request packet
NetSr->>Engine: dispatch handler
Engine->>Engine: btree_find / lock_object / ...
Engine->>SrvCtx: er_set(ER_BTREE_DUPLICATE_OID, ...)
SrvCtx->>SrvCtx: writes crt_error in cubthread::entry's m_error
Engine-->>NetSr: return ER_FAILED
NetSr->>SrvCtx: er_get_area_error(buf, &len)
SrvCtx-->>NetSr: [errid|sev|len|msg|\0]
NetSr-->>NetCl: TCP response packet (incl. error area)
NetCl->>CliCtx: er_set_area_error(server_area)
CliCtx->>CliCtx: overwrite crt_error, log to client *.err
CliCtx-->>NetCl: returns errid
NetCl-->>App: returns errid (e.g. ER_BTREE_DUPLICATE_OID)
App->>App: db_error_code() == ER_BTREE_DUPLICATE_OID
Convenience macros — assertions and friend constants
Section titled “Convenience macros — assertions and friend constants”error_manager.h exposes a set of debugging assertions that the
rest of the engine sprinkles liberally:
// assertion macros — base/error_manager.h#define ASSERT_ERROR() \ assert (er_errid () != NO_ERROR)
#define ASSERT_ERROR_AND_SET(error_code) \ do { \ error_code = er_errid (); \ if (error_code == NO_ERROR) \ { \ assert (false); \ error_code = ER_FAILED; \ } \ } while (false)
#define ASSERT_NO_ERROR() \ assert (er_errid () == NO_ERROR);
#define ASSERT_NO_ERROR_OR_INTERRUPTED() \ assert (er_errid () == NO_ERROR || er_errid () == ER_INTERRUPTED);The semantic contract these macros encode is the invariant the
engine maintains across error returns: a function that returns
ER_FAILED must have set an error via er_set. A function that
returns NO_ERROR must not have set one (or must have cleared
any it accidentally set). ASSERT_ERROR_AND_SET is the
defensive variant — if the caller failed but the error context
is empty, fall back to ER_FAILED so the function still has a
sensible non-zero return.
The assert_release_* family is interesting because it has two
modes:
// release-mode asserts — base/error_manager.h#if defined(NDEBUG)#define STRINGIZE(s) #s#define assert_release(e) \ ((e) ? (void) 0 : er_set (ER_NOTIFICATION_SEVERITY, ARG_FILE_LINE, \ ER_FAILED_ASSERTION, 1, STRINGIZE (e)))#define assert_release_notify(e) assert_release(e)#define assert_release_error(e) \ ((e) ? (void) 0 : er_set (ER_ERROR_SEVERITY, ARG_FILE_LINE, \ ER_FAILED_ASSERTION, 1, STRINGIZE (e)))#else#define assert_release(e) assert(e)#define assert_release_notify(e) assert_release(e)#define assert_release_error(e) assert(e)#endifIn debug builds, these are plain asserts and abort the
process. In release builds, they raise an ER_FAILED_ASSERTION
notification (or error) carrying the stringified expression and
let the engine continue. The intent is that an invariant
violation in the field should be loud and recorded but not
necessarily fatal — operators get the log entry, the engine
keeps serving the next query. assert_release defaults to
notification severity, assert_release_error to error
severity; the choice depends on whether the violated invariant
threatens correctness or just expectations.
Stack trace / call-stack capture
Section titled “Stack trace / call-stack capture”The engine can emit C-level call stacks when an error is
raised. The implementation is in stack_dump.c (not in the
listed sources but invoked by error_manager.c via
er_dump_call_stack). On Linux, it uses backtrace /
backtrace_symbols plus a per-process mht_t table
(fname_table) of resolved source filenames so the format is
function (file:line) rather than the raw (0xADDR) form.
The hash table is initialised by er_call_stack_init at
er_init time and freed by er_call_stack_final at
er_final.
Three knobs control when the stack is dumped:
PRM_ID_CALL_STACK_DUMP_ON_ERROR— global on/off.PRM_ID_CALL_STACK_DUMP_ACTIVATION— list of error codes that opt into dumping when the global is off.PRM_ID_CALL_STACK_DUMP_DEACTIVATION— list of error codes that opt out of dumping when the global is on.
er_call_stack_dump_on_error reads the three:
// er_call_stack_dump_on_error — base/error_manager.cstatic voider_call_stack_dump_on_error (int severity, int err_id){ if (severity == ER_FATAL_ERROR_SEVERITY) { er_dump_call_stack (er_Msglog_fh); } else if (prm_get_bool_value (PRM_ID_CALL_STACK_DUMP_ON_ERROR)) { if (!sysprm_find_err_in_integer_list (PRM_ID_CALL_STACK_DUMP_DEACTIVATION, err_id)) er_dump_call_stack (er_Msglog_fh); } else { if (sysprm_find_err_in_integer_list (PRM_ID_CALL_STACK_DUMP_ACTIVATION, err_id)) er_dump_call_stack (er_Msglog_fh); }}Fatal severity always dumps unconditionally. Otherwise the
default is off; operators flip the global on or whitelist
specific codes. The er_print_callstack user-callable variant
does the same with a printf-style header.
There is also a crash handler — er_print_crash_callstack(int sig)
— installed for SIGABRT / SIGILL / SIGFPE / SIGBUS /
SIGSEGV / SIGSYS. On Linux it reads /proc/self/cmdline,
chdir’s into $CUBRID/log/coredump/, opens
<cmdline>_<YYYYMMDDHHMMSS>.<ms>.coredump, writes process
information and a backtrace, then returns. The intent is to
capture a minimal artifact even when the OS-level core dump
is suppressed (e.g. by ulimit).
Event handler
Section titled “Event handler”The final niche is the event handler — an external program
that the engine pipes select error events to. Configured by the
PRM_ID_EVENT_HANDLER system parameter (a shell command),
filtered by PRM_ID_EVENT_ACTIVATION (the list of error codes
that fire it). er_event_init popens the command, writes a
startup banner, and stores the FILE * in er_Event_pipe. On
each error matching PRM_ID_EVENT_ACTIVATION, er_notify_event_on_error
calls er_event which writes one line <err_id> <severity_string> <message>\n
to the pipe.
SIGPIPE is tricky because the external program may close the
pipe; the engine wraps each write in a setjmp/longjmp-guarded
section with er_event_sigpipe_handler so a closed pipe turns
into a non-fatal jump back, the pipe is closed and reopened,
and the engine continues. This is the only setjmp in
error_manager.c.
flowchart LR
subgraph engine["any subsystem (server thread)"]
A[btree / lock / heap / ...] --> B[er_set / er_set_with_oserror]
end
B --> C[cuberr::context::get_current_error_level]
C --> D[er_find_fmt → msgcat lookup]
D -->|first time| E[msgcat_message: cubrid.cat / set 1]
D --> F[er_estimate_size + reserve_message_area]
F --> G[er_vsprintf: render into msg_area]
G --> H{severity ≤ PRM_ER_LOG_LEVEL?}
H -- yes --> I[er_log: timestamped line\nin cubrid_*.err\nor stderr]
I --> J{call-stack dump?}
J -- yes --> K[er_dump_call_stack]
J -- no --> L
K --> L
L[er_notify_event_on_error] -->|matches PRM_EVENT_ACTIVATION| M[fprintf to er_Event_pipe]
L --> N{severity == FATAL?}
N -- yes --> O[exit_ask: ABORT / EXIT / ASK / NEVER]
N -- no --> P[return to caller; thread-local error remains set]
H -- no --> P
P --> Q[caller returns ER_FAILED]
Q --> R[network_interface_sr packs er_get_area_error → response]
R --> S[client decodes via er_set_area_error]
S --> T[client app reads db_error_code / db_error_string]
Source Walkthrough
Section titled “Source Walkthrough”This section lists the symbols by call-flow grouping. Each entry names the symbol; the prose-side narrative above explains the intent. Line numbers are deferred to the position-hint table.
Initialisation and shutdown — error_manager.c
Section titled “Initialisation and shutdown — error_manager.c”er_init (msglog_filename, exit_ask)— main entry point. Initialiseser_Fmt_list[], primeser_Cached_msg[]from the catalog, opens the log file, registers the singleton context (CS_MODE only), installs the event handler.er_is_initialized ()— reportser_Hasalready_initiated.er_set_print_property (print_console)— flipser_Print_to_console.er_final (do_global_final)— tears down: closes log files, frees cached messages, deinits format list, callser_event_finalander_call_stack_final.er_clear ()— clears the current error level for the calling thread.er_clearid ()/er_setid (err_id)— manipulate just the error id without touching the message or severity.
er_set family — the public set API
Section titled “er_set family — the public set API”er_set (severity, file, line, err_id, num_args, ...)— the standard entry.er_set_with_file (..., FILE *fp, ...)— same plus splice the file’s contents into the log alongside the message.er_set_with_oserror (...)— same plus appendstrerror(errno).er_set_internal (severity, file, line, err_id, num_args, include_os_error, fp, ap_ptr)— common implementation.
Format compilation and rendering
Section titled “Format compilation and rendering”er_find_fmt (err_id, num_args)— lazy lookup iner_Fmt_list[-err_id]; on first miss, callsmsgcat_messageander_create_fmt_msg.er_create_fmt_msg (fmt, err_id, msg)— copies the format string and runser_study_fmt.er_study_fmt (fmt)— scans the format for%specs, fills infmt->spec[].er_study_spec (conversion_spec, simple_spec, position, width, va_class)— parses one%-spec, returns the characters consumed and the position/width/va_class.er_estimate_size (fmt, ap)— upper-bounds the rendered message length given the va_list.er_vsprintf (er_entry_p, fmt, ap)— captures the args intoer_va_arg[], then walks the format callingsprintfper spec intomsg_area.er_init_fmt (fmt)/er_clear_fmt (fmt)/er_internal_msg (fmt, code, msg_num)— per-fmt initialisation, teardown, and substitute-msg replacement.er_emergency (file, line, fmt, ...)— minimal sprintf for the case whereer_mallocitself fails; understands only%s/%dand writes directly into the existingmsg_buffer.er_malloc_helper (size, file, line)— wrapsmallocwither_emergencyon failure.
Per-thread context — error_context.cpp
Section titled “Per-thread context — error_context.cpp”er_message::er_message (logging)— constructor.er_message::~er_message ()— callsclear_message_areaandclear_args.er_message::swap (other)— non-trivial swap that preserves the SBO-vs-heap invariant.er_message::clear_error ()/set_error (id, sev, file, line)— small accessors.er_message::reserve_message_area (size)— doubling growth.er_message::clear_message_area ()/er_message::clear_args ()— release heap buffers.context::context (auto_register, logging)— constructor; optionally registers as TLS.context::~context ()— deregisters and clears.context::get_current_error_level ()— returnsm_stack.top()orm_base_level.context::register_thread_local ()/deregister_thread_local ()— set/resettl_Context_p.context::clear_current_error_level ()/clear_all_levels ()/clear_stack ()— bulk clear helpers.context::push_error_stack ()/pop_error_stack (popped)/pop_error_stack_and_destroy ()/has_error_stack ()— stack manipulation.context::get_thread_local_context ()/get_thread_local_error ()— TLS accessors with thecubthread::entryfallback inSERVER_MODE.
Stack save/restore (callable wrappers)
Section titled “Stack save/restore (callable wrappers)”er_stack_push ()— push current onto stack (always).er_stack_push_if_exists ()— push only if there is something to protect.er_stack_pop ()— discard top, restore previous.er_stack_pop_and_keep_error ()— pop top, but if it had a real error swap it into the now-current level.er_restore_last_error ()— symmetric companion toer_stack_push_if_exists.er_stack_clearall ()— drain stack, keep last real error.
Logging
Section titled “Logging”er_log (err_id)— theer_Fnlog[*]callback. Selects log file (msg vs. access), checks/rotates size, formats withER_LOG_MSG_WRAPPER_D, writes, optionally prompts on FATAL.er_call_stack_dump_on_error (severity, err_id)— consults the three knobs and dumps the stack.er_print_callstack (file, line, fmt, ...)— user-callable header + stack dump.er_print_crash_callstack (sig)— signal-handler variant; writes to$CUBRID/log/coredump/.er_call_stack_init ()/er_call_stack_final ()/er_fname_free (key, data, args)— fname hash-table lifecycle.
Log file plumbing
Section titled “Log file plumbing”er_get_msglog_filename ()— getter.er_set_access_log_filename ()— derives access-log name from msglog name.er_file_open (path)—fopenwithmkdir -p-style parent creation and rotation if size exceeded.er_file_isa_null_device (path)— recognise/dev/null/NUL.er_file_backup (fp, path)— close, rename to.bak, reopen.er_file_create_link_to_current_log_file (path, suffix)— Unix_latestsymlink.
Wire protocol
Section titled “Wire protocol”er_get_area_error (buffer, length)— flatten current error to packed buffer.er_set_area_error (server_area)— unflatten and inject into client thread.er_get_ermsg_from_area_error (buffer)— accessor for the message text inside an already-packed buffer.er_all (err_id, severity, n_levels, line_no, file_name, msg)— multi-out copy of the current error.er_msg ()/er_errid ()/er_errid_if_has_error ()/er_get_severity ()/er_has_error ()— read accessors.er_is_error_severity (severity)— true for FATAL / ERROR / SYNTAX, false for WARNING / NOTIFICATION.
Event handler
Section titled “Event handler”er_event_init ()—popenthe configured command, write startup banner.er_event ()— write one line to the pipe undersetjmp-guarded SIGPIPE handler.er_event_sigpipe_handler (sig)—_longjmps back.er_event_final ()— write shutdown banner,pclose.er_notify_event_on_error (err_id)— gate onPRM_ID_EVENT_ACTIVATION.er_register_log_handler (handler)— register a CS-side callback fired per-error with the EID.er_event_restart ()— public wrapper arounder_event_initfor runtime reconfigure.
Debug output
Section titled “Debug output”_er_log_debug (file, line, fmt, ...)— public macro wrapper; writes a timestamped DEBUG line._er_log_debug_internal (file, line, fmt, ap)— body shared wither_print_callstack.er_log_debug (...)macro — gated byPRM_ID_ER_LOG_DEBUG.
Auxiliary types
Section titled “Auxiliary types”er_severityenum — five levels.er_exit_askenum —ER_NEVER_EXIT,ER_EXIT_ASK,ER_EXIT_DONT_ASK,ER_ABORT.er_print_optionenum —ER_DO_NOT_PRINT,ER_PRINT_TO_CONSOLE.er_final_codeenum —ER_THREAD_FINAL,ER_ALL_FINAL.ER_COPY_AREA(file-static) — legacy flat-area struct used by older client/server paths.ER_FMT/ER_SPEC(file-static) — compiled-format cache entries, one per error code iner_Fmt_list[].er_messagestruct (error_context.hpp) — per-level error state.cuberr::context— per-thread error container.cuberr::manager— RAII wrapper arounder_init/er_final(used viaER_SAFE_INIT).
Catalog — message_catalog.c
Section titled “Catalog — message_catalog.c”msgcat_init ()— opens the three system catalogs.msgcat_final ()— closes them.msgcat_message (cat_id, set_id, msg_id)— the public lookup; returns localised string or empty string on miss.msgcat_open (name)— locale-resolved open, with fallback toLANG_NAME_DEFAULT.msgcat_close (msg_catd)— close one.msgcat_get_descriptor (cat_id)— get cached descriptor.msgcat_gets (msg_catd, set_id, msg_id, default)— looks up one message with a default fallback.msgcat_open_file (name)— open an arbitrary file in the message directory (used for non-error message files).cub_catopen (name, type)— internal NLSPATH-aware open, mimickingcatopen(3).cub_catgets (catd, set_id, msg_id, default)— internal binary-search lookup.cub_catclose (catd)—munmapand free.load_msgcat (path)—mmapthe file, validate theNLS_MAGICheader, return the descriptor.nls_cat_hdr/nls_set_hdr/nls_msg_hdr— on-disk binary structs (big-endian)._nl_cat_d(typedef’dcub_nl_catd) — in-memory catalog descriptor.msgcat_def— entry inmsgcat_System[]table mapping cat_id to filename.
Error-code namespace — error_code.h
Section titled “Error-code namespace — error_code.h”NO_ERROR(0) — success constant.ER_FAILED(-1) — generic failure.ER_GENERIC_ERROR/ER_OUT_OF_VIRTUAL_MEMORY/ER_INTERRUPTED(-2..-4) — top-level errors.ER_LK_*— lock manager (timeouts, deadlocks, unilateral aborts).ER_LOG_*— recovery / WAL.ER_BO_*— boot.ER_BTREE_*— B-tree.ER_PB_*— page buffer.ER_HEAP_*— heap files.ER_DISK_*— disk manager.ER_FILE_*— file system layer.ER_NET_*— network / connection.ER_AU_*— authorisation.ER_SM_*— schema manager.ER_QPROC_*— query processing.ER_TR_*— triggers.ER_TM_*— transaction manager.ER_LDR_*— bulk loader.ER_MR_*— match-rule (legacy).ER_FK_*— foreign keys.ER_TP_*— type coercion.ER_LC_*— locator.ER_IO_*— raw I/O.ER_CT_*— catalog manager.ER_OBJ_*— object manager.ER_REGU_*— regu/expression.ER_REG_*— regex.ER_FAILED_ASSERTION— released-assert support.ER_LAST_ERROR(-1371) — sentinel; sizing theer_Fmt_list[]array.
Position hints (as of updated: 2026-05-01)
Section titled “Position hints (as of updated: 2026-05-01)”| Symbol | File | Line |
|---|---|---|
NO_ERROR | src/base/error_code.h | 49 |
ER_FAILED | src/base/error_code.h | 50 |
ER_GENERIC_ERROR | src/base/error_code.h | 52 |
ER_INTERRUPTED | src/base/error_code.h | 54 |
ER_LK_UNILATERALLY_ABORTED | src/base/error_code.h | 133 |
ER_FAILED_ASSERTION | src/base/error_code.h | 698 |
ER_LAST_ERROR | src/base/error_code.h | 1760 |
ARG_FILE_LINE | src/base/error_manager.h | 44 |
ERROR0 macro | src/base/error_manager.h | 49 |
ERROR_SET_ERROR macro | src/base/error_manager.h | 137 |
assert_release macro | src/base/error_manager.h | 189 |
er_severity enum | src/base/error_manager.h | 216 |
ER_IS_LOCK_TIMEOUT_ERROR | src/base/error_manager.h | 237 |
ER_IS_ABORTED_DUE_TO_DEADLOCK | src/base/error_manager.h | 246 |
ER_IS_SERVER_DOWN_ERROR | src/base/error_manager.h | 250 |
ASSERT_ERROR | src/base/error_manager.h | 258 |
ASSERT_ERROR_AND_SET | src/base/error_manager.h | 263 |
er_init decl | src/base/error_manager.h | 287 |
er_set decl | src/base/error_manager.h | 292 |
er_set_with_oserror decl | src/base/error_manager.h | 295 |
er_errid decl | src/base/error_manager.h | 304 |
er_msg decl | src/base/error_manager.h | 307 |
er_get_area_error decl | src/base/error_manager.h | 314 |
er_set_area_error decl | src/base/error_manager.h | 315 |
er_stack_push decl | src/base/error_manager.h | 316 |
er_stack_pop decl | src/base/error_manager.h | 318 |
cuberr::manager class | src/base/error_manager.h | 354 |
ER_SAFE_INIT macro | src/base/error_manager.h | 366 |
cuberr::ER_EMERGENCY_BUF_SIZE | src/base/error_context.hpp | 32 |
cuberr::er_va_arg union | src/base/error_context.hpp | 35 |
cuberr::er_message struct | src/base/error_context.hpp | 45 |
cuberr::context class | src/base/error_context.hpp | 76 |
tl_Context_p (thread_local) | src/base/error_context.cpp | 49 |
er_message::swap | src/base/error_context.cpp | 73 |
er_message::set_error | src/base/error_context.cpp | 151 |
er_message::reserve_message_area | src/base/error_context.cpp | 187 |
context::push_error_stack | src/base/error_context.cpp | 278 |
context::pop_error_stack | src/base/error_context.cpp | 284 |
context::pop_error_stack_and_destroy | src/base/error_context.cpp | 296 |
context::get_thread_local_context | src/base/error_context.cpp | 332 |
context::get_thread_local_error | src/base/error_context.cpp | 350 |
er_severity_string[] | src/base/error_manager.c | 152 |
ER_MSG_SET / ER_INTERNAL_MSG_SET | src/base/error_manager.c | 170 |
enum er_msg_no | src/base/error_manager.c | 192 |
er_Builtin_msg[] | src/base/error_manager.c | 221 |
ER_MSG_LOG_FILE_SUFFIX | src/base/error_manager.c | 287 |
er_Fmt_list[] | src/base/error_manager.c | 299 |
er_Fnlog[] | src/base/error_manager.c | 374 |
er_init | src/base/error_manager.c | 679 |
er_file_open | src/base/error_manager.c | 999 |
er_file_backup | src/base/error_manager.c | 1065 |
er_file_create_link_to_current_log_file | src/base/error_manager.c | 1091 |
er_final | src/base/error_manager.c | 1127 |
er_clear | src/base/error_manager.c | 1202 |
er_set | src/base/error_manager.c | 1229 |
er_set_with_file | src/base/error_manager.c | 1257 |
er_set_with_oserror | src/base/error_manager.c | 1286 |
er_call_stack_dump_on_error | src/base/error_manager.c | 1347 |
er_set_internal | src/base/error_manager.c | 1385 |
er_log | src/base/error_manager.c | 1595 |
er_register_log_handler | src/base/error_manager.c | 1801 |
er_errid | src/base/error_manager.c | 1826 |
er_errid_if_has_error | src/base/error_manager.c | 1853 |
er_clearid | src/base/error_manager.c | 1866 |
er_setid | src/base/error_manager.c | 1887 |
er_get_severity | src/base/error_manager.c | 1907 |
er_has_error | src/base/error_manager.c | 1918 |
er_msg | src/base/error_manager.c | 1932 |
er_all | src/base/error_manager.c | 1974 |
_er_log_debug | src/base/error_manager.c | 1998 |
er_get_ermsg_from_area_error | src/base/error_manager.c | 2085 |
er_get_area_error | src/base/error_manager.c | 2100 |
er_set_area_error | src/base/error_manager.c | 2147 |
er_stack_push | src/base/error_manager.c | 2226 |
er_stack_push_if_exists | src/base/error_manager.c | 2239 |
er_stack_pop | src/base/error_manager.c | 2265 |
er_stack_pop_and_keep_error | src/base/error_manager.c | 2276 |
er_restore_last_error | src/base/error_manager.c | 2314 |
er_stack_clearall | src/base/error_manager.c | 2334 |
er_study_spec | src/base/error_manager.c | 2364 |
er_study_fmt | src/base/error_manager.c | 2545 |
er_estimate_size | src/base/error_manager.c | 2644 |
er_find_fmt | src/base/error_manager.c | 2737 |
er_create_fmt_msg | src/base/error_manager.c | 2800 |
er_init_fmt | src/base/error_manager.c | 2833 |
er_clear_fmt | src/base/error_manager.c | 2850 |
er_internal_msg | src/base/error_manager.c | 2877 |
er_malloc_helper | src/base/error_manager.c | 2895 |
er_emergency | src/base/error_manager.c | 2921 |
er_vsprintf | src/base/error_manager.c | 3016 |
er_is_error_severity | src/base/error_manager.c | 3233 |
cuberr::manager::manager | src/base/error_manager.c | 3262 |
er_print_crash_callstack | src/base/error_manager.c | 3278 |
nls_cat_hdr struct | src/base/message_catalog.c | 81 |
nls_set_hdr struct | src/base/message_catalog.c | 90 |
nls_msg_hdr struct | src/base/message_catalog.c | 97 |
cub_catopen | src/base/message_catalog.c | 131 |
cub_catgets | src/base/message_catalog.c | 292 |
cub_catclose | src/base/message_catalog.c | 366 |
load_msgcat | src/base/message_catalog.c | 389 |
msgcat_System[] | src/base/message_catalog.c | 487 |
msgcat_init | src/base/message_catalog.c | 500 |
msgcat_final | src/base/message_catalog.c | 526 |
msgcat_message | src/base/message_catalog.c | 557 |
msgcat_open | src/base/message_catalog.c | 599 |
msgcat_get_descriptor | src/base/message_catalog.c | 641 |
msgcat_gets | src/base/message_catalog.c | 658 |
msgcat_close | src/base/message_catalog.c | 674 |
msgcat_open_file | src/base/message_catalog.c | 695 |
Cross-check Notes
Section titled “Cross-check Notes”The structural picture in this document — er_set writes through
the thread-local cuberr::context into either the base
er_message or the top of a std::stack<er_message>, the format
is compiled lazily into er_Fmt_list[-err_id] from the
NetBSD/FreeBSD-format cubrid.cat catalog mounted via mmap,
the wire format is three packed OR_INTs plus a NUL-terminated
message — matches the source as of the listed file revisions.
Three points of friction with informal narratives are worth
explicit cross-check.
The “error stack is for nested errors” framing. A reading
that calls er_stack_push / er_stack_pop “the way you’d push
an exception frame” overstates the analogy. There is no
unwinding — these are manual save-restore operations. A
function that calls er_stack_push must call a corresponding
er_stack_pop family member or it leaks the frame; the
destructor’s assert (m_stack.empty ()) in
deregister_thread_local is the only safety net, and it is
guarded by defined (SERVER_MODE). In CS_MODE the safeguard
is silently disabled because the comment notes “this is too
late anyway”.
Notification severity is automatic. er_set_internal
push-pops the stack itself when severity == ER_NOTIFICATION_SEVERITY
and a real error already occupies the current level. Callers
of er_set with notification severity therefore do not need
to paired-call er_stack_push/pop; the engine does it for
them. New code should not duplicate the push/pop around such
calls.
er_set with a wrong num_args is detected, not silently
trusted. er_find_fmt compares fmt->nspecs against the
caller’s num_args and substitutes the format with
ER_ER_SUBSTITUTE_MSG on mismatch. The rendered message will
read “No message available; original message format in error”,
which is a hint to a reader that the caller passed wrong
data, not that the catalog is missing. Confusing the two
diagnostic strings (ER_ER_MISSING_MSG vs
ER_ER_SUBSTITUTE_MSG) is a frequent source of bug-report
noise.
The error log file size is approximate. er_log reads
ftell(*log_fh) > PRM_ID_ER_LOG_SIZE after writing the
current entry, then rotates if the threshold is exceeded. So the
file may grow somewhat beyond the configured size before
rotation. This is intentional — the threshold is a signal not
a limit, and the in-progress entry is preserved in full. A
strict size limit would require pre-emptive rotation that could
truncate a long stack-dump entry.
er_set_area_error runs the local logging pipe. A common
misreading is “the server already logged this error, so the
client decoding is silent”. In fact the client also calls
er_log via er_Fnlog[severity] on each unpacked area, so the
same logical error appears in both cubrid_*.err files — the
server’s with the original (file, line) site, the client’s
with (Unknown from server, -1). Operators looking for an
error need only check the side they have access to.
er_emergency does not allocate. The function is invoked
when er_malloc itself failed. It writes directly into the
already-allocated msg_buffer (the 256-byte SBO), understands
only %s and %d, and calls er_log to dump a final entry.
It is the engine’s last-gasp pathway and should not be called
explicitly from non-error-manager code.
Open Questions
Section titled “Open Questions”A handful of details are not pinned down by the listed source files alone and would benefit from cross-referencing other modules.
How does cubthread::entry pick up its m_error context on
worker dispatch? The fallback in context::get_thread_local_context
goes through cubthread::get_entry().get_error_context() when
tl_Context_p is NULL. Whether the entry registers itself as
TLS at task entry or relies entirely on this fallback is a
question for the worker-pool source — see
cubrid-thread-worker-pool.md for cubthread::entry::register_thread_local.
Are there any error codes that are raised only on the client
or only on the server? The dbi_compat.h mirror suggests
all codes are visible to both. But codes like ER_NET_* and
ER_OBJ_NO_CONNECT only make sense on the client; codes like
ER_LK_UNILATERALLY_ABORTED and ER_PB_* only make sense on
the server. The wire path packs whatever the server sets and
the client unpacks it without code-validity checks (it does
range-check that the value is in [ER_LAST_ERROR, ER_FAILED]).
A misbehaving server could in principle ship a client-only code
to the client; how this is handled is unclear.
What happens when the message catalog fails to open during
a long-running server’s boot_register_client reload?
er_init is “sticky” — if a specific filename is provided, it
will not reinitialise. But the broker’s CAS clients call
er_init again with a different filename during
boot_register_client, which is not sticky. If the
msgcat_init inside that call fails, er_emergency is
invoked and the function returns ER_FAILED. The follow-up
behaviour — does the CAS continue with the partial state, or
abort the connection — is not visible in this file alone.
The er_event_pipe SIGPIPE handler is process-global.
er_event installs a SIGPIPE handler with
os_set_signal_handler and restores it after the write. If
two server threads are writing to the event pipe simultaneously
(serialised by er_Log_file_mutex for most paths but not
strictly here), the saved/restored handler dance is racy. The
practical impact is unclear — the event-handler feature is
rarely enabled in production.
The “must-free” flag on ER_FMT::fmt distinguishes
catalog-pointer fmts from heap-copied ones. For
er_internal_msg-substituted formats, must_free = 0 and the
fmt pointer aliases er_Cached_msg[] (which is owned by the
init/final lifecycle). For er_create_fmt_msg-built formats,
must_free = 1 and the fmt was malloced from the catalog
string. The hand-off across er_clear_fmt is correct but
fragile — a future refactor that drops the flag and centralises
on heap copies might be safer.
Sources
Section titled “Sources”src/base/error_manager.c— central state,er_setfamily,er_log, format compilation/rendering, wire-area pack/unpack, event handler, crash-stack writer.src/base/error_manager.h— public API: severity enum, exit policy enum,er_*declarations,cuberr::manager, convenience macros (ERROR0..ERROR5,ERROR_SET_ERROR_*,ASSERT_ERROR*,assert_release_*).src/base/error_context.cpp— implementation ofcuberr::er_messageandcuberr::context, including the thread_local pointer and the SBO-aware swap.src/base/error_context.hpp— interface forer_va_arg,er_message,context.src/base/error_code.h— flat list ofER_*macros fromNO_ERROR (0)andER_FAILED (-1)down toER_LAST_ERROR (-1371), organised by subsystem prefix.src/base/message_catalog.c— NetBSD/FreeBSDnl_catdreader, the three system catalogs (cubrid.cat,csql.cat,utils.cat),msgcat_messagelookup,mmap-based load.- Cross-references inside the document:
cubrid-thread-worker-pool.md— the per-thread error context lives incubthread::entry::m_error; worker dispatch and entry claim/retire govern context lifecycle.cubrid-network-protocol.md— request/response framing that carries the packed error area between client and server.
- Textbook framing: Database System Concepts (Silberschatz, Korth, Sudarshan) for the recovery/concurrency-control rationale that motivates per-thread, structured, localised errors.