(KO) CUBRID 오류 관리 — 스레드별 오류 컨텍스트, 스택, 메시지 카탈로그, 네트워크 전파
목차
학술적 배경
섹션 제목: “학술적 배경”모든 데이터베이스 엔진은 소스 줄 수의 상당한 비율을 오류 보고에 쏟는다. 버퍼 풀이 프레임을 할당하지 못하거나, B-트리가 잘못된 페이지를 분할하거나, 네트워크 계층이 짧은 헤더를 읽거나, 파서가 엉뚱한 자리에서 키워드를 만나거나 — 이런 상황은 모두 네 가지로 변환되어야 한다. 호출자가 분기할 수 있는 값, 운영자가 매뉴얼에서 찾아볼 수 있는 사람이 읽을 수 있는 문자열, 사후 분석에 충분한 맥락을 담은 로그 줄, 클라이언트 드라이버가 SQLException으로 다시 올릴 수 있는 네트워크 페이로드. 교과서적 문제는 작지만 엔진의 모든 서브시스템이 이를 다루기 때문에 공학적 범위는 넓다.
Database System Concepts (Silberschatz, Korth, Sudarshan)는 오류 보고를 독립 장으로 다루지 않는다. 그러나 복구와 동시성 제어 장 곳곳에서 이 제약이 암묵적으로 드러난다. 트랜잭션은 더러운 읽기를 발견했을 때 스스로 중단할 수 있어야 하고, 교착 상태의 피해자는 왜 선택되었는지 통보받아야 하며, WAL 기록자는 일시적 I/O 재시도와 치명적 미디어 오류를 구분해야만 한다. 이 각각은 반환값, 오류 코드, 메시지를 요구한다. 교재는 이 추상화에 이름을 붙이지 않지만 모든 구현체가 이를 필요로 한다는 점은 분명하다.
구현 문헌은 더 직접적이다. 모든 실제 오류 서브시스템을 빚는 네 가지 제약이 있다.
-
스레드별 오류 상태. 관계형 엔진은 멀티스레드다. 현재 오류는 프로세스의 속성이 아니라 방금 시스템 호출을 실행한 스레드의 속성이다. 병렬로 쿼리를 실행하는 두 스레드는 각자 가장 최근 자신의 오류만 보아야 하고 상대방 오류는 보지 않아야 한다. 따라서 구현체들은 현재 오류 레코드를 스레드별 슬롯에 보관한다. 단순한 경우에는 TLS, 정교한 경우에는 스레드별 컨텍스트 구조체의 멤버다. CUBRID는 후자를 택한다.
-
구조화된 오류 코드. 텍스트 메시지만을 신호로 반환하는 것은 실용적이지 않다. 호출자는 분기해야 하는데 문자열 비교는 느리고 로케일에 따라 깨질 수 있기 때문이다. 모든 DBMS는 안정적인 정수 키 오류 코드 공간을 갖는다. PostgreSQL의 SQLSTATE와 ErrCode, Oracle의
ORA-NNNNN, MySQL의ER_*상수, CUBRID의error_code.h에 있는ER_*매크로가 그것이다. 정수는 프로토콜이고 문자열은 사용자 인터페이스다. -
로케일별 메시지. 한국인 DBA는 한국어 오류 메시지를 읽고 영어권 DBA는 영어 메시지를 읽는다. 정수 코드는 공유되고 문자열만 다르다. 표준 메커니즘은
gencat같은 도구로 빌드한 로케일별 메시지 카탈로그 파일이다. 프로세스 시작 시 열어두고(set_id, msg_id)쌍으로printf형식 문자열을 조회한다. CUBRID는 이 메커니즘을 NetBSD/FreeBSD의nl_catd계열에서 이어받는다. -
네트워크 전파. 클라이언트와 서버가 다른 프로세스에 있을 때 오류는 네트워크를 건너야 한다. 실용적인 두 가지 방식이 있다. (a) 서버에서 오류를 패킹해 응답에 싣고 클라이언트에서 언패킹, (b) 미리 렌더링한 메시지 문자열을 보내고 클라이언트는 불투명하게 처리. CUBRID는 (a)를 택한다. 세 개의 패킹된 정수(errid, severity, length)와 렌더링된 메시지 바이트를 전송한 뒤, 클라이언트에서 같은
er_set경로로 오류를 다시 올린다. 그래서 클라이언트 측 핸들러는 로컬 오류를 다루는 것과 동일한 방식으로 서버 오류를 처리한다.
기본 형태가 갖춰지고 나면 두 가지 추가 설계 질문이 등장한다.
첫 번째는 심각도 — 경고인가, 오류인가, 치명적인가? 경고는 로그에
남길 필요조차 없을 수 있고, 오류는 기억되어 반환되어야 하며, 치명적
오류는 엔진을 멈춰야 할 수 있다. 두 번째는 스택 — 오류 경로에서
호출된 정리 루틴이 스스로 실패했을 때, 원래 오류를 덮어쓸 것인가
(원인을 잃는다) 아니면 새 오류를 중첩할 것인가 (항목 누출 위험이
있다)? CUBRID의 답은 스레드별 std::stack<er_message>를 두고
er_stack_push / er_stack_pop 을 명시적으로 호출하는 것이다.
그리고 pop_and_keep_error 변형이 살아남은 오류를 보존한다.
DBMS 공통 설계 패턴
섹션 제목: “DBMS 공통 설계 패턴”위에서 나열한 네 가지 제약은 모든 실제 DBMS 오류 서브시스템의 형태를 결정한다. 변형은 어떤 추상화를 호출자에게 노출하느냐와 카탈로그를 얼마나 공격적으로 활용하느냐에 있다.
PostgreSQL — ereport(elevel, ...) 매크로
섹션 제목: “PostgreSQL — ereport(elevel, ...) 매크로”PostgreSQL은 필드 (errcode, errmsg, errdetail, errhint,
errcontext, errposition, …) 로 오류를 구성한다. 진행 중인 오류
레코드에 체인 호출로 필드를 붙이는 방식이다. 진입점은
ereport(level, (errcode(ERRCODE_FOO), errmsg(...), ...)) 매크로다.
level은 심각도(LOG, WARNING, ERROR, FATAL, PANIC)를 선택하고,
본문은 필드를 채운다. errmsg는 gettext로 번역된 형식 문자열을
받는다. 레벨이 ERROR 이상이면 siglongjmp로 가장 가까운
PG_TRY/PG_CATCH 블록으로 제어가 비지역적으로 이동한다. PostgreSQL은
이 던지기-잡기 모델로 모든 트랜잭션 중단 시 백엔드의 할당을 해제한다.
백엔드는 프로세스당 단일 ErrorData 스택을 갖고, elog_start /
elog_finish가 항목을 올리고 내린다.
MySQL — my_error(error_code, MYF(0), ...) 와 THD::raise_error_*
섹션 제목: “MySQL — my_error(error_code, MYF(0), ...) 와 THD::raise_error_*”MySQL의 오래된 C 인터페이스는 my_error(int err, myf MyFlags, ...)다.
my_get_err_msg()로 시스템 오류 카탈로그 errmsg.sys에서 메시지를
형식화하고 my_message()로 출력한다. 현대적인 C++ 인터페이스는
THD::raise_error_printf / THD::raise_warning_printf로
THD별 진단 영역(Diagnostics_area)에 기록한다. 진단 영역은
SQL 표준 모델을 따른다. 조건 스택에 GET DIAGNOSTICS로 접근한다.
로케일화는 errmsg-utf8.txt를 로케일별로 컴파일한 errmsg.sys로
이뤄진다.
Oracle — ORA-NNNNN 번호와 dbms_utility.format_error_*
섹션 제목: “Oracle — ORA-NNNNN 번호와 dbms_utility.format_error_*”Oracle은 다섯 자리 ORA-NNNNN 번호로 오류를 노출한다. 내부적으로
커널은 kge 오류 계층(kernel generic error)으로 오류를 올린다.
오류 정의는 oraus.msg와 로케일 카탈로그에 있다. PL/SQL은
STANDARD.sql에 이름 붙은 예외로 이를 표면화한다. 서버 측 저장
프로시저는 DBMS_UTILITY.FORMAT_ERROR_BACKTRACE로 호출 지점을
복원할 수 있다. 심각도는 번호 범위에서 대략 유추된다. 네트워크
전파는 OCI/Net의 구조화된 오류 패킷을 통한다.
CUBRID — er_set(severity, file, line, code, n_args, ...)
섹션 제목: “CUBRID — er_set(severity, file, line, code, n_args, ...)”CUBRID는 PostgreSQL과 MySQL 설계의 중간 어딘가에 위치한다.
매크로는 er_set (그리고 편의 매크로 ERROR0..ERROR5,
ERROR_SET_ERROR_*, ERROR_SET_WARNING_*)이다. 정수 코드 공간은
error_code.h의 ER_*이고, 카탈로그는 로케일별 NetBSD/FreeBSD
nl_catd 파일(cubrid.msg, csql.msg, utils.msg)이며,
스레드별 상태는 기본 er_message와 명시적 저장/복원용
std::stack<er_message>를 담은 cuberr::context다. 던지기-잡기는
없다. 엔진은 C 스타일로 오류를 일반 반환값으로 돌려보내고, 오류는
지워질 때까지 스레드 컨텍스트에 산다. 와이어 형식은 세 개의 패킹된
OR_INT 필드와 렌더링된 메시지 문자열이라는 가장 작은 형태다.
다음 절에서 각 조각을 걸어본다.
CUBRID의 구현
섹션 제목: “CUBRID의 구현”CUBRID 오류 서브시스템은 클라이언트와 서버가 함께 사용한다. 동일한
error_manager.c가 cub_server 바이너리(SERVER_MODE), cubridcs
클라이언트 라이브러리(CS_MODE), cubridsa 단독 실행 라이브러리
(SA_MODE)에 컴파일된다. er_set 호출은 양쪽에서 모두 일어나고
와이어 형식이 그 둘을 연결한다. 세 가지 전처리기 가드가 모드별
동작을 선택한다.
SERVER_MODE— 스레드별cuberr::context는cubthread::entry가 소유한다. 워커 하나당 하나, 데몬 하나당 하나다.er_set은 호출 스레드의THREAD_ENTRY *에 바인딩된 컨텍스트에 기록한다. 상호 참조:cubrid-thread-worker-pool.md.CS_MODE— 클라이언트 프로세스는 단일 싱글턴 컨텍스트er_Singleton_context_p를 갖는다. 이것이 메인 스레드의 스레드 로컬 컨텍스트로 등록된다. 헬퍼 스레드(브로커 연결 스레드, HA copy/applylogdb 워커)는 자신의 컨텍스트를 직접 등록한다.SA_MODE—SERVER_MODE와 같지만 엔트리 풀 크기가 작다. 매니저가 아직 초기화되지 않은 경우 스레드 컨텍스트 조회는 싱글턴으로 폴백한다.
먼저 데이터 배치를 이해해야만 한다. 어떤 상태가 어디에 사는지를 파악하는 것이다.
// 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 ...};이 구조에서 세 가지 관찰이 나머지 설명의 틀을 잡는다.
첫째, er_message는 256바이트 인라인 버퍼(msg_buffer)를 갖는다.
짧은 메시지라면 msg_area가 이 버퍼를 가리킨다. 메시지가 256바이트를
초과할 때만 reserve_message_area가 힙 버퍼를 할당한다. 일반적인
경우에는 할당이 전혀 발생하지 않는다는 점이다.
둘째, 캡처된 인자(args, nargs)는 렌더링된 텍스트와 함께 보관된다.
er_set이 반환된 뒤에도 msg_area의 렌더링된 문자열과 args의
원시 인자값이 모두 접근 가능하다. 단, 일반적인 독자(er_msg, 와이어
패커)는 렌더링된 문자열만 소비한다.
셋째, context::get_current_error_level()은 스택이 비어 있지 않으면
m_stack.top()을, 그렇지 않으면 m_base_level을 반환한다. 현재
오류는 위에 있는 것이며, er_stack_push / er_stack_pop이 어느
레벨이 활성인지를 바꾼다. 이는 PostgreSQL의 필드 체인 모델보다 구조적으로
깔끔하다. 어느 순간에나 정확히 하나의 er_message가 활성이기
때문이다. 다만 정리 경로에서 push/pop을 기억해야 한다는 비용이 따른다.
오류 코드 열거형
섹션 제목: “오류 코드 열거형”error_code.h는 오류별 #define ER_xxx -N 매크로의 단순 목록이다.
맨 위에 NO_ERROR = 0과 ER_FAILED = -1이 예약되어 있다.
// 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두 가지 설계 선택을 짚고 넘어간다.
0이 아닌 모든 오류 코드는 음수다. 코드는 성공값과의 차이이며,
er_find_fmt는 -err_id를 배열 인덱스로 써서 er_Fmt_list를
참조한다. 심각도는 코드에 인코딩되지 않는다. 같은 ER_INTERRUPTED
(-4)가 정리 경로에서는 WARNING 심각도로, 사용자 쿼리에서는 ERROR
심각도로 올려질 수 있다.
코드는 서브시스템 접두사로 묶인다. 접두사는 어느 모듈이 이 오류를
소유하는지 알려준다. ER_LK_*는 잠금 관리자, ER_LOG_*는 로그
기록자/복구기, ER_BO_*는 부트, ER_BTREE_*는 B-트리, ER_PB_*는
페이지 버퍼, ER_HEAP_*는 힙 파일, ER_DISK_*는 디스크 관리자,
ER_NET_*는 네트워크, ER_AU_*는 인증, ER_SM_*는 스키마 관리자,
ER_QPROC_*는 쿼리 처리, ER_TR_*는 트리거, ER_LDR_*는 벌크
로더, ER_FK_*는 외래 키, ER_TP_*는 타입 강제 변환, ER_LC_*는
로케이터, ER_IO_*는 원시 I/O, ER_FILE_*는 파일 시스템, ER_CT_*는
카탈로그, ER_OBJ_*는 객체 관리자, ER_REGU_*는 regu/표현식 평가기다.
새 오류를 추가하려면 접두사를 고르고 ER_LAST_ERROR 아래의 다음
빈 슬롯을 선택한 뒤 ER_LAST_ERROR를 올려야 한다. 헤더 파일 맨 위에
그 여섯 단계 절차가 주석으로 명시되어 있다.
// 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. */나머지 엔진이 가장 많이 참조하는 코드들 — 매크로로 이름 붙여지고
호출자가 직접 검사하는 것들 — 은 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)이 매크로들이 이 오류가 교착 상태인가?, 서버가 죽었는가?에
대한 권위 있는 답이다. 브로커, JDBC 드라이버 심, HA 복사 데몬,
재시도가 필요한 사용자 모드 유틸리티가 이를 읽는다. 접두사 규칙만
으로는 충분하지 않다. 잠금 관리자에는 실제 잠금 타임아웃
(ER_LK_OBJECT_TIMEOUT_*)과 교착 상태 피해자 중단 티켓
(ER_LK_UNILATERALLY_ABORTED) 모두 있기 때문이다. 이 매크로가
둘을 구분하는 방법이다.
심각도
섹션 제목: “심각도”// 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};심각도는 오류 코드와 독립된 축이다. 같은 코드가 어떤 심각도로든 올려질 수 있으며, 심각도는 오류의 정체성이 아닌 이후 동작을 결정한다. 다섯 단계가 정의된다.
ER_FATAL_ERROR_SEVERITY는 er_log의 중단/종료 로직을 발동시킨다.
er_Exit_ask 값에 따라 ER_ABORT는 abort()를 호출하고,
ER_EXIT_DONT_ASK는 er_final을 거쳐 exit(EXIT_FAILURE)를 호출하며,
ER_EXIT_ASK는 비디버그 빌드에서 stdin에 프롬프트를 보내고,
ER_NEVER_EXIT는 아무것도 하지 않는다. FATAL 심각도는 C 호출 스택을
로그에 무조건 덤프한다.
ER_ERROR_SEVERITY는 기본값이다. ASSERT_ERROR() / ASSERT_NO_ERROR()가
신경 쓰는 것, er_has_error()와 er_errid_if_has_error()가 실제
오류로 세는 것이다. 편의 매크로 계열 ERROR_SET_ERROR_*가 이 심각도를
생성한다.
ER_SYNTAX_ERROR_SEVERITY는 파서의 버킷이다. er_has_error 목적상
ERROR와 의미적으로 동일하다. (er_is_error_severity 헬퍼는 FATAL,
ERROR, SYNTAX를 true를 반환한다.) 다만 브로커로 다른 경로로
라우팅된다.
ER_WARNING_SEVERITY는 er_has_error()에서 오류로 등록되지 않는다.
ERROR0..ERROR5 매크로는 기본적으로 이 심각도를 쓴다. 경고는
(PRM_ID_ER_LOG_WARNING에 따라) 로그에 기록되지만 er_has_error
판정 관점에서 호출자의 er_errid()는 NO_ERROR를 반환한다. 단
er_errid() 자체는 경고 코드를 반환한다.
ER_NOTIFICATION_SEVERITY는 가장 특이하다. er_set_internal은
알림을 특별히 처리한다. 현재 레벨에 이미 실제 오류가 있으면 알림은
스택에 쌓인다(tl_context.push_error_stack()). 그래서 원래 오류가
보존된다. 알림 항목을 로그에 남긴 뒤 스택을 팝하고 파기한다
(pop_error_stack_and_destroy). 결과적으로 알림은 실제 오류를 결코
덮어쓰지 않는다. 로그 파이프를 통과하다 사라진다.
// 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 (); }같은 보호가 현재 오류가 ER_INTERRUPTED일 때도 적용된다. “정리하고
종료하라”는 신호를 받은 스레드는 그 상태를 이후 정리 루틴의 오류에
잃어서는 안 된다. push/pop 가드가 정리 오류를 잠깐 보이게 했다가
버린다.
설정 진입점 — er_set, er_set_with_oserror, er_set_with_file
섹션 제목: “설정 진입점 — er_set, er_set_with_oserror, er_set_with_file”사용자가 직접 호출하는 진입점은 er_set이다. 세 친구와 함께 전체
서명은 다음과 같다.
// 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는 렌더링된 메시지에 strerror(errno)를 덧붙인다.
I/O 경로에서 OS 수준 원인을 언급할 때 쓴다. er_set_with_file은
FILE *을 받아 그 내용을 오류와 함께 로그에 넣는다. 로더와 파서가
문제의 입력 파일을 덤프할 때 사용한다. 셋 모두 er_set_internal로
수렴한다. ARG_FILE_LINE 매크로(#define ARG_FILE_LINE __FILE__, __LINE__)는
호출자가 file/line 인자를 직접 타이핑하지 않아도 되게 해준다.
// 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)ERROR0..ERROR5는 기본적으로 경고를 올린다는 점에 주의해야 한다.
오류를 의도할 때는 ERROR_SET_ERROR_*를 쓰는 것이 맞다.
er_set_internal 흐름은 아홉 단계다. 오류 흐름 유한 상태 기계는
이 본문으로만 의미가 생기므로 순서대로 따라가볼 가치가 있다.
- 초기화 전이면 거부한다.
er_Hasalready_initiated가 true여야 한다. 그렇지 않으면er_Errid_not_initialized = err_id를 설정하고 assert 후ER_FAILED를 반환한다. HAapplylogdb/copylogdb의CS_MODE우회 경로는 초기화 전 호출을 조용히 버린다. - errno를 캡처한다. 호출자가
er_set_with_oserror라면 다른 어떤 것이errno를 덮어쓰기 전에strerror(errno)를 즉시 스냅숏한다. - 스택 여부를 결정한다. 새 심각도가 NOTIFICATION이고 현재
레벨에 실제 오류가 있으면, 또는 현재 오류가
ER_INTERRUPTED이면 새 스택 프레임을 push해 원래 오류를 보존한다. - 새 오류 레벨을 초기화한다.
crt_error.set_error(err_id, severity, file, line)으로 새 오류의 식별 튜플을 찍는다. 기존msg_area는 아직 해제하지 않는다. 버퍼가 충분히 크면 재사용한다. - 형식을 조회한다.
er_find_fmt(err_id, num_args)가 캐시된ER_FMT를 반환한다. 첫 사용이면 메시지 카탈로그에서 새로 빌드한다. 형식은 로케일화된 메시지 문자열의 printf 변환 명세를 설명한다. - 크기를 추정한다.
er_estimate_size(fmt, ap)가 캡처된 명세 배열과va_list를 훑어 렌더링된 문자열 길이의 상한을 계산한다. 정수는MAX_INT_WIDTH(20), 부동소수점은MAX_DOUBLE_WIDTH(32),%s는 실제 문자열의strlen이다. - 공간을 확보한다.
crt_error.reserve_message_area(new_size + 1)이 필요한 크기에 맞을 때까지 두 배씩msg_area버퍼를 키운다. 메시지가 짧으면 256바이트 SBO를 재사용한다. - 렌더링한다.
er_vsprintf(&crt_error, fmt, ap)가 형식 명세 배열과ER_FMT::spec에 저장된 단순화된 변환 명세를 훑어가며 명세별로sprintf를msg_area에 호출한다. 이는 구형 플랫폼에서 종종 깨지는vsprintf의%<num>$<code>위치 지정자 지원을 우회한다.os_error가 캡처되었다면... + strerror(errno)를 덧붙인다. - 로그를 기록한다.
severity ≤ PRM_ID_ER_LOG_LEVEL이고 경고 억제가 없으면er_Log_file_mutex아래에서er_Fnlog[severity]를 호출한다. 모든 심각도에서 실제로는er_log다. 그 뒤 선택적으로 호출 스택을 덤프(PRM_ID_CALL_STACK_DUMP_ON_ERROR/PRM_ID_CALL_STACK_DUMP_ACTIVATION/PRM_ID_CALL_STACK_DUMP_DEACTIVATION목록)하고, 오류가PRM_ID_EVENT_ACTIVATION에 있으면 이벤트 핸들러에 알리며,er_Print_to_console이 켜져 있으면 stderr에도 출력한다.
9단계 뒤 PRM_ID_ER_STOP_ON_ERROR 디버그 노브가 따라온다. 특정
오류 코드로 설정되면 엔진이 stdin에 프롬프트를 띄우고 해당 코드에서
종료할 수 있다. 알림 스택 팝이 마지막 동작이다.
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
섹션 제목: “스레드별 컨텍스트 — cuberr::context”cuberr::context는 error_context.cpp / .hpp에 있다. 임의의
호출 지점과 컨텍스트를 연결하는 스레드 로컬 포인터는 C++11
thread_local 하나다.
// thread_local pointer — base/error_context.cppnamespace cuberr{ thread_local context *tl_Context_p = NULL;}context 등록은 명시적이다. 두 가지 패턴이 있다.
- 자동.
context (true, false)는 생성 시 스스로를tl_Context_p로 등록하고 소멸 시 해제한다. 클라이언트er_init의 싱글턴과 테스트에서 함수 범위로 컨텍스트를 쓸 때 사용한다. - 수동.
context (false, false)는 등록 없이 컨텍스트를 생성한다. 호출자가register_thread_local()/deregister_thread_local()을 직접 호출한다.cubthread::entry가 이 방식을 쓰는데, 엔트리는 내장된m_error멤버를 현재 스레드의 컨텍스트로 등록하는 시점을 직접 결정한다. 보통 워커가 엔트리를 획득할 때, 즉 태스크 코드가 실행되기 전에 등록한다.
// 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;}폴백 체인은 다음과 같다. 먼저 assert(정상 코드에서는 여기 도달하면
안 된다). SERVER_MODE이고 매니저가 켜져 있으면 호출 스레드의
cubthread::entry를 조회해 내장 컨텍스트를 반환한다. 그렇지 않으면
정적 긴급 컨텍스트로 폴백한다. 긴급 컨텍스트는 최후 안전망이다.
거기에 도달하는 모든 스레드가 공유하므로 동시 쓰기 시 경쟁이 생긴다.
긴급 컨텍스트에 도달하는 것은 등록 버그의 신호이지 정상 코드 경로가
아니다.
오류 스택은 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()은 스택이 비어 있지 않으면 m_stack.top()을,
그렇지 않으면 m_base_level을 반환한다. push_error_stack()은
새로 기본 생성된 er_message를 스택에 emplace한다. 다음 er_set이
채울 깨끗한 슬레이트다. pop_error_stack(er_message &out)은 최상위
프레임을 호출자가 제공한 목적지로 swap해 팝한다. 호출자가 보존할지
파기할지 결정한다. pop_error_stack_and_destroy()가 일반적인 경우다.
로컬 변수로 팝한 뒤 범위를 벗어나면 소멸된다.
// 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은 SBO 버퍼 때문에 단순하지 않다. 두 메시지 모두
msg_buffer 안에 있으면 바이트를 교환한다. 두 메시지 모두 힙 할당
이면 포인터를 swap한다. 하나는 SBO이고 하나는 힙이면 SBO의 바이트를
복사하고 힙 포인터를 이동한다. swap 이후에도 (msg_area_size == sizeof(msg_buffer)) == (msg_area == msg_buffer) 불변식이 성립한다.
이 주의 깊은 코드가 std::stack의 값 의미론이 요구하는 것처럼 swap
후에도 SBO가 구조체와 연속으로 붙어 있게 한다.
push/pop 위에 네 가지 호출 패턴이 있다.
er_stack_push/er_stack_pop— 직접 저장/복원. 함수가 새 오류를 설정할 때 호출자가 이전 것을 돌려받고 싶을 때 쓴다. 대칭이다.er_stack_push_if_exists/er_restore_last_error— 조건부 저장. 보호할 기존 오류가 있을 때만 push한다. 복원은 현재 또는 push된 것 중 비어 있지 않은 것을 선택한다. 오류가 있을 수도 없을 수도 있는 정리 경로에서 쓴다.er_stack_pop_and_keep_error— 최상위를 팝하되 실제 오류가 있었다면 현재 레벨로 swap한다. 어느 레벨이 이겼든 그것을 올리는 데 쓴다.er_stack_clearall— 스택을 기본 레벨까지 비우되, 실제 오류를 담은 프레임이 있으면 그것을 보존한다.
상호 참조: 스레드별 컨텍스트는 cubthread::entry(m_error 멤버) 안에
있다. 엔트리가 워커와 데몬에 어떻게 디스패치되는지는
cubrid-thread-worker-pool.md를 참조한다. 워커가 엔트리를 획득하면
엔트리의 get_error_context()가 tl_Context_p가 가리키는 것이 된다.
워커가 태스크를 마치고 엔트리를 반납할 때 컨텍스트는 초기화된다
(clear_all_levels). 다음 워커가 엔트리를 획득할 때 NO_ERROR 상태를
보게 된다.
메시지 카탈로그 — cubrid.msg, csql.msg, utils.msg
섹션 제목: “메시지 카탈로그 — cubrid.msg, csql.msg, utils.msg”카탈로그 형식은 NetBSD의 nl_catd 바이너리 형식을 그대로 복사한 것이다.
헤더 충돌을 피하기 위해 이름만 바꿨다.
// 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;};디스크 상의 정수는 모두 빅엔디안이다(읽을 때 ntohl). 파일 구조는
헤더, 정렬된 집합 헤더 테이블, 정렬된 메시지 헤더 테이블, 연속된
텍스트 영역 순이다. 조회는 set_id로 집합 테이블을 이진 탐색하고,
해당 집합 안에서 msg_id로 메시지 테이블을 이진 탐색한 뒤 텍스트
영역의 포인터를 반환한다. 파일은 읽기 전용으로 mmap된다. 조회당
할당이 전혀 없다.
세 가지 시스템 카탈로그가 시작 시 로드된다.
// 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은 엔진의 주 오류 및 알림 카탈로그다. csql.cat은
대화형 SQL 셸, utils.cat은 cubrid backupdb 같은 유틸리티용이다.
.cat 파일은 msg/<locale>/의 .msg 소스로부터 gencat(또는
CUBRID 동등 도구)으로 빌드된다. 소스 트리에 포함된 로케일은
en_US.utf8과 ko_KR.utf8이다. 기타 로케일은 msg/<locale>/에
파일이 있으면 로드된다.
// 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 ...}로케일 해석 순서는 $CUBRID/msg/<system_param_msg_lang>/<name>,
그 다음이 $CUBRID/msg/<LANG_NAME_DEFAULT>/<name>이다. 어느 파일도
없으면 msgcat_open은 NULL을 반환하고 호출자(msgcat_init)가
ER_FAILED를 기록한다. 그래도 엔진은 부팅된다. er_set은 카탈로그
조회 실패 시 내장된 긴급 문자열로 폴백한다. 카탈로그가 없어도
시스템은 동작 가능하다.
cubrid.msg 안에서 두 집합이 중요하다. MSGCAT_SET_INTERNAL(집합 2)은
error_manager.c 자체가 쓰는 문자열을 담는다(“No error message
available, Can’t allocate %d bytes, \n*** The previous error
message is the last one. ***\n\n”, …). MSGCAT_SET_ERROR(집합 1)은
-err_id로 인덱싱되는 코드별 형식 문자열을 담는다. 내부 집합
문자열은 init 시 er_init이 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;카탈로그가 없거나 특정 코드가 없으면 슬롯은 er_Builtin_msg[]의
내장 기본값을 유지한다. 기본값은 카탈로그가 제공할 것과 동일한
영어 문자열을 error_manager.c에 하드코딩한 것이다. 엔진 자신의
내부 메시지는 바이너리에 구워져 있어 사라질 수 없다.
코드별 형식(MSGCAT_SET_ERROR)은 init 시 캐싱되지 않는다. 각 오류가
처음 발생할 때 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는 er_study_fmt를 호출한다. er_study_fmt는
형식 문자열에서 % 변환 명세를 스캔해 ER_FMT::spec[]을 채운다.
명세별로 위치 지정자(예: %2$s), 단순화된 형식(위치 없는), 필드 폭,
va-클래스('i', 'p', 'f', 's', SPEC_CODE_LONGLONG,
SPEC_CODE_SIZE_T)를 기록한다. 이 한 번 컴파일, 재사용 방식 덕분에
같은 오류가 두 번째 이후 발생할 때 er_set이 저렴하다. 명세 배열이
한 번 빌드되어 er_Fmt_list[-err_id]에 보관되고 이후 참조된다.
인자 수 검사(fmt->nspecs == num_args)가 핵심 안전장치다. 호출자가
잘못된 인자 수를 넘기면 엔진은 va_list를 끝 너머로 걷는 대신 “No
message available; original message format in error” 제너릭 문자열로
대체한다.
sequenceDiagram
participant Caller as 호출자 코드
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 (이 코드 첫 사용)
Fmt->>Cat: msgcat_message(CUBRID, SET_ERROR, -code)
Cat-->>Fmt: 로케일화된 printf 형식 문자열
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 → msg_area에 렌더링
ErSet->>Log: er_Fnlog[severity](err_id)\n(er_Log_file_mutex 아래)
Log->>File: 타임스탬프 줄 via er_Cached_msg[ER_LOG_MSG_WRAPPER_D]
Log->>File: 선택적 호출 스택 덤프
alt notification 경로
ErSet->>Ctx: pop_error_stack_and_destroy()
end
오류 로그 파일 — cubrid_*.err
섹션 제목: “오류 로그 파일 — cubrid_*.err”오류 로그 파일은 er_init에서 열리며 두 파일 이름 중 하나에 연결된다.
er_Msglog_filename— 주 오류 로그, 접미사.err. 파일 이름은er_init에 전달된msglog_filename을$CUBRID/log/기준으로 해석한 것이거나PRM_ID_ER_LOG_FILE시스템 매개변수이거나, NULL이면stderr다.er_Accesslog_filename— 클라이언트 연결 이벤트용 접근 로그, 접미사.access.er_Msglog_filename에서.err를 벗겨 만든다(.err가 없으면.access를 덧붙인다). 오류 코드ER_BO_CLIENT_CONNECTED만 이 파일로 라우팅된다. 나머지는 주 로그로 간다.
운영 모드(PRM_ID_ER_PRODUCTION_MODE = true)에서는 파일을 그대로
연다. 비운영 모드에서는 pid를 덧붙여(server.log.<pid>) 여러 서버
프로세스가 서로의 로그를 덮어쓰지 않게 한다. Unix와 SERVER_MODE에서는
<dirname>/<dbname>_latest<suffix> 심볼릭 링크를 만들거나 갱신해
현재 로그 파일을 가리킨다. 그래서 운영자가 tail -f log/server/foo_latest.err로
백업 이벤트를 거쳐도 현재 파일을 계속 추적할 수 있다.
// 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);}회전은 시간이 아닌 크기 기반이다. 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은 현재 파일을 닫고 *.bak으로 이름을 바꾼 뒤(기존
.bak이 있으면 삭제) 새 파일을 연다. 백업 히스토리는 한 단계뿐이다.
가장 최근 백업이 이전 것을 덮어쓴다. 의도적인 절충이다. 로그 파일이
무한히 커지지 않는 대신, 장기 보존을 원하는 운영자는 다음 회전 전에
.bak을 복사해 두어야 한다. PRM_ID_ER_LOG_SIZE 매개변수(보통 4
MiB)가 임계값을 제어한다.
단일 로그 항목의 형식은 내부 메시지 카탈로그의 ER_LOG_MSG_WRAPPER_D다.
내장 기본값은 다음과 같다.
\nTime: %s - %s *** file %s, line %d %s CODE = %d, Tran = %d%s\n%s\n치환되는 것들: 타임스탬프; 심각도 이름(ERROR, FATAL ERROR,
…); file_name; line_no; 심각도 분류자(ERROR 또는 WARNING/NOTIFICATION이면
빈 문자열); err_id; 트랜잭션 인덱스; 선택적 , CLIENT = host:prog(pid), EID = N 접미사; 렌더링된 메시지. EID는 프로세스 단조 증가 이벤트
식별자(er_Eid)로, 클라이언트와 서버 로그 사이에 동일 오류를 연결하는 데
쓰인다. 매 줄 뒤에 엔진은 ER_LOG_LAST_MSG("\n*** The previous error message is the last one. ***\n\n")를 기록하고 플러시한 뒤
fseek(-wsz, SEEK_CUR)으로 되돌린다. 다음 항목이 이 마커를 덮어쓴다.
어느 순간에 읽어도 완전한 꼬리를 볼 수 있다.
네트워크 전파 — er_get_area_error / er_set_area_error
섹션 제목: “네트워크 전파 — er_get_area_error / er_set_area_error”서버 측에서 er_set이 발생하면 오류는 클라이언트로 전달되어야 한다.
패킹 함수는 현재 스레드 로컬 오류를 세 개의 패킹된 정수와 렌더링된
메시지로 평탄화한다.
// 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;}와이어 레이아웃은 따라서 [errid:int32][severity:int32][length:int32][msg:bytes][\0]이다.
모든 정수는 OR_PUT_INT로 네트워크 바이트 순서로 기록된다.
원래 er_set 호출 지점의 file_name과 line_no는 전송되지 않는다.
서버 내부 정보기 때문이다. 클라이언트는 오류 코드, 심각도, 로케일화된
메시지를 보고 클라이언트 측에서 의미 있는 er_set을 재현하기에 충분하다.
언패킹 측은 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;}클라이언트는 현재 스레드 로컬 오류를 서버가 공급한 튜플로 덮어쓴다.
file/line은 자리표시자 Unknown from server / -1로 설정하고,
메시지 바이트를 그대로 복사한 뒤 로컬 로그 파이프를 실행한다. 클라이언트
프로세스도 자신의 cubrid_*.err에 이벤트를 기록하게 된다. 함수는
crt_error.err_id를 반환해 네트워크 계층이 return er_set_area_error(buf)로
코드를 함수 반환값으로 전파할 수 있게 한다.
보완 헬퍼 er_get_ermsg_from_area_error(buffer)는 한 줄짜리
return buffer + (OR_INT_SIZE * 3);이다. 이미 패킹된 영역 안의 메시지
텍스트에 재평탄화 없이 접근하는 방법을 네트워크 통계 계층에 제공한다.
패킹/언패킹은 네트워크 인터페이스 계층(network_interface_sr.c /
network_interface_cl.c)에서 감싼다. 요청/응답 패킷이 어떻게 구성되는지는
cubrid-network-protocol.md를 참조한다. 관련 패턴은 이렇다. 서버 측
요청 핸들러는 성공(NO_ERROR)이면 그대로 반환하고, 오류이면 결과와
er_get_area_error(...)를 함께 응답 버퍼에 패킹한다. 클라이언트의
net_client_request_* 계열은 응답에서 오류 영역을 확인하고 있으면
er_set_area_error를 호출한 뒤 언패킹된 코드를 함수 결과로 반환한다.
sequenceDiagram
participant App as 클라이언트 앱
participant NetCl as network_interface_cl
participant NetSr as network_interface_sr
participant Engine as 서버 엔진 경로
participant SrvCtx as 서버 스레드 컨텍스트
participant CliCtx as 클라이언트 스레드 컨텍스트
App->>NetCl: db_query_execute(...)
NetCl->>NetSr: TCP 요청 패킷
NetSr->>Engine: 핸들러 디스패치
Engine->>Engine: btree_find / lock_object / ...
Engine->>SrvCtx: er_set(ER_BTREE_DUPLICATE_OID, ...)
SrvCtx->>SrvCtx: cubthread::entry의 m_error에 crt_error 기록
Engine-->>NetSr: return ER_FAILED
NetSr->>SrvCtx: er_get_area_error(buf, &len)
SrvCtx-->>NetSr: [errid|sev|len|msg|\0]
NetSr-->>NetCl: TCP 응답 패킷 (오류 영역 포함)
NetCl->>CliCtx: er_set_area_error(server_area)
CliCtx->>CliCtx: crt_error 덮어쓰기, 클라이언트 *.err에 로그
CliCtx-->>NetCl: errid 반환
NetCl-->>App: errid 반환 (예: ER_BTREE_DUPLICATE_OID)
App->>App: db_error_code() == ER_BTREE_DUPLICATE_OID
편의 매크로 — 어설션과 관련 상수
섹션 제목: “편의 매크로 — 어설션과 관련 상수”error_manager.h는 엔진 전체에 자유롭게 뿌려진 디버깅 어설션 집합을
노출한다.
// 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);이 매크로들이 인코딩하는 의미 계약은 엔진이 오류 반환에 걸쳐 유지하는
불변식이다. ER_FAILED를 반환하는 함수는 반드시 er_set으로 오류를
설정했어야 한다. NO_ERROR를 반환하는 함수는 오류를 설정하지 않았거나
실수로 설정했다면 지웠어야 한다. ASSERT_ERROR_AND_SET은 방어적
변형이다. 호출자가 실패했지만 오류 컨텍스트가 비어 있으면 ER_FAILED로
폴백해 함수가 여전히 의미 있는 0이 아닌 반환값을 갖게 한다.
assert_release_* 계열은 두 가지 모드가 있어 흥미롭다.
// 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디버그 빌드에서는 평범한 assert다. 프로세스를 중단시킨다. 릴리스
빌드에서는 문자열화된 표현식을 담은 ER_FAILED_ASSERTION 알림(또는
오류)을 올리고 엔진은 계속 실행된다. 의도는 이렇다. 현장에서의
불변식 위반은 크게 기록되어야 하지만 반드시 치명적일 필요는 없다.
운영자가 로그 항목을 얻고 엔진은 다음 쿼리를 처리한다. assert_release는
알림 심각도를, assert_release_error는 오류 심각도를 기본으로 쓴다.
위반된 불변식이 정확성을 위협하는지 아니면 기대에 어긋나는지에
따라 선택한다.
스택 추적 / 호출 스택 캡처
섹션 제목: “스택 추적 / 호출 스택 캡처”엔진은 오류가 올라올 때 C 수준 호출 스택을 내보낼 수 있다. 구현은
stack_dump.c에 있다(나열된 소스에는 없지만 error_manager.c가
er_dump_call_stack으로 호출한다). Linux에서는 backtrace /
backtrace_symbols를 쓰고, 해석된 소스 파일 이름의 프로세스별
mht_t 테이블(fname_table)을 함께 사용한다. 형식이 원시 (0xADDR)가
아닌 function (file:line)이 되는 이유다. 해시 테이블은 er_init
시 er_call_stack_init이 초기화하고 er_final의 er_call_stack_final이
해제한다.
스택을 언제 덤프할지 제어하는 세 가지 노브가 있다.
PRM_ID_CALL_STACK_DUMP_ON_ERROR— 전역 켜기/끄기.PRM_ID_CALL_STACK_DUMP_ACTIVATION— 전역이 꺼져 있을 때 덤프에 참여하는 오류 코드 목록.PRM_ID_CALL_STACK_DUMP_DEACTIVATION— 전역이 켜져 있을 때 덤프에서 제외되는 오류 코드 목록.
er_call_stack_dump_on_error가 이 셋을 읽는다.
// 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 심각도는 무조건 덤프한다. 그 외의 경우 기본은 꺼짐이다. 운영자가
전역을 켜거나 특정 코드를 허용 목록에 올린다. er_print_callstack
사용자 호출 변형은 printf 형식 헤더와 함께 같은 일을 한다.
충돌 핸들러 er_print_crash_callstack(int sig)도 있다. SIGABRT /
SIGILL / SIGFPE / SIGBUS / SIGSEGV / SIGSYS에 설치된다.
Linux에서는 /proc/self/cmdline을 읽고 $CUBRID/log/coredump/로
chdir한 뒤 <cmdline>_<YYYYMMDDHHMMSS>.<ms>.coredump를 열어 프로세스
정보와 백트레이스를 기록하고 반환한다. OS 수준 코어 덤프가 억제되어
있을 때(ulimit 등)도 최소한의 아티팩트를 캡처하려는 것이다.
이벤트 핸들러
섹션 제목: “이벤트 핸들러”마지막 틈새는 이벤트 핸들러다. 엔진이 선택된 오류 이벤트를 외부
프로그램으로 파이프하는 기능이다. PRM_ID_EVENT_HANDLER 시스템
매개변수(셸 명령)로 설정되고 PRM_ID_EVENT_ACTIVATION(발동 오류
코드 목록)으로 걸러진다. er_event_init이 popen으로 명령을 열고
시작 배너를 기록한 뒤 FILE *을 er_Event_pipe에 저장한다. 각 오류가
PRM_ID_EVENT_ACTIVATION과 일치하면 er_notify_event_on_error가
er_event를 호출해 파이프에 한 줄 <err_id> <severity_string> <message>\n을
기록한다.
SIGPIPE는 까다롭다. 외부 프로그램이 파이프를 닫을 수 있기 때문이다.
엔진은 각 쓰기를 setjmp/longjmp로 보호하고 er_event_sigpipe_handler를
둔다. 닫힌 파이프는 치명적이지 않은 점프로 전환되어 파이프를 닫고
다시 열고 엔진을 계속 실행한다. error_manager.c에서 유일한
setjmp다.
flowchart LR
subgraph engine["임의의 서브시스템 (서버 스레드)"]
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 조회]
D -->|첫 번째| E[msgcat_message: cubrid.cat / set 1]
D --> F[er_estimate_size + reserve_message_area]
F --> G[er_vsprintf: msg_area에 렌더링]
G --> H{severity ≤ PRM_ER_LOG_LEVEL?}
H -- 예 --> I[er_log: 타임스탬프 줄\ncubrid_*.err 또는 stderr]
I --> J{호출 스택 덤프?}
J -- 예 --> K[er_dump_call_stack]
J -- 아니오 --> L
K --> L
L[er_notify_event_on_error] -->|PRM_EVENT_ACTIVATION 일치| M[er_Event_pipe에 fprintf]
L --> N{severity == FATAL?}
N -- 예 --> O[exit_ask: ABORT / EXIT / ASK / NEVER]
N -- 아니오 --> P[호출자에게 반환; 스레드 로컬 오류 유지]
H -- 아니오 --> P
P --> Q[호출자가 ER_FAILED 반환]
Q --> R[network_interface_sr이 er_get_area_error 패킹 → 응답]
R --> S[클라이언트가 er_set_area_error로 디코드]
S --> T[클라이언트 앱이 db_error_code / db_error_string 읽기]
소스 코드 가이드
섹션 제목: “소스 코드 가이드”이 절은 호출 흐름 그룹별로 심볼을 나열한다. 각 항목은 심볼 이름만 명시하며, 의도에 대한 산문 설명은 위의 CUBRID의 구현 절에 있다. 행 번호는 위치 힌트 표로 미룬다.
초기화와 종료 — error_manager.c
섹션 제목: “초기화와 종료 — error_manager.c”er_init (msglog_filename, exit_ask)— 주 진입점.er_Fmt_list[]를 초기화하고, 카탈로그에서er_Cached_msg[]를 채우고, 로그 파일을 열고, 싱글턴 컨텍스트를 등록(CS_MODE만)하고, 이벤트 핸들러를 설치한다.er_is_initialized ()—er_Hasalready_initiated를 보고한다.er_set_print_property (print_console)—er_Print_to_console을 전환한다.er_final (do_global_final)— 종료 처리. 로그 파일을 닫고, 캐시된 메시지를 해제하고, 형식 목록을 초기화 해제하고,er_event_final과er_call_stack_final을 호출한다.er_clear ()— 호출 스레드의 현재 오류 레벨을 지운다.er_clearid ()/er_setid (err_id)— 메시지나 심각도는 건드리지 않고 오류 id만 조작한다.
er_set 계열 — 공개 설정 API
섹션 제목: “er_set 계열 — 공개 설정 API”er_set (severity, file, line, err_id, num_args, ...)— 표준 진입점.er_set_with_file (..., FILE *fp, ...)— 같은 기능에 더해 파일 내용을 메시지와 함께 로그에 넣는다.er_set_with_oserror (...)— 같은 기능에 더해strerror(errno)를 덧붙인다.er_set_internal (severity, file, line, err_id, num_args, include_os_error, fp, ap_ptr)— 공통 구현.
형식 컴파일과 렌더링
섹션 제목: “형식 컴파일과 렌더링”er_find_fmt (err_id, num_args)—er_Fmt_list[-err_id]에서 지연 조회. 첫 미스 시msgcat_message와er_create_fmt_msg를 호출한다.er_create_fmt_msg (fmt, err_id, msg)— 형식 문자열을 복사하고er_study_fmt를 실행한다.er_study_fmt (fmt)— 형식에서%명세를 스캔해fmt->spec[]을 채운다.er_study_spec (conversion_spec, simple_spec, position, width, va_class)— 하나의%명세를 파싱해 소비한 문자 수와 위치/폭/ va_class를 반환한다.er_estimate_size (fmt, ap)— va_list가 주어졌을 때 렌더링된 메시지 길이의 상한을 구한다.er_vsprintf (er_entry_p, fmt, ap)— 인자를er_va_arg[]에 캡처한 뒤 형식을 훑어가며 명세별로sprintf를msg_area에 호출한다.er_init_fmt (fmt)/er_clear_fmt (fmt)/er_internal_msg (fmt, code, msg_num)— 형식별 초기화, 종료, 대체 메시지 교체.er_emergency (file, line, fmt, ...)—er_malloc이 실패했을 때를 위한 최소 sprintf.%s/%d만 이해하며 기존msg_buffer에 직접 기록한다.er_malloc_helper (size, file, line)—malloc을 감싸고 실패 시er_emergency를 호출한다.
스레드별 컨텍스트 — error_context.cpp
섹션 제목: “스레드별 컨텍스트 — error_context.cpp”er_message::er_message (logging)— 생성자.er_message::~er_message ()—clear_message_area와clear_args를 호출한다.er_message::swap (other)— SBO 대 힙 불변식을 보존하는 비단순 swap.er_message::clear_error ()/set_error (id, sev, file, line)— 소형 접근자.er_message::reserve_message_area (size)— 두 배씩 증가.er_message::clear_message_area ()/er_message::clear_args ()— 힙 버퍼 해제.context::context (auto_register, logging)— 생성자. 선택적으로 TLS로 등록한다.context::~context ()— 등록 해제 후 지운다.context::get_current_error_level ()—m_stack.top()또는m_base_level을 반환한다.context::register_thread_local ()/deregister_thread_local ()—tl_Context_p를 설정/해제한다.context::clear_current_error_level ()/clear_all_levels ()/clear_stack ()— 일괄 지우기 헬퍼.context::push_error_stack ()/pop_error_stack (popped)/pop_error_stack_and_destroy ()/has_error_stack ()— 스택 조작.context::get_thread_local_context ()/get_thread_local_error ()—SERVER_MODE에서cubthread::entry폴백을 포함한 TLS 접근자.
스택 저장/복원 (호출 가능 래퍼)
섹션 제목: “스택 저장/복원 (호출 가능 래퍼)”er_stack_push ()— 현재를 스택에 push(항상).er_stack_push_if_exists ()— 보호할 것이 있을 때만 push.er_stack_pop ()— 최상위 버리고 이전 복원.er_stack_pop_and_keep_error ()— 최상위를 팝하되 실제 오류가 있었으면 현재 레벨로 swap.er_restore_last_error ()—er_stack_push_if_exists의 대칭 짝.er_stack_clearall ()— 스택을 비우되 마지막 실제 오류를 보존.
로그 기록
섹션 제목: “로그 기록”er_log (err_id)—er_Fnlog[*]콜백. 로그 파일(메시지 vs. 접근)을 선택하고, 크기를 확인/회전하며,ER_LOG_MSG_WRAPPER_D로 형식화하고 기록한다. FATAL이면 선택적으로 stdin에 프롬프트를 띄운다.er_call_stack_dump_on_error (severity, err_id)— 세 가지 노브를 참고해 스택을 덤프한다.er_print_callstack (file, line, fmt, ...)— 사용자 호출 가능한 헤더 + 스택 덤프.er_print_crash_callstack (sig)— 신호 핸들러 변형.$CUBRID/log/coredump/에 기록한다.er_call_stack_init ()/er_call_stack_final ()/er_fname_free (key, data, args)— fname 해시 테이블 생명 주기.
로그 파일 배관
섹션 제목: “로그 파일 배관”er_get_msglog_filename ()— getter.er_set_access_log_filename ()— msglog 이름에서 접근 로그 이름을 파생한다.er_file_open (path)— 부모 디렉터리를 자동 생성하고 크기 초과 시 회전하는fopen.er_file_isa_null_device (path)—/dev/null/NUL을 인식한다.er_file_backup (fp, path)— 닫고,.bak으로 이름 바꾸고, 다시 연다.er_file_create_link_to_current_log_file (path, suffix)— Unix_latest심볼릭 링크.
와이어 프로토콜
섹션 제목: “와이어 프로토콜”er_get_area_error (buffer, length)— 현재 오류를 패킹된 버퍼로 평탄화한다.er_set_area_error (server_area)— 언패킹해 클라이언트 스레드에 주입한다.er_get_ermsg_from_area_error (buffer)— 이미 패킹된 버퍼 안의 메시지 텍스트 접근자.er_all (err_id, severity, n_levels, line_no, file_name, msg)— 현재 오류의 다중 출력 복사.er_msg ()/er_errid ()/er_errid_if_has_error ()/er_get_severity ()/er_has_error ()— 읽기 접근자.er_is_error_severity (severity)— FATAL / ERROR / SYNTAX를 true, WARNING / NOTIFICATION를 false.
이벤트 핸들러
섹션 제목: “이벤트 핸들러”er_event_init ()— 설정된 명령을popen하고 시작 배너를 기록한다.er_event ()—setjmp로 보호된 SIGPIPE 핸들러 아래에서 파이프에 한 줄을 기록한다.er_event_sigpipe_handler (sig)—_longjmp로 돌아간다.er_event_final ()— 종료 배너를 기록하고pclose한다.er_notify_event_on_error (err_id)—PRM_ID_EVENT_ACTIVATION으로 통제한다.er_register_log_handler (handler)— EID와 함께 오류당 발동하는 CS 측 콜백을 등록한다.er_event_restart ()— 런타임 재구성을 위한er_event_init의 공개 래퍼.
디버그 출력
섹션 제목: “디버그 출력”_er_log_debug (file, line, fmt, ...)— 공개 매크로 래퍼. 타임스탬프가 붙은 DEBUG 줄을 기록한다._er_log_debug_internal (file, line, fmt, ap)—er_print_callstack과 공유하는 본문.er_log_debug (...)매크로 —PRM_ID_ER_LOG_DEBUG로 통제된다.
보조 타입
섹션 제목: “보조 타입”er_severity열거형 — 다섯 단계.er_exit_ask열거형 —ER_NEVER_EXIT,ER_EXIT_ASK,ER_EXIT_DONT_ASK,ER_ABORT.er_print_option열거형 —ER_DO_NOT_PRINT,ER_PRINT_TO_CONSOLE.er_final_code열거형 —ER_THREAD_FINAL,ER_ALL_FINAL.ER_COPY_AREA(파일 정적) — 구형 클라이언트/서버 경로가 쓰는 레거시 평탄 영역 구조체.ER_FMT/ER_SPEC(파일 정적) —er_Fmt_list[]에 오류 코드당 하나씩 있는 컴파일된 형식 캐시 항목.er_message구조체(error_context.hpp) — 레벨별 오류 상태.cuberr::context— 스레드별 오류 컨테이너.cuberr::manager—er_init/er_final을 감싸는 RAII 래퍼 (ER_SAFE_INIT으로 사용).
카탈로그 — message_catalog.c
섹션 제목: “카탈로그 — message_catalog.c”msgcat_init ()— 세 가지 시스템 카탈로그를 연다.msgcat_final ()— 닫는다.msgcat_message (cat_id, set_id, msg_id)— 공개 조회. 로케일화된 문자열 또는 미스 시 빈 문자열을 반환한다.msgcat_open (name)— 로케일 해석 후 열기.LANG_NAME_DEFAULT로 폴백한다.msgcat_close (msg_catd)— 하나를 닫는다.msgcat_get_descriptor (cat_id)— 캐시된 디스크립터를 가져온다.msgcat_gets (msg_catd, set_id, msg_id, default)— 기본값 폴백과 함께 메시지 하나를 조회한다.msgcat_open_file (name)— 메시지 디렉터리에서 임의의 파일을 연다 (오류가 아닌 메시지 파일에 쓴다).cub_catopen (name, type)— NLSPATH를 인식하는 내부 열기.catopen(3)을 모방한다.cub_catgets (catd, set_id, msg_id, default)— 내부 이진 탐색 조회.cub_catclose (catd)—munmap하고 해제한다.load_msgcat (path)— 파일을mmap하고NLS_MAGIC헤더를 검증하고 디스크립터를 반환한다.nls_cat_hdr/nls_set_hdr/nls_msg_hdr— 디스크 상의 바이너리 구조체(빅엔디안)._nl_cat_d(cub_nl_catd로 typedef) — 인메모리 카탈로그 디스크립터.msgcat_def— cat_id를 파일 이름에 매핑하는msgcat_System[]테이블 항목.
오류 코드 공간 — error_code.h
섹션 제목: “오류 코드 공간 — error_code.h”NO_ERROR(0) — 성공 상수.ER_FAILED(-1) — 일반 실패.ER_GENERIC_ERROR/ER_OUT_OF_VIRTUAL_MEMORY/ER_INTERRUPTED(-2..-4) — 최상위 오류.ER_LK_*— 잠금 관리자(타임아웃, 교착 상태, 단방적 중단).ER_LOG_*— 복구 / WAL.ER_BO_*— 부트.ER_BTREE_*— B-트리.ER_PB_*— 페이지 버퍼.ER_HEAP_*— 힙 파일.ER_DISK_*— 디스크 관리자.ER_FILE_*— 파일 시스템 계층.ER_NET_*— 네트워크 / 연결.ER_AU_*— 인증.ER_SM_*— 스키마 관리자.ER_QPROC_*— 쿼리 처리.ER_TR_*— 트리거.ER_TM_*— 트랜잭션 관리자.ER_LDR_*— 벌크 로더.ER_MR_*— 매치 규칙(레거시).ER_FK_*— 외래 키.ER_TP_*— 타입 강제 변환.ER_LC_*— 로케이터.ER_IO_*— 원시 I/O.ER_CT_*— 카탈로그 관리자.ER_OBJ_*— 객체 관리자.ER_REGU_*— regu/표현식.ER_REG_*— 정규 표현식.ER_FAILED_ASSERTION— 릴리스 어설션 지원.ER_LAST_ERROR(-1371) — 경계값.er_Fmt_list[]배열 크기를 결정한다.
위치 힌트 (updated: 2026-05-01 기준)
섹션 제목: “위치 힌트 (updated: 2026-05-01 기준)”| 심볼 | 파일 | 행 |
|---|---|---|
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 |
소스 검증 노트
섹션 제목: “소스 검증 노트”이 문서의 구조적 그림 — er_set이 스레드 로컬 cuberr::context로
기본 er_message 또는 std::stack<er_message> 최상위에 기록하고,
형식은 mmap된 NetBSD/FreeBSD 형식의 cubrid.cat 카탈로그에서 지연
컴파일되어 er_Fmt_list[-err_id]에 보관되며, 와이어 형식은 세 개의
패킹된 OR_INT와 NUL 종료 메시지다 — 는 나열된 파일 버전 기준의
소스와 일치한다. 비공식 설명과의 마찰 지점 다섯 가지를 명시적으로
짚는다.
오류 스택은 중첩 오류를 위한 것이라는 해석. er_stack_push /
er_stack_pop을 예외 프레임을 push하는 방식으로 부르는 독해는 유추를
과장한다. 되감기(unwinding)는 없다. 이것은 수동 저장-복원 작업이다.
er_stack_push를 호출한 함수는 반드시 대응하는 er_stack_pop 계열
멤버를 호출해야 한다. 그렇지 않으면 프레임이 누출된다. deregister_thread_local의
소멸자에 있는 assert (m_stack.empty ())가 유일한 안전망인데, 이마저
defined (SERVER_MODE) 아래에 있다. CS_MODE에서는 주석에 “어차피
이미 늦었다”라고 적혀 있어 해당 보호 장치가 조용히 비활성화된다.
알림 심각도는 자동이다. er_set_internal은 severity == ER_NOTIFICATION_SEVERITY이고 현재 레벨에 실제 오류가 있을 때 스택
push/pop을 스스로 처리한다. 따라서 알림 심각도로 er_set을 호출하는
쪽은 er_stack_push/pop을 짝으로 감쌀 필요가 없다. 엔진이 알아서
한다. 새 코드에서 그런 호출 주변에 push/pop을 중복으로 넣어서는
안 된다.
er_set에 틀린 num_args를 넘기면 감지되며 조용히 신뢰하지 않는다.
er_find_fmt는 fmt->nspecs와 호출자의 num_args를 비교해 불일치 시
형식을 ER_ER_SUBSTITUTE_MSG로 교체한다. 렌더링된 메시지에는 “No
message available; original message format in error”가 나타난다. 이는
호출자가 잘못된 데이터를 넘겼다는 신호이지 카탈로그가 없다는 신호가
아니다. 두 진단 문자열(ER_ER_MISSING_MSG 대 ER_ER_SUBSTITUTE_MSG)을
혼동하는 것이 버그 리포트 소음의 잦은 원인이다.
오류 로그 파일 크기는 근사값이다. er_log는 현재 항목을 기록한
뒤 ftell(*log_fh) > PRM_ID_ER_LOG_SIZE를 읽고 임계값 초과 시 회전한다.
그래서 파일이 설정된 크기를 다소 넘긴 뒤 회전할 수 있다. 의도적이다.
임계값은 신호이지 한계가 아니며, 진행 중인 항목은 완전히 보존된다.
엄격한 크기 제한은 선점적 회전을 요구해 긴 스택 덤프 항목을 잘라낼
수 있다.
er_set_area_error는 로컬 로그 파이프를 실행한다. “서버가 이미
이 오류를 로그에 남겼으니 클라이언트 디코딩은 조용하다”는 흔한 오독이
있다. 실제로 클라이언트는 언패킹된 각 영역을 er_Fnlog[severity]로
er_log를 또 호출한다. 따라서 같은 논리적 오류가 서버의 원래
(file, line) 지점과 함께 서버 cubrid_*.err에, 그리고 (Unknown from server, -1)과 함께 클라이언트 cubrid_*.err에 각각 나타난다.
운영자는 접근 가능한 어느 쪽을 확인해도 된다.
er_emergency는 할당하지 않는다. 이 함수는 er_malloc 자체가
실패했을 때 호출된다. 이미 할당된 msg_buffer(256바이트 SBO)에 직접
기록하고 %s와 %d만 이해하며 er_log를 호출해 마지막 항목을 남긴다.
엔진의 마지막 탈출 경로이며 오류 관리자 코드 바깥에서 직접 호출해서는
안 된다.
미해결 질문
섹션 제목: “미해결 질문”나열된 소스 파일만으로는 확정되지 않아 다른 모듈과 교차 참조가 필요한 세부 사항들이다.
워커 디스패치 시 cubthread::entry는 m_error 컨텍스트를 어떻게
획득하는가? context::get_thread_local_context의 폴백은 tl_Context_p가
NULL일 때 cubthread::get_entry().get_error_context()를 통과한다.
엔트리가 태스크 진입 시 스스로를 TLS로 등록하는지, 아니면 전적으로
이 폴백에 의존하는지는 워커 풀 소스 — cubrid-thread-worker-pool.md의
cubthread::entry::register_thread_local 참조 — 에서 확인해야 한다.
클라이언트에서만 또는 서버에서만 올라가는 오류 코드가 있는가?
dbi_compat.h 미러는 모든 코드가 양쪽에서 보인다는 것을 시사한다.
그러나 ER_NET_*와 ER_OBJ_NO_CONNECT는 클라이언트에서만 의미가
있고, ER_LK_UNILATERALLY_ABORTED와 ER_PB_*는 서버에서만 의미가
있다. 와이어 경로는 서버가 설정한 것을 패킹하고 클라이언트는 코드 유효성
검사 없이 언패킹한다([ER_LAST_ERROR, ER_FAILED] 범위 검사만 한다).
오동작하는 서버가 원칙적으로 클라이언트 전용 코드를 클라이언트에 보낼
수 있는데 이것이 어떻게 처리되는지는 불명확하다.
장기 실행 서버의 boot_register_client 재로드 중 메시지 카탈로그
열기가 실패하면 어떻게 되는가? er_init은 고착성이 있다. 특정
파일 이름이 제공되면 재초기화하지 않는다. 그러나 브로커의 CAS 클라이언트는
boot_register_client 중 다른 파일 이름으로 er_init을 다시 호출하는데,
이는 고착성이 없다. 그 호출 안의 msgcat_init이 실패하면 er_emergency가
호출되고 함수가 ER_FAILED를 반환한다. 이후 동작 — CAS가 부분 상태로
계속 실행하는지 아니면 연결을 중단하는지 — 은 이 파일만으로는 보이지
않는다.
er_event_pipe SIGPIPE 핸들러는 프로세스 전역이다. er_event는
os_set_signal_handler로 SIGPIPE 핸들러를 설치하고 쓰기 후 복원한다.
두 서버 스레드가 이벤트 파이프에 동시에 쓰고 있다면(대부분의 경로는
er_Log_file_mutex로 직렬화되지만 엄밀히는 여기가 아니다), 저장/복원
핸들러 춤은 경쟁 상태를 만든다. 실질적 영향은 불명확하다. 이벤트
핸들러 기능은 운영 환경에서 드물게 활성화된다.
ER_FMT::fmt의 반드시 해제 플래그는 카탈로그 포인터 형식과 힙
복사 형식을 구분한다. er_internal_msg로 대체된 형식은 must_free = 0이고
fmt 포인터는 er_Cached_msg[]를 별칭한다(이는 init/final 생명 주기가
소유한다). er_create_fmt_msg로 빌드된 형식은 must_free = 1이고
카탈로그 문자열에서 malloc된 것이다. er_clear_fmt를 통한 인계는
올바르지만 취약하다. 플래그를 없애고 힙 복사로 통일하는 미래의 리팩터가
더 안전할 수 있다.
src/base/error_manager.c— 중앙 상태,er_set계열,er_log, 형식 컴파일/렌더링, 와이어 영역 패킹/언패킹, 이벤트 핸들러, 충돌 스택 기록자.src/base/error_manager.h— 공개 API: 심각도 열거형, 종료 정책 열거형,er_*선언,cuberr::manager, 편의 매크로(ERROR0..ERROR5,ERROR_SET_ERROR_*,ASSERT_ERROR*,assert_release_*).src/base/error_context.cpp—cuberr::er_message와cuberr::context구현. 스레드 로컬 포인터와 SBO 인식 swap 포함.src/base/error_context.hpp—er_va_arg,er_message,context인터페이스.src/base/error_code.h—NO_ERROR (0)과ER_FAILED (-1)부터ER_LAST_ERROR (-1371)까지 서브시스템 접두사로 정리된ER_*매크로 목록.src/base/message_catalog.c— NetBSD/FreeBSDnl_catd리더, 세 시스템 카탈로그(cubrid.cat,csql.cat,utils.cat),msgcat_message조회,mmap기반 로드.- 문서 내 상호 참조:
cubrid-thread-worker-pool.md— 스레드별 오류 컨텍스트가cubthread::entry::m_error멤버 안에 있다. 워커 디스패치와 엔트리 획득/반납이 컨텍스트 생명 주기를 관장한다.cubrid-network-protocol.md— 클라이언트와 서버 사이에 패킹된 오류 영역을 운반하는 요청/응답 패킷 구성.
- 교재 배경: Database System Concepts (Silberschatz, Korth, Sudarshan) — 스레드별, 구조화된, 로케일화된 오류의 필요성을 뒷받침하는 복구/동시성 제어 논거.