Skip to content

CUBRID Error Management — Per-Thread Error Context, Stack, Message Catalog, and Wire Propagation

Contents:

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:

  1. 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.

  2. 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’s ER_* constants, CUBRID’s ER_* macros in error_code.h. The integer is the protocol; the string is the user interface.

  3. 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 a printf-style format string. CUBRID inherits this mechanism from the NetBSD/FreeBSD nl_catd family.

  4. 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_set machinery 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.

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 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 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-thread cuberr::context is owned by cubthread::entry (one context per worker, one per daemon), and er_set writes into the context bound to the calling THREAD_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 as SERVER_MODE but 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.hpp
struct 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.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 -1371

Two 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.

// er_severity — base/error_manager.h
enum 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.h
extern 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:

  1. Refuse if uninitialised. er_Hasalready_initiated must be true. If not, set er_Errid_not_initialized = err_id, assert, return ER_FAILED. The HA applylogdb / copylogdb workaround under CS_MODE lets uninit slide to silently drop the call.
  2. Capture errno. If the caller is er_set_with_oserror, snapshot strerror(errno) before anything else might clobber errno.
  3. 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.
  4. Initialise the new error level. crt_error.set_error(err_id, severity, file, line) stamps the new error’s identifying tuple. Any prior msg_area is not freed yet — it will be reused if its buffer is large enough.
  5. Look up the format. er_find_fmt(err_id, num_args) returns the cached ER_FMT (or builds one on first use from the message catalog). The format describes the printf conversion specs in the localised message string.
  6. Estimate the size. er_estimate_size(fmt, ap) walks the captured spec array and the va_list to compute an upper bound on the rendered string length. MAX_INT_WIDTH (20) for integers, MAX_DOUBLE_WIDTH (32) for floats, strlen of the actual string for %s.
  7. Reserve space. crt_error.reserve_message_area(new_size + 1) grows the msg_area buffer, doubling until the requested size fits. The 256-byte SBO is reused if the message is short.
  8. Render. er_vsprintf(&crt_error, fmt, ap) walks the format spec array and the simplified, non-positional conversion specs stored in ER_FMT::spec to call sprintf once per spec into the message area. This sidesteps vsprintf’s sometimes-broken %<num>$<code> positional support on legacy platforms — see er_study_spec for the parser. If os_error was captured, "... " + strerror(errno) is appended.
  9. Log. If severity ≤ PRM_ID_ER_LOG_LEVEL (and warnings are not suppressed by PRM_ID_ER_LOG_WARNING), call er_Fnlog[severity] — which is er_log for all severities — under er_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_DEACTIVATION lists), notify the event handler if the error appears in PRM_ID_EVENT_ACTIVATION, and if er_Print_to_console is 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

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.cpp
namespace 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 as tl_Context_p on construction and deregisters on destruction. Used in client er_init for the singleton, and in tests where a context is scoped to a function.
  • Manual. context (false, false) constructs the context without registering; the caller calls register_thread_local() / deregister_thread_local() explicitly. Used by cubthread::entry because the entry decides when to register its embedded m_error member 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.cpp
cuberr::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.hpp
class 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.cpp
void 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.c
struct 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.c
MSG_CATD
msgcat_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.c
static 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

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 either msglog_filename passed to er_init, resolved relative to $CUBRID/log/, or PRM_ID_ER_LOG_FILE from system parameters, or NULL (in which case stderr is used).
  • er_Accesslog_filename — the access log for client-connect events, suffix .access. Built by stripping .err from er_Msglog_filename (or appending .access if there is no .err). Only error code ER_BO_CLIENT_CONNECTED is 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.c
void
er_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\n

Substituting 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.c
char *
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.c
int
er_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)
#endif

In 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.

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.c
static void
er_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).

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]

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. Initialises er_Fmt_list[], primes er_Cached_msg[] from the catalog, opens the log file, registers the singleton context (CS_MODE only), installs the event handler.
  • er_is_initialized () — reports er_Hasalready_initiated.
  • er_set_print_property (print_console) — flips er_Print_to_console.
  • er_final (do_global_final) — tears down: closes log files, frees cached messages, deinits format list, calls er_event_final and er_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 (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 append strerror(errno).
  • er_set_internal (severity, file, line, err_id, num_args, include_os_error, fp, ap_ptr) — common implementation.
  • er_find_fmt (err_id, num_args) — lazy lookup in er_Fmt_list[-err_id]; on first miss, calls msgcat_message and er_create_fmt_msg.
  • er_create_fmt_msg (fmt, err_id, msg) — copies the format string and runs er_study_fmt.
  • er_study_fmt (fmt) — scans the format for % specs, fills in fmt->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 into er_va_arg[], then walks the format calling sprintf per spec into msg_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 where er_malloc itself fails; understands only %s / %d and writes directly into the existing msg_buffer.
  • er_malloc_helper (size, file, line) — wraps malloc with er_emergency on failure.
  • er_message::er_message (logging) — constructor.
  • er_message::~er_message () — calls clear_message_area and clear_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 () — returns m_stack.top() or m_base_level.
  • context::register_thread_local () / deregister_thread_local () — set/reset tl_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 the cubthread::entry fallback in SERVER_MODE.
  • 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 to er_stack_push_if_exists.
  • er_stack_clearall () — drain stack, keep last real error.
  • er_log (err_id) — the er_Fnlog[*] callback. Selects log file (msg vs. access), checks/rotates size, formats with ER_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.
  • er_get_msglog_filename () — getter.
  • er_set_access_log_filename () — derives access-log name from msglog name.
  • er_file_open (path)fopen with mkdir -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 _latest symlink.
  • 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.
  • er_event_init ()popen the configured command, write startup banner.
  • er_event () — write one line to the pipe under setjmp-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 on PRM_ID_EVENT_ACTIVATION.
  • er_register_log_handler (handler) — register a CS-side callback fired per-error with the EID.
  • er_event_restart () — public wrapper around er_event_init for runtime reconfigure.
  • _er_log_debug (file, line, fmt, ...) — public macro wrapper; writes a timestamped DEBUG line.
  • _er_log_debug_internal (file, line, fmt, ap) — body shared with er_print_callstack.
  • er_log_debug (...) macro — gated by PRM_ID_ER_LOG_DEBUG.
  • er_severity enum — five levels.
  • er_exit_ask enum — ER_NEVER_EXIT, ER_EXIT_ASK, ER_EXIT_DONT_ASK, ER_ABORT.
  • er_print_option enum — ER_DO_NOT_PRINT, ER_PRINT_TO_CONSOLE.
  • er_final_code enum — 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 in er_Fmt_list[].
  • er_message struct (error_context.hpp) — per-level error state.
  • cuberr::context — per-thread error container.
  • cuberr::manager — RAII wrapper around er_init / er_final (used via ER_SAFE_INIT).
  • 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 to LANG_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, mimicking catopen(3).
  • cub_catgets (catd, set_id, msg_id, default) — internal binary-search lookup.
  • cub_catclose (catd)munmap and free.
  • load_msgcat (path)mmap the file, validate the NLS_MAGIC header, return the descriptor.
  • nls_cat_hdr / nls_set_hdr / nls_msg_hdr — on-disk binary structs (big-endian).
  • _nl_cat_d (typedef’d cub_nl_catd) — in-memory catalog descriptor.
  • msgcat_def — entry in msgcat_System[] table mapping cat_id to filename.
  • 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 the er_Fmt_list[] array.

Position hints (as of updated: 2026-05-01)

Section titled “Position hints (as of updated: 2026-05-01)”
SymbolFileLine
NO_ERRORsrc/base/error_code.h49
ER_FAILEDsrc/base/error_code.h50
ER_GENERIC_ERRORsrc/base/error_code.h52
ER_INTERRUPTEDsrc/base/error_code.h54
ER_LK_UNILATERALLY_ABORTEDsrc/base/error_code.h133
ER_FAILED_ASSERTIONsrc/base/error_code.h698
ER_LAST_ERRORsrc/base/error_code.h1760
ARG_FILE_LINEsrc/base/error_manager.h44
ERROR0 macrosrc/base/error_manager.h49
ERROR_SET_ERROR macrosrc/base/error_manager.h137
assert_release macrosrc/base/error_manager.h189
er_severity enumsrc/base/error_manager.h216
ER_IS_LOCK_TIMEOUT_ERRORsrc/base/error_manager.h237
ER_IS_ABORTED_DUE_TO_DEADLOCKsrc/base/error_manager.h246
ER_IS_SERVER_DOWN_ERRORsrc/base/error_manager.h250
ASSERT_ERRORsrc/base/error_manager.h258
ASSERT_ERROR_AND_SETsrc/base/error_manager.h263
er_init declsrc/base/error_manager.h287
er_set declsrc/base/error_manager.h292
er_set_with_oserror declsrc/base/error_manager.h295
er_errid declsrc/base/error_manager.h304
er_msg declsrc/base/error_manager.h307
er_get_area_error declsrc/base/error_manager.h314
er_set_area_error declsrc/base/error_manager.h315
er_stack_push declsrc/base/error_manager.h316
er_stack_pop declsrc/base/error_manager.h318
cuberr::manager classsrc/base/error_manager.h354
ER_SAFE_INIT macrosrc/base/error_manager.h366
cuberr::ER_EMERGENCY_BUF_SIZEsrc/base/error_context.hpp32
cuberr::er_va_arg unionsrc/base/error_context.hpp35
cuberr::er_message structsrc/base/error_context.hpp45
cuberr::context classsrc/base/error_context.hpp76
tl_Context_p (thread_local)src/base/error_context.cpp49
er_message::swapsrc/base/error_context.cpp73
er_message::set_errorsrc/base/error_context.cpp151
er_message::reserve_message_areasrc/base/error_context.cpp187
context::push_error_stacksrc/base/error_context.cpp278
context::pop_error_stacksrc/base/error_context.cpp284
context::pop_error_stack_and_destroysrc/base/error_context.cpp296
context::get_thread_local_contextsrc/base/error_context.cpp332
context::get_thread_local_errorsrc/base/error_context.cpp350
er_severity_string[]src/base/error_manager.c152
ER_MSG_SET / ER_INTERNAL_MSG_SETsrc/base/error_manager.c170
enum er_msg_nosrc/base/error_manager.c192
er_Builtin_msg[]src/base/error_manager.c221
ER_MSG_LOG_FILE_SUFFIXsrc/base/error_manager.c287
er_Fmt_list[]src/base/error_manager.c299
er_Fnlog[]src/base/error_manager.c374
er_initsrc/base/error_manager.c679
er_file_opensrc/base/error_manager.c999
er_file_backupsrc/base/error_manager.c1065
er_file_create_link_to_current_log_filesrc/base/error_manager.c1091
er_finalsrc/base/error_manager.c1127
er_clearsrc/base/error_manager.c1202
er_setsrc/base/error_manager.c1229
er_set_with_filesrc/base/error_manager.c1257
er_set_with_oserrorsrc/base/error_manager.c1286
er_call_stack_dump_on_errorsrc/base/error_manager.c1347
er_set_internalsrc/base/error_manager.c1385
er_logsrc/base/error_manager.c1595
er_register_log_handlersrc/base/error_manager.c1801
er_erridsrc/base/error_manager.c1826
er_errid_if_has_errorsrc/base/error_manager.c1853
er_clearidsrc/base/error_manager.c1866
er_setidsrc/base/error_manager.c1887
er_get_severitysrc/base/error_manager.c1907
er_has_errorsrc/base/error_manager.c1918
er_msgsrc/base/error_manager.c1932
er_allsrc/base/error_manager.c1974
_er_log_debugsrc/base/error_manager.c1998
er_get_ermsg_from_area_errorsrc/base/error_manager.c2085
er_get_area_errorsrc/base/error_manager.c2100
er_set_area_errorsrc/base/error_manager.c2147
er_stack_pushsrc/base/error_manager.c2226
er_stack_push_if_existssrc/base/error_manager.c2239
er_stack_popsrc/base/error_manager.c2265
er_stack_pop_and_keep_errorsrc/base/error_manager.c2276
er_restore_last_errorsrc/base/error_manager.c2314
er_stack_clearallsrc/base/error_manager.c2334
er_study_specsrc/base/error_manager.c2364
er_study_fmtsrc/base/error_manager.c2545
er_estimate_sizesrc/base/error_manager.c2644
er_find_fmtsrc/base/error_manager.c2737
er_create_fmt_msgsrc/base/error_manager.c2800
er_init_fmtsrc/base/error_manager.c2833
er_clear_fmtsrc/base/error_manager.c2850
er_internal_msgsrc/base/error_manager.c2877
er_malloc_helpersrc/base/error_manager.c2895
er_emergencysrc/base/error_manager.c2921
er_vsprintfsrc/base/error_manager.c3016
er_is_error_severitysrc/base/error_manager.c3233
cuberr::manager::managersrc/base/error_manager.c3262
er_print_crash_callstacksrc/base/error_manager.c3278
nls_cat_hdr structsrc/base/message_catalog.c81
nls_set_hdr structsrc/base/message_catalog.c90
nls_msg_hdr structsrc/base/message_catalog.c97
cub_catopensrc/base/message_catalog.c131
cub_catgetssrc/base/message_catalog.c292
cub_catclosesrc/base/message_catalog.c366
load_msgcatsrc/base/message_catalog.c389
msgcat_System[]src/base/message_catalog.c487
msgcat_initsrc/base/message_catalog.c500
msgcat_finalsrc/base/message_catalog.c526
msgcat_messagesrc/base/message_catalog.c557
msgcat_opensrc/base/message_catalog.c599
msgcat_get_descriptorsrc/base/message_catalog.c641
msgcat_getssrc/base/message_catalog.c658
msgcat_closesrc/base/message_catalog.c674
msgcat_open_filesrc/base/message_catalog.c695

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.

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.

  • src/base/error_manager.c — central state, er_set family, 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 of cuberr::er_message and cuberr::context, including the thread_local pointer and the SBO-aware swap.
  • src/base/error_context.hpp — interface for er_va_arg, er_message, context.
  • src/base/error_code.h — flat list of ER_* macros from NO_ERROR (0) and ER_FAILED (-1) down to ER_LAST_ERROR (-1371), organised by subsystem prefix.
  • src/base/message_catalog.c — NetBSD/FreeBSD nl_catd reader, the three system catalogs (cubrid.cat, csql.cat, utils.cat), msgcat_message lookup, mmap-based load.
  • Cross-references inside the document:
    • cubrid-thread-worker-pool.md — the per-thread error context lives in cubthread::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.