(KO) CUBRID PL 서버 브리지 — 두 PL 런타임이 함께 타고 있는 실행 중 콜백 채널
PL 패밀리에서의 위치
섹션 제목: “PL 패밀리에서의 위치”이 문서는 PL 패밀리의 세 번째 형제 이다. 다른 둘은
cubrid-pl-javasp.md (JavaSP) 와 cubrid-pl-plcsql.md
(PL/CSQL) 이다. 그 두 문서는 각자의 런타임을 부팅, 카탈로그,
디스패치, 언어별 실행까지 end-to-end 로 다룬다. 이 문서가 다루는
것은 두 런타임이 실행 중 데이터베이스로 콜백을 보낼 때 함께
타게 되는 공유 브리지 이다.
브리지를 두 런타임 문서에 접어 넣지 않고 별도 문서로 둔 이유는
간단하다. 브리지가 cub_pl 보다 오래되었다는 점이다.
같은 src/method/ 코드가 동작 중인 시스템에서 물리적으로 서로
다른 두 콜백 채널을 동시에 서비스하고 있으며, 모던 PL 런타임들은
그 메커니즘을 소유하지 않은 채 위에 얹혀 있을 뿐이다.
| 계층 | 어디에 있는가 |
|---|---|
와이어 분류 체계 (METHOD_REQUEST_*, METHOD_CALLBACK_*) | src/sp/sp_constants.hpp — 두 경로가 공유 |
Packed value/query/OID 구조체 (method_struct_*) | src/method/method_struct_*.{cpp,hpp} — 두 경로가 공유 |
Path A — cub_server→CAS, 레거시 C-메서드 스캔 | src/method/method_callback.{cpp,hpp}, src/method/query_method.cpp, src/method/method_scan.{cpp,hpp} |
Path B — cub_pl→cub_server, 모던 PL 호출 | src/sp/pl_executor.cpp (response_callback_command 디스패처); SP_CODE_INTERNAL_JDBC 봉투 안에 실려 운반된다 (cubrid-pl-javasp.md §Wire protocol 참조) |
| Path A 를 구동하는 서버측 메서드 스캔 | src/method/method_scan.{cpp,hpp} (cubscan::method::scanner); 그룹 추상은 src/sp/method_invoke_group.{cpp,hpp} |
| PL/CSQL 임베디드 SQL 을 위한 컴파일타임 브리지 | Path A 의 METHOD_CALLBACK_GET_SQL_SEMANTICS / _GET_GLOBAL_SEMANTICS; PL/CSQL 컴파일 경로가 임베디드 쿼리 검증을 위해 발사한다 (cubrid-pl-plcsql.md §C 측에 글로벌 시맨틱을 묻기 참조) |
cubrid-pl-javasp.md 는 호출당 시퀀스를 이미 고수준에서
명명해두고 있다 (SP_CODE_INVOKE →
METHOD_CALLBACK_QUERY_PREPARE →
METHOD_CALLBACK_QUERY_EXECUTE → SP_CODE_RESULT). 이 문서가
설명하는 것은 브리지 메커니즘 그 자체 이다. 각 오피코드가
무엇을 하는지, 디스패치 표가 어떻게 짜여 있는지, CAS 측 query
handler 가 무엇을 하는지, Path A 와 Path B 가 어디서 합쳐지는지.
이론적 배경
섹션 제목: “이론적 배경”실행 중 콜백 채널 이란, 데이터베이스 엔진이 자신의 실행 스레드가 아닌 런타임에서 도는 사용자 코드(저장 프로시저 본문, 사용자 정의 메서드, 커스텀 aggregate)가 SQL 쿼리, OID materialisation, 스키마 introspection, 메타데이터를 위해 다시 엔진 자체를 호출할 때 사용하는 IPC 메커니즘이다. 데이터베이스가 사용자 코드를 호스팅하는 어디에서나 등장하는 개념이다.
세 가지 디자인 선택지가 있다는 점이다:
-
In-process 언어 런타임. 사용자 코드가 서버 프로세스 안에서 돌면(PostgreSQL 의 PL/pgSQL, Oracle 의 PL/SQL, SQL Server 의 T-SQL), 콜백 은 직접 C-수준 호출이 된다. IPC 가 없고, 서버는 자기 쿼리 실행기를 함수로 노출하기만 하면 된다는 뜻이다. 대가는 실패 격리이다. 사용자 코드의 버그가 서버 상태를 망칠 수 있다.
-
외부 프로세스 언어 런타임. 사용자 코드가 별도 프로세스에서 돌면(DB2 fenced-mode SP, Oracle EJB, CUBRID 의 JavaSP + PL/CSQL), 엔진은 사이드카 프로세스를 fork(혹은 미리 띄워두고) 하고 호출을 IPC 채널로 전달한다. 대가는 반대 방향이다. 실패 격리는 회복되지만, SQL 이나 메타데이터를 위한 모든 콜백이 같은 채널에서의 동기 round-trip 이 되어버린다는 점이다.
-
메서드 스캔. 특별히 pre-OODB 시대의 패턴이다. SQL 쿼리 언어가 클래스 인스턴스 위에서 메서드를 호출하는 능력을 가지도록 확장되고, 실행기는 매 행의 메서드 호출을 다른 어딘가에 사는 런타임(보통 OOSQL 세션을 들고 있는 forked 클라이언트 프로세스)으로 디스패치해야 한다. 이것이 SP 콜백 채널의 pre-stored-procedure 조상이며, 모던 엔진은 보통 SP 머신이 새 워크로드를 가져간 뒤에도 호환성을 위해 이를 유지한다.
콜백 채널 디자인에서 가장 결과를 좌우하는 두 결정은 (a) 오피코드 분류 체계 — caller 가 실행 중에 무엇을 요청할 수 있게 허용할 것인가. 와 (b) 재귀 모델 — 한 콜백이 자기 콜백을 또 발사하는 다른 프로시저를 부를 수 있는가, nest 의 깊이를 얼마나 허용할 것인가이다.
CUBRID 의 브리지는 두 결정을 한 번 풀고, 그 풀이를 물리적으로
서로 다른 두 경로(Path A: server→CAS, Path B: cub_pl→server)에
같이 적용한다. 분류 체계는 sp_constants.hpp 에 있고, 재귀
한도는 두 경로에 공통으로 METHOD_MAX_RECURSION_DEPTH = 15 이다.
일반 DBMS 디자인
섹션 제목: “일반 DBMS 디자인”| 엔진 | 브리지 메커니즘 | 재귀 가드 | 콜백 오피코드 |
|---|---|---|---|
| PostgreSQL PL/pgSQL | In-process — SPI(Server Programming Interface). 함수 호출이 같은 백엔드 안에서 C-수준 SPI_execute()/SPI_cursor_* 호출이 된다. | max_stack_depth GUC 로 스택 깊이 검사(기본 2 MB). | SPI_execute, SPI_prepare, SPI_cursor_open, SPI_getvalue 등 — 오피코드가 아니라 직접 함수. |
| Oracle PL/SQL | In-process — PL/SQL VM 이 SQL 커널을 다시 부르는 SQL/FETCH 명령을 포함하는 오피코드들을 실행. | _PLSQL_OPTIMIZE_LEVEL 과 스택 오버플로 감지. | 내부 오피코드 — 노출되지 않는다는 점이다. |
| DB2 fenced-mode SP | 외부 — db2fmp (fenced-mode 프로세스)가 SP 를 호스트, 백채널은 Unix socket 과 prepare, open, fetch, close, execute, param 오피코드를 가진 작은 메시지 프로토콜. | MAX_NESTED_CALLS 레지스트리(기본 16). | CUBRID 의 METHOD_CALLBACK_* 집합과 비슷한 형태. |
| Oracle EJB / Java | 외부 — KPRB 드라이버(Kernel-Programmatic-Resident-in-Backend); JVM 안에서 발사된 JDBC 요청이 서버의 SQL 엔진으로 short-circuit 된다. | Java 스레드 스택 한도 + 재귀 카운터. | 표준 JDBC, 드라이버 수준에서 가로챔. |
| CUBRID PL 패밀리 (이 문서) | 외부 — cub_pl JVM, UDS 또는 TCP 위의 IPC. Path A (cub_server→CAS) 는 같은 오피코드 집합을 재사용하는 더 오래된 C-메서드-스캔 배관이다. | tran_get_libcas_depth() 로 적용되는 METHOD_MAX_RECURSION_DEPTH = 15. | METHOD_CALLBACK_* (약 18 개); Path A 에서는 METHOD_REQUEST_* 봉투; Path B 에서는 SP_CODE_INTERNAL_JDBC 봉투. |
CUBRID 디자인은 외부 프로세스 축에서 DB2 fenced-mode 와 Oracle
EJB 와 같은 군집에 든다. 차별화 포인트는 두 세대의 콜백 경로가
공존한다는 점이다. 클래스 인스턴스 위의 C 사용자 메서드를
위해 원래 쓰여진 오래된 server→CAS 채널(Path A), 그리고 JavaSP
와 PL/CSQL 이 서버 프로세스 밖으로 옮겨갔을 때 만들어진 새로운
cub_pl→server 채널(Path B). 두 경로는 같은 오피코드 집합과
packed 와이어 구조체를 공유한다는 점이다. Path B 가 가능한
한 기존 핸들러를 재사용하도록 설계되었기 때문이다.
CUBRID 의 접근
섹션 제목: “CUBRID 의 접근”두 콜백 경로
섹션 제목: “두 콜백 경로”flowchart LR
subgraph PathA["Path A — server → CAS (legacy C-method scan)"]
direction LR
SRV1["cub_server<br/>(query executor)"]
SCAN["cubscan::method::scanner<br/>(SCAN_TYPE_METHOD)"]
INV["cubmethod::method_invoke_group<br/>(per-call group)"]
CAS["cub_cas<br/>(CAS process)"]
DISP1["cubmethod::callback_handler<br/>::callback_dispatch"]
SRV1 --> SCAN --> INV
INV -- "METHOD_REQUEST_INVOKE<br/>METHOD_REQUEST_CALLBACK<br/>METHOD_REQUEST_ARG_PREPARE<br/>METHOD_REQUEST_END" --> CAS
CAS --> DISP1
DISP1 -- "METHOD_CALLBACK_QUERY_PREPARE<br/>METHOD_CALLBACK_OID_GET<br/>METHOD_CALLBACK_GET_SQL_SEMANTICS<br/>... 18 opcodes" --> SRV1
end
subgraph PathB["Path B — cub_pl → server (modern PL bridge)"]
direction LR
SRV2["cub_server<br/>(query executor)"]
PLEXEC["cubpl::executor<br/>(per-invocation)"]
PL["cub_pl<br/>(JVM)"]
DISP2["cubpl::executor<br/>::response_callback_command"]
SRV2 --> PLEXEC
PLEXEC -- "SP_CODE_INVOKE<br/>(invoke_java payload)" --> PL
PL -- "SP_CODE_INTERNAL_JDBC<br/>(carries METHOD_CALLBACK_*)" --> PLEXEC
PLEXEC --> DISP2
DISP2 -- "callback_prepare / _execute<br/>_fetch / _oid_get / _collection<br/>... 12 handlers" --> SRV2
end
두 경로는 서로 연결되어 있지 않다. 오피코드 상수, packed 구조체, 개념적 루프 패턴 을 공유하지만, 서로 다른 프로세스 쌍 사이에서 동작한다는 점이다:
- Path A 는
cub_server(엔진)와cub_cas(CAS 워커 프로세스 —cubrid-broker.md참조) 사이에 산다. 쿼리 실행기가SCAN_TYPE_METHOD— C 빌트인이나 본문이 CAS 측 세션에 있는 클래스 메서드로 디스패치되어야 하는 메서드 호출 표현식 — 를 마주쳤을 때 발동된다.cubscan::method::scanner(src/method/method_scan.cpp) 가cubmethod::method_invoke_group(src/sp/method_invoke_group.hpp) 를 감싸 구동한다. - Path B 는
cub_pl(JavaSP 와 PL/CSQL 을 호스팅하는 JVM —cubrid-pl-javasp.md§Process topology 참조)과cub_server사이에 산다. 저장 프로시저 본문이 SQL 쿼리, OID fetch, 스키마 lookup 을 발사하면 발동된다. 서버 측에서는cubpl::executor(src/sp/pl_executor.cpp) 가 구동하고, JVM 측 드라이버 클래스들(CUBRIDServerSideDriver,…PreparedStatement)이 와이어의 JVM 끝을 처리한다.
물리적으로는 비대칭이지만 디스패치 패턴은 동일하다는 점이다.
한 쪽이 요청 봉투(Path A 에서는 METHOD_REQUEST_*, Path B 에서는
SP_CODE_INTERNAL_JDBC)를 보내고, 받는 쪽은 봉투에서
METHOD_CALLBACK_* 오피코드를 디코드해 핸들러 함수로 디스패치하고,
그 핸들러는 서버 또는 CAS 측 머신을 실행한 다음 같은 채널로
응답을 큐잉해 돌려보낸다.
공유 오피코드 분류 체계
섹션 제목: “공유 오피코드 분류 체계”// sp_constants.hpp — 요청 봉투 (Path A 만)enum METHOD_REQUEST { METHOD_REQUEST_ARG_PREPARE = 0x40, METHOD_REQUEST_INVOKE = 0x01, METHOD_REQUEST_ERROR = 0x04, METHOD_REQUEST_CALLBACK = 0x08, METHOD_REQUEST_END = 0x20,
METHOD_REQUEST_COMPILE = 0x80, METHOD_REQUEST_SQL_SEMANTICS = 0xA0, METHOD_REQUEST_GLOBAL_SEMANTICS = 0xA1};// sp_constants.hpp — 콜백 오피코드 (공유, 두 경로)enum METHOD_CALLBACK_RESPONSE { METHOD_CALLBACK_END_TRANSACTION = 1, METHOD_CALLBACK_QUERY_PREPARE = 2, METHOD_CALLBACK_QUERY_EXECUTE = 3, METHOD_CALLBACK_GET_DB_PARAMETER = 4,
METHOD_CALLBACK_CURSOR = 7, METHOD_CALLBACK_FETCH = 8, METHOD_CALLBACK_GET_SCHEMA_INFO = 9,
METHOD_CALLBACK_OID_GET = 10, METHOD_CALLBACK_OID_PUT = 11, METHOD_CALLBACK_OID_CMD = 17, METHOD_CALLBACK_COLLECTION = 18,
METHOD_CALLBACK_NEXT_RESULT = 19, METHOD_CALLBACK_EXECUTE_BATCH = 20, METHOD_CALLBACK_EXECUTE_ARRAY = 21, METHOD_CALLBACK_CURSOR_UPDATE = 22,
METHOD_CALLBACK_MAKE_OUT_RS = 33, METHOD_CALLBACK_GET_GENERATED_KEYS = 34,
METHOD_CALLBACK_LOB_NEW = 35, METHOD_CALLBACK_LOB_WRITE = 36, METHOD_CALLBACK_LOB_READ = 37,
METHOD_CALLBACK_CURSOR_CLOSE = 42,
METHOD_CALLBACK_SET_PL_SESSION_PARAM = 50,
// COMPILE METHOD_CALLBACK_GET_SQL_SEMANTICS = 100, METHOD_CALLBACK_GET_GLOBAL_SEMANTICS = 101,
// AUTH METHOD_CALLBACK_CHANGE_RIGHTS = 200,
// CLASS ACCESS METHOD_CALLBACK_GET_CODE_ATTR = 201};두 열거형은 서로 다른 계층에 있다는 점이다:
METHOD_REQUEST_*는 외부 봉투 — 서버가 Path A 에서 CAS 에게 어떤 종류의 단계 를 요청하는지 말하는 데 쓴다. 인자 prepare, 빌트인 invoke, 콜백 요청 본문 전달, 호출-종료 신호, 또는 compile/semantics.METHOD_CALLBACK_*는 내부 오피코드 — 실제로 요청된 서비스(쿼리, OID, 스키마 등)를 기술한다는 점이다. 두 경로 모두 이 집합을 쓴다. Path A 에서는METHOD_REQUEST_CALLBACK이 첫 packed int 로 이 중 하나를 운반하고, Path B 에서는SP_CODE_INTERNAL_JDBC가 같은 봉투 역할을 한다.
숫자 범위는 의도적이다. 낮은 ID(1–22)는 JDBC 의미에서 물려받은 쿼리/커서 동작, 30 번대는 out-result 와 generated-key, 40/50 번대는 LOB / cursor-close / session-param, 100 번대는 PL/CSQL 전용 컴파일타임 도우미, 200 번대는 auth 와 class-access 이다. 빈 슬롯들(5, 6, 12–16, 23–32)은 이전 프로토콜 개정에 존재했다가 번호 재할당 없이 은퇴된 오피코드들의 흔적이다. 릴리스 간 상수 안정성을 지키기 위해서이다.
Path A — server → CAS (레거시 메서드 스캔 채널)
섹션 제목: “Path A — server → CAS (레거시 메서드 스캔 채널)”CAS 측 진입 함수는 하나뿐이다. method_dispatch
(src/method/query_method.cpp) 가 packed 요청을 받아
cubmethod::header ((uint64_t id, int command) 가 들어 있다)
를 벗겨내고, command 에 따라 네 핸들러 중 하나로 디스패치한다는
점이다.
// method_dispatch_internal — query_method.cppswitch (header.command) { case METHOD_REQUEST_ARG_PREPARE: // group_id 아래 DB_VALUE 인자 보관 error = method_prepare_arguments (unpacker); break; case METHOD_REQUEST_INVOKE: // 보관된 인자로 C 빌트인 호출 AU_SAVE_AND_ENABLE (save_auth); error = method_invoke_builtin (unpacker, value); AU_RESTORE (save_auth); break; case METHOD_REQUEST_CALLBACK: // CAS 세션으로 콜백 처리 AU_SAVE_AND_ENABLE (save_auth); error = cubmethod::get_callback_handler()->callback_dispatch (unpacker); AU_RESTORE (save_auth); break; case METHOD_REQUEST_END: // 명명된 query handler 들 free std::vector<int> handlers; unpacker.unpack_all (handlers); for (size_t i = 0; i < handlers.size (); i++) { cubmethod::get_callback_handler()->free_query_handle (handlers[i], false); } break;}네 봉투가 한 번의 메서드-스캔 호출의 lifecycle 을 이룬다는 점이다:
ARG_PREPARE. 서버가 메서드 그룹의id로 키 잡힌 per-row 인자 벡터를 패킹한다. CAS 가runtime_args[id] = args(std::unordered_map<UINT64, std::vector<DB_VALUE>>) 에 저장한다.INVOKE. 서버가 메서드를 기술하는pl_signature와 함께 실제 호출을 요청한다. CAS 가runtime_args[group_id]를 찾아 인스턴스 메서드를obj_send_array를 부르고,xs_send_queue (METHOD_SUCCESS, result)로 결과를 큐잉한다.CALLBACK(0 회 이상). C 메서드 본문이 어떤db_query_*SQL 이나 OID 호출을 실행했고, 그 호출이 서버로 돌아가, 서버가 요청을 패키지화해서METHOD_REQUEST_CALLBACK으로 돌려보낸다. CAS 는 내부METHOD_CALLBACK_*오피코드별로callback_handler::callback_dispatch로 디스패치한다.END. 서버가 CAS 에게 메서드 그룹이 끝났다고 알리고 free 할 query handler ID 들을 나열한다. CAS 가 각각에free_query_handle()를 부른다.
CAS 측 핸들러는 호출들 너머로 세션 단위 상태를 유지한다는 점이다:
// callback_handler — method_callback.hppclass EXPORT_IMPORT callback_handler { // ... std::multimap <std::string, int> m_sql_handler_map; // SQL -> handler id (statement cache) std::unordered_map <uint64_t, int> m_qid_handler_map; // query_id -> handler (out resultset) std::vector<query_handler *> m_query_handlers; // bounded slot table oid_handler * m_oid_handler; // OID materialisation cache std::queue <cubmem::extensible_block> m_data_queue; // packed responses pending xs_queue_send std::list <cubmethod::query_handler *> m_deferred_query_free_handler; error_context m_error_ctx;};m_query_handlers 는 고정 크기 배열(생성자 인자
max_query_handler 가 크기를 정한다)이고, 각 슬롯이 DB_SESSION
과 DB_QUERY_RESULT 를 감싼 query_handler 를 소유해 JDBC
PreparedStatement 의 lifetime 을 그대로 따른다. m_sql_handler_map
은 prepared-statement 캐시 이다. 같은 사용자가 같은 세션에
서 이미 prepare 한 SQL 문자열을 (현재 occupied 가 아닌
한도에서) METHOD_CALLBACK_QUERY_PREPARE 가 도착하면, 새 슬롯을
할당하는 대신 기존 핸들러를 재사용한다는 점이다:
// callback_handler::prepare — method_callback.cppquery_handler *handler = get_query_handler_by_sql (sql, [&] (query_handler *h) { return h->get_is_occupied() == false && (h->get_tran_id () == NULL_TRANID || h->get_tran_id() == tid) && h->get_user_name ().compare (au_get_current_user_name ()) == 0;});
if (handler == nullptr) { // not in cache: allocate a new slot and prepare handler = new_query_handler (); if (handler != nullptr) { int error = handler->prepare (sql, flag); // ... }}eligibility 술어가 세 가지 불변량을 강제한다는 점이다. 캐시된 핸들러가 free 여야 하고, 현재 트랜잭션 소속(혹은 트랜잭션 바인딩이 없어야)이고, 현재 사용자 소유여야 한다.
callback_dispatch 자체의 디스패치 표는 내부 오피코드에 대한
flat switch 이다:
// callback_dispatch — method_callback.cppswitch (code) { case METHOD_CALLBACK_END_TRANSACTION: error = end_transaction (unpacker); break; case METHOD_CALLBACK_QUERY_PREPARE: error = prepare (unpacker); break; case METHOD_CALLBACK_QUERY_EXECUTE: error = execute (unpacker); break; case METHOD_CALLBACK_OID_GET: error = oid_get (unpacker); break; case METHOD_CALLBACK_OID_PUT: error = oid_put (unpacker); break; case METHOD_CALLBACK_OID_CMD: error = oid_cmd (unpacker); break; case METHOD_CALLBACK_COLLECTION: error = collection_cmd (unpacker); break; case METHOD_CALLBACK_MAKE_OUT_RS: error = make_out_resultset (unpacker); break; case METHOD_CALLBACK_GET_GENERATED_KEYS: error = generated_keys (unpacker); break; case METHOD_CALLBACK_GET_SCHEMA_INFO: assert (false); break; // disabled case METHOD_CALLBACK_GET_SQL_SEMANTICS: error = get_sql_semantics (unpacker); break; case METHOD_CALLBACK_GET_GLOBAL_SEMANTICS: error = get_global_semantics (unpacker); break; case METHOD_CALLBACK_CHANGE_RIGHTS: error = change_rights (unpacker); break; default: assert (false); error = ER_FAILED;}#if defined (CS_MODE)xs_queue_send (); // flush queued responses to server#endif말미의 xs_queue_send() (CS_MODE 에서만)는 큐잉된 응답들을
핸들러마다가 아니라 한 transport_xs_* 패킷으로 모아 서버로
보낸다는 점이다. 핸들러들은 xs_pack_and_queue 로 응답을
큐에 넣고, 디스패처가 요청 끝에서 한 번에 flush 한다.
METHOD_CALLBACK_GET_SCHEMA_INFO 는 CAS 측에서 디스패치
사이트의 assert(false) 로 하드 비활성화되어 있다. 스키마 정보
자체는 여전히 method_schema_info.{cpp,hpp} 에 산다(PL/CSQL
컴파일 도우미 get_sql_semantics / get_global_semantics 가
직접 사용한다). 하지만 free-standing 서비스로서는 콜백 채널에서
은퇴되었다는 뜻이다.
Path B — cub_pl → server (모던 PL 브리지)
섹션 제목: “Path B — cub_pl → server (모던 PL 브리지)”Path B 는 cubrid-pl-javasp.md §Server-side JDBC back-channel
이 고수준에서 도입하는 그것이다. 디스패치 사이트 자체는
src/sp/pl_executor.cpp 에 산다는 점이다:
// executor::response_callback_command — pl_executor.cppint code;unpacker.unpack_int (code);switch (code) { case METHOD_CALLBACK_GET_DB_PARAMETER: error_code = callback_get_db_parameter (thread_ref, unpacker); break; case METHOD_CALLBACK_QUERY_PREPARE: error_code = callback_prepare (thread_ref, unpacker); break; case METHOD_CALLBACK_QUERY_EXECUTE: error_code = callback_execute (thread_ref, unpacker); break; case METHOD_CALLBACK_FETCH: error_code = callback_fetch (thread_ref, unpacker); break; case METHOD_CALLBACK_OID_GET: error_code = callback_oid_get (thread_ref, unpacker); break; case METHOD_CALLBACK_OID_PUT: error_code = callback_oid_put (thread_ref, unpacker); break; case METHOD_CALLBACK_OID_CMD: error_code = callback_oid_cmd (thread_ref, unpacker); break; case METHOD_CALLBACK_COLLECTION: error_code = callback_collection_cmd (thread_ref, unpacker); break; case METHOD_CALLBACK_MAKE_OUT_RS: error_code = callback_make_outresult (thread_ref, unpacker); break; case METHOD_CALLBACK_GET_GENERATED_KEYS: error_code = callback_get_generated_keys (thread_ref, unpacker); break; case METHOD_CALLBACK_END_TRANSACTION: error_code = callback_end_transaction (thread_ref, unpacker); break; case METHOD_CALLBACK_GET_CODE_ATTR: error_code = callback_get_code_attr (thread_ref, unpacker); break; case METHOD_CALLBACK_SET_PL_SESSION_PARAM: error_code = callback_set_pl_session_param (thread_ref, unpacker); break; default: assert (false); error_code = ER_FAILED;}오피코드 집합은 Path A 의 표의 엄격한 부분집합 이다. 12 개 핸들러 vs 14 개 핸들러. CAS 측 컴파일 도우미들
(GET_SQL_SEMANTICS, GET_GLOBAL_SEMANTICS, CHANGE_RIGHTS)
은 cub_pl 이 발사하지 않기 때문이다는 점이다. PL/CSQL
컴파일러는 Path A 로 서버 측에서 돌고, auth 변경은 실행 중
콜백이 아니라 DDL 로 흐른다. Path B 는 Path A 에 없는 두 오피코드를
추가하기는 한다. GET_DB_PARAMETER (격리 수준 + lock-wait +
client ID 를 돌려준다)와 GET_CODE_ATTR (저장 프로시저 본문의
카탈로그 행을 돌려준다. JVM 이 이름으로 다른 프로시저를 참조하는
프로시저의 코드를 로드할 때 사용)와 SET_PL_SESSION_PARAM
(DBMS_OUTPUT.ENABLE 같은 per-session JVM 측 플래그를 변형).
FETCH 는 여기서 별도 오피코드인데, Path B 가 prepared-statement
캐시 모델을 재사용하지 않고 커서 결과를 배치로 전달하기
때문이다.
핸들러 자체는 외부 클라이언트 쿼리가 사용할 같은 서버 측 머신
(db_compile_statement, db_execute_statement, xqmgr_*,
locator_get_class 등)으로 들어가, m_stack->send_data_to_java(blk)
로 packed 응답을 큐잉한다는 점이다. 각 핸들러는
pack_data_block(METHOD_RESPONSE_SUCCESS, ...) 또는
pack_data_block(METHOD_RESPONSE_ERROR, err, msg) 페이로드로
끝나며, Path A 의 xs_pack_and_queue 패턴을 그대로 따른다.
callback_get_db_parameter 가 가장 단순한 예이며 그 모양을
보여준다:
// executor::callback_get_db_parameter — pl_executor.cppdb_parameter_info *parameter_info = pl_session->get_db_parameter_info ();if (parameter_info == nullptr) { int tran_index = LOG_FIND_THREAD_TRAN_INDEX (m_stack->get_thread_entry()); parameter_info = new db_parameter_info (); parameter_info->tran_isolation = logtb_find_isolation (tran_index); parameter_info->wait_msec = logtb_find_wait_msecs (tran_index); logtb_get_client_ids (tran_index, ¶meter_info->client_ids); pl_session->set_db_parameter_info (parameter_info);}
cubmem::block blk = std::move (pack_data_block (METHOD_RESPONSE_SUCCESS, *parameter_info));if (blk.is_valid ()) { m_stack->send_data_to_java (blk); blk.freemem ();}결과는 pl_session 에 메모이즈 되어, 같은 SP 실행 안에서
이어지는 GET_DB_PARAMETER 콜백이 lookup 을 반복하지 않는다는
점이다. 다른 콜백 핸들러 대부분은 메모이즈하지 않고, 서버의
쿼리/OID 머신에 직접 위임하고 호출당 응답을 패킹한다.
PL/CSQL 임베디드 SQL 을 위한 컴파일타임 브리지
섹션 제목: “PL/CSQL 임베디드 SQL 을 위한 컴파일타임 브리지”PL/CSQL 은 JVM 안에서 파싱되고 컴파일된다는 점이다 (PlcParser.g4
→ PlcsqlSemantics → JavaCodeWriter → 인-프로세스 javac;
cubrid-pl-plcsql.md §“Compilation pipeline at CREATE PROCEDURE
time” 참조). 컴파일러는 모든 임베디드 SQL 문을 살아 있는 카탈로그
를 검증해야 한다. 컬럼 이름, 데이터 타입, 함수 오버로드.
하지만 자기 SQL 파서나 스키마 캐시가 있지는 않다.
브리지가 두 개의 컴파일타임 전용 콜백 오피코드로 이를 푼다. 실행 중인 어떤 쿼리와도 무관하게 Path A 로 흐른다는 점이다:
METHOD_CALLBACK_GET_SQL_SEMANTICS(100). 한 임베디드 문마다 보내진다. CAS 측 핸들러get_sql_semantics(method_callback.cpp안)가 SQL 을 파싱하고 시맨틱 검사를 돌리고, 실행 없이 구조화된 기술(컬럼 타입, 테이블 참조, 파라미터 placeholder)을 packed 형태로 돌려준다.METHOD_CALLBACK_GET_GLOBAL_SEMANTICS(101). 글로벌 심볼 lookup(함수 이름, 프로시저 이름, 타입)를 보내진다. CAS 측 핸들러get_global_semantics가 카탈로그를 해석하고 해석된 시그니처를 packed 형태로 돌려준다.
PL/CSQL 컴파일러는 시맨틱 분석 도중에 이들을 시퀀스로 발사한다.
각 round-trip 은 안쪽 오피코드가 위 둘 중 하나로 설정된 완전한
METHOD_REQUEST_CALLBACK 봉투이다. cubrid-pl-plcsql.md
§C 측에 글로벌 시맨틱을 묻기 가 JVM 측 caller 를 기술하고,
이 문서는 CAS 측 responder 를 다룬다.
컴파일타임 시맨틱 검사가 런타임 쿼리 실행과 같은 콜백 채널을 탄다는 사실은 의도된 것이다. PL/CSQL 이 컴파일과 실행 모두에 정확히 한 조각의 CAS 인프라(prepared-statement 캐시 + 카탈로그 액세스 경로)를 공유하게 해주며, 병렬 컴파일타임 RPC 를 유지할 필요가 없게 한다는 점이다.
메서드 스캔 연산자 (SCAN_TYPE_METHOD)
섹션 제목: “메서드 스캔 연산자 (SCAN_TYPE_METHOD)”서버 측에서 Path A 의 트리거 는 cubscan::method::scanner
(src/method/method_scan.cpp) — 스캔 매니저 디스패치 표
(cubrid-scan-manager.md 가 더 넓은 access-method 카탈로그를
다룬다)에서 SCAN_TYPE_METHOD 에 등록된 access method 이다.
scanner 의 행당 작업은 다음과 같다. 다음 메서드 호출 인자
집합을 상류 list-file 에서 끌어내어, 호출 시그니처를 감싸는
cubmethod::method_invoke_group 에 넘기고, 메서드별 반환값들을
실행기가 행으로 위로 흘려보낼 수 있는 qproc_db_value_list 로
모은다는 점이다.
// scanner::next_scan — method_scan.cppSCAN_CODE scan_code = S_SUCCESS;
next_value_array (vl); // prepare slot list for resultsscan_code = get_single_tuple (); // pull next row from upstream list-file
std::vector<std::reference_wrapper<DB_VALUE>> arg_wrapper (m_arg_vector, m_arg_vector + m_arg_count);
if (scan_code == S_SUCCESS && (error = m_method_group->execute (arg_wrapper)) != NO_ERROR) { scan_code = S_ERROR;}
if (scan_code == S_SUCCESS) { int num_methods = m_method_group->get_num_methods (); for (int i = 0; i < num_methods; i++) { DB_VALUE *dbval_p = (DB_VALUE *) db_private_alloc (m_thread_p, sizeof (DB_VALUE)); db_make_null (dbval_p); DB_VALUE &result = m_method_group->get_return_value (i); db_value_clone (&result, dbval_p); m_dbval_list[i].val = dbval_p; db_value_clear (&result); } m_method_group->reset (false);}m_method_group->execute(args) 가 Path A 의
METHOD_REQUEST_INVOKE 봉투를 CAS 로 발사하는 호출 사이트이다.
같은 method_invoke_group 은 obsolete 한 서버 측 상수 폴더
(xmethod_invoke_fold_constants — query_method.cpp 말미에서
#if 0-d-out 됨)도 사용한다. 활성 호출 사이트는 scanner 이다.
여기서 행당 비용 에 주목하자 — 각 next_scan() 호출이
그룹 안 메서드 하나마다 CAS 프로토콜을 통한 한 번의 동기
round-trip 을 만들어낸다는 점이다. 그래서 메서드 스캔은 비싸다고
문서화되어 있고(src/method/AGENTS.md 의 Gotchas 절 참조),
실행기는 hot path 에서 이를 도입하지 않는다.
재귀 가드와 tran_begin/end_libcas_function
섹션 제목: “재귀 가드와 tran_begin/end_libcas_function”두 경로 모두 무한 콜백 nesting 를 단단한 재귀 한도로 게이트 한다는 점이다:
// query_method.cpp — Path A 진입 (CAS 측)tran_begin_libcas_function ();int depth = tran_get_libcas_depth ();if (depth > METHOD_MAX_RECURSION_DEPTH) { // sp_constants.hpp 의 15 er_set (ER_ERROR_SEVERITY, ARG_FILE_LINE, ER_SP_TOO_MANY_NESTED_CALL, 0); error = ER_SP_TOO_MANY_NESTED_CALL;}// ... handle dispatch ...tran_end_libcas_function ();tran_begin_libcas_function / tran_end_libcas_function 쌍
(transaction_cl.h 에 선언)이 트랜잭션에 scoped 된 libcas-depth
카운터 를 증감시킨다. 카운터가 의미하는 바는 “이 트랜잭션 안에
서 콜백 사슬 안에 얼마나 깊이 들어가 있는가” 이다. 15 를 넘으면
호출이 ER_SP_TOO_MANY_NESTED_CALL 로 거절된다.
쌍이 중요한 이유는, 콜백이 또 다른 메서드 스캔을 발사하는 SQL
문을 실행할 수 있기 때문이다. 그 nested call 은 0 에서 시작하지
않고 depth == current + 1 을 봐야 한다는 점이다. 모든 디스패치
를 begin/end 로 bracketing 함으로써, 스택이 프로세스를 가로
지를 때조차 카운터가 정확하게 유지된다.
같은 ER_SP_TOO_MANY_NESTED_CALL 가 Path B 에서도
cubpl::executor::request_invoke_command 로부터 — SP nest 가
이미 한도에 있다면 — 발생한다. 두 경로는 카운터가 아니라 상수를
공유한다. Path A 는 libcas depth 를 쓰고, Path B 는 PL session
의 stack-map 크기를 쓴다는 점이다.
Authorization 핸드오프
섹션 제목: “Authorization 핸드오프”Path A 는 INVOKE 와 CALLBACK 봉투의 디스패치 동안 무조건
AU_SAVE_AND_ENABLE 로 전환했다가 이후 AU_RESTORE 한다(위
method_dispatch_internal switch 참조). 이렇게 함으로써 메서드
들이, 호출자의 주변 컨텍스트가 일시적으로 auth 가 비활성화된
상태(예: 시스템 내부 쿼리)에 있었더라도, auth 를 켠 채로 (즉,
호출 사용자가 볼 수 있는 것만 볼 수 있게) 동작한다.
callback_handler::change_rights (METHOD_CALLBACK_CHANGE_RIGHTS,
오피코드 200) 는 메서드 호출 권한을 owner (METHOD_AUTH_OWNER)
와 invoker (METHOD_AUTH_INVOKER) 모드 사이에서 전환하는
명시적인 오피코드 — Oracle 의 AUTHID DEFINER / AUTHID CURRENT_USER 와 같은 구분이다. CUBRID 의 PL/CSQL 문법이 컴파일
타임에 발사되는 change_rights 콜백으로 매핑된다는 점이다.
Packed 와이어 구조체 (method_struct_*)
섹션 제목: “Packed 와이어 구조체 (method_struct_*)”두 경로는 src/method/method_struct_*.{cpp,hpp} 에 있는
cubpacking::packable_object 서브클래스 패밀리를 공유해서
도메인 페이로드를 직렬화한다는 점이다:
| 헤더 | 소스 | 내용 |
|---|---|---|
cubmethod::header | method_struct_invoke.hpp | (uint64_t id, int command) — Path A 의 외부 봉투 |
cubmethod::prepare_args | method_struct_invoke.hpp | group_id, tran_id, METHOD_TYPE, 인자 벡터 — METHOD_REQUEST_ARG_PREPARE 의 페이로드 |
cubmethod::query_handler_info 등 | method_struct_query.{cpp,hpp} | Prepare/execute 요청과 응답 페이로드 |
cubmethod::oid_get_info 등 | method_struct_oid_info.{cpp,hpp} | OID get/put/cmd 페이로드 |
cubmethod::schema_info_* | method_struct_schema_info.{cpp,hpp} | 컬럼/테이블 디스크립터 구조체 |
cubmethod::dbvalue_packing | method_struct_value.{cpp,hpp} | 브리지에 맞춰진 DB_VALUE 직렬화 (VOBJ → object, OID → object fixup 처리) |
각 구조체가 표준 cubpacking::packer/unpacker 를 pack,
unpack, get_packed_size 를 구현한다. 두 경로가 이 같은 구조체
패밀리를 사용하기 때문에 cross-path 오피코드 공유가 가능하다는
점이다. 와이어 형식이 IPC 채널을 가로질러 portable 하기 때문이다.
SP 호출당 정리
섹션 제목: “SP 호출당 정리”호출 사슬이 종료되면(원래 메서드 스캔이 모든 행을 끝내거나, SP
호출이 반환), Path A 의 METHOD_REQUEST_END 봉투 또는 Path B
의 동등한 호출-종료가 CAS 측 callback_handler (Path A) 또는
서버 측 cubpl::executor (Path B) 가 호출당 상태를 free 하게
한다는 점이다:
- 호출 도중 사용된 모든 query handler 가 슬롯 표로 반환된다
(
free_query_handle). SQL→handler 캐시 항목은 남아 있어, 같은 SQL 을 발사하는 이어지는 호출이 prepared statement 를 재사용할 수 있다. - OID handler 가 호출당 materialisation 캐시를 비운다.
m_data_queue가 비워진다.- Path A 에서는
free_deferred_query_handler가 호출 중에 일어 나지 못했던(예: 결과셋 디스크립터가 아직 들고 있는 결과들) 큐잉 free 들을 실행한다.
free_query_handle_all(true) 는 CAS 세션의 가장 마지막에 (호출당
이 아니라) 호출되어, 캐시된 SQL handler map 까지 포함한 모든 것을
강제로 release 한다. 보통 캐시된 항목을 보호하는 재사용 로직을
우회한다는 뜻이다.
소스 워크스루
섹션 제목: “소스 워크스루”CAS 측 (src/method/) — Path A 핸들러
섹션 제목: “CAS 측 (src/method/) — Path A 핸들러”| 심볼 | 역할 |
|---|---|
method_dispatch | query_method.cpp 의 CAS 진입; tran_begin/end_libcas_function 으로 bracket, 재귀 깊이 검사, method_dispatch_internal 호출 |
method_dispatch_internal | METHOD_REQUEST_* 의 switch; 인자 prep / 빌트인 invoke / 콜백 디스패치 / end 로 디스패치 |
method_invoke_builtin | runtime_args[group_id] 를 읽고, obj_send_array 로 C 빌트인 호출, xs_send_queue (METHOD_SUCCESS, result) 로 결과 큐잉 |
method_prepare_arguments | 다음 INVOKE 를 위해 그룹 ID 아래 per-row DB_VALUE 인자 저장 |
method_set_runtime_arguments / _erase_runtime_arguments | VOBJ → object fixup (method_fixup_vobjs) 을 가진 args map mutator |
method_fixup_vobjs / _fixup_set_vobjs / _has_set_vobjs | OID/VOBJ 값을 사용자 메서드 본문에 넘기기 전에 materialise 된 객체로 변환 |
method_error | 디스패치 시작 전에 CAS 가 에러를 감지했을 때 서버로 METHOD_ERROR 전송 |
cubmethod::callback_handler | per-CAS-세션 상태 — query handler, OID handler, SQL→handler 캐시, 데이터 큐, error context |
callback_handler::callback_dispatch | METHOD_CALLBACK_* 의 switch; 마지막에 xs_queue_send 로 큐잉된 응답 flush |
callback_handler::prepare / execute / end_transaction / make_out_resultset / generated_keys | 쿼리 관련 핸들러 |
callback_handler::oid_get / oid_put / oid_cmd / collection_cmd | OID 핸들러 |
callback_handler::get_sql_semantics / get_global_semantics | PL/CSQL 컴파일러가 사용하는 컴파일타임 시맨틱 검사 핸들러 |
callback_handler::change_rights | owner/invoker 모드를 위한 auth 오피코드 |
callback_handler::new_query_handler / free_query_handle / free_query_handle_all / get_query_handler_by_* | 슬롯 표와 SQL 캐시 관리 |
cubmethod::oid_handler | OID 핸들러가 사용하는 OID materialisation 캐시 |
cubmethod::query_handler | 한 prepared-statement 슬롯을 위해 DB_SESSION + DB_QUERY_RESULT 를 감쌈 |
cubmethod::header, prepare_args | 외부 봉투 구조체 |
cubmethod::dbvalue_packing | VOBJ fixup 을 가진 DB_VALUE pack/unpack |
cubmethod::schema_info_* | 컬럼/테이블 디스크립터 타입과 도우미 |
서버 측 (src/sp/pl_executor.cpp) — Path B 핸들러
섹션 제목: “서버 측 (src/sp/pl_executor.cpp) — Path B 핸들러”| 심볼 | 역할 |
|---|---|
cubpl::executor::request_invoke_command | invoke_java 페이로드를 패킹하고 풀에서 claim 한 연결로 SP_CODE_INVOKE 전송 |
cubpl::executor::response_invoke_command | 응답을 읽는 루프; 결과, 에러, SP_CODE_INTERNAL_JDBC 봉투를 라우팅 |
cubpl::executor::response_callback_command | METHOD_CALLBACK_* 의 switch; 오피코드별 핸들러로 디스패치 |
callback_get_db_parameter | 트랜잭션 격리, lock-wait, client ID 를 돌려줌; pl_session 에 메모이즈 |
callback_prepare | 서버 측 쿼리 prepare; prepared-statement handle ID 반환 |
callback_execute | prepared statement 실행, query ID 반환 |
callback_fetch | query ID 로 커서 배치 fetch |
callback_oid_get / _put / _cmd | OID materialisation, mutation, class/instance 명령 |
callback_collection_cmd | Set/multiset/sequence 동작 |
callback_make_outresult | 쿼리 결과를 SP 반환을 위한 out 파라미터로 승격 |
callback_get_generated_keys | 마지막 INSERT 의 자동 생성 키 반환 |
callback_end_transaction | JVM 측 JDBC 가 트리거한 서버 측 commit/abort |
callback_get_code_attr | 저장 프로시저의 카탈로그 행 속성 반환 — JVM 이 참조된 SP 코드를 로드할 때 사용 |
callback_set_pl_session_param | per-session JVM 측 파라미터(예: DBMS_OUTPUT.ENABLE) 변형 |
서버 스캔 측 — Path A 트리거
섹션 제목: “서버 스캔 측 — Path A 트리거”| 심볼 | 파일 | 역할 |
|---|---|---|
cubscan::method::scanner | src/method/method_scan.{cpp,hpp} | SCAN_TYPE_METHOD access method; 행당 메서드 호출 |
scanner::open / close / next_scan / init / clear | src/method/method_scan.cpp | 표준 SCAN_ID lifecycle 와 메서드 그룹 바인딩 |
scanner::get_single_tuple / next_value_array | src/method/method_scan.cpp | 상류 list-file 에서 인자 추출, 결과 슬롯 준비 |
cubmethod::method_invoke_group | src/sp/method_invoke_group.{cpp,hpp} | pl_signature_array 를 감쌈; METHOD_REQUEST_INVOKE 를 발사하는 호출당 객체 |
method_invoke_group::execute / prepare / begin / end / reset | src/sp/method_invoke_group.cpp | 메서드 요청을 CAS 로 보내는 실제 호출 사이트 |
위치 힌트 (2026-05-05 기준)
섹션 제목: “위치 힌트 (2026-05-05 기준)”| 심볼 | 경로 |
|---|---|
method_dispatch | src/method/query_method.cpp:113 |
method_dispatch_internal | src/method/query_method.cpp:201 |
method_invoke_builtin | src/method/query_method.cpp:253 |
method_invoke_builtin_internal | src/method/query_method.cpp:341 |
method_prepare_arguments | src/method/query_method.cpp:286 |
method_fixup_vobjs | src/method/query_method.cpp:541 |
cubmethod::callback_handler (class) | src/method/method_callback.hpp:58 |
callback_handler::callback_dispatch | src/method/method_callback.cpp:68 |
callback_handler::end_transaction | src/method/method_callback.cpp:138 |
callback_handler::prepare | src/method/method_callback.cpp:174 |
cubmethod::header (struct) | src/method/method_struct_invoke.hpp:45 |
cubmethod::prepare_args (struct) | src/method/method_struct_invoke.hpp:62 |
METHOD_REQUEST (enum) | src/sp/sp_constants.hpp:184 |
METHOD_CALLBACK_RESPONSE (enum) | src/sp/sp_constants.hpp:203 |
METHOD_MAX_RECURSION_DEPTH (#define 15) | src/sp/sp_constants.hpp:160 |
METHOD_TYPE (enum) | src/sp/sp_constants.hpp:169 |
cubpl::executor::response_callback_command | src/sp/pl_executor.cpp:511 |
executor::callback_get_db_parameter | src/sp/pl_executor.cpp:606 |
executor::callback_prepare | src/sp/pl_executor.cpp:651 |
cubscan::method::scanner::next_scan | src/method/method_scan.cpp:173 |
cubmethod::method_invoke_group (class) | src/sp/method_invoke_group.hpp:66 |
심볼 이름이 정규 anchor 이고, 라인 번호는 updated: 날짜에
스코프된 힌트이다.
Cross-check 노트
섹션 제목: “Cross-check 노트”- 두 콜백 경로, 한 분류 체계. 오피코드 표는 공유되지만
(
sp_constants.hpp), 디스패치 구현은 분리되어 있다 (Path A 는method_callback.cpp, Path B 는pl_executor.cpp). 논의할 때 콜백 채널을 한 가지로 묶어 다루는 것은 함정이다.METHOD_CALLBACK_QUERY_PREPARE의미를 변경하면 두 디스패처 모두에서 변경하고 JVM 측 드라이버와의 와이어 호환성 가정을 체크해야 한다는 점이다. METHOD_CALLBACK_GET_SCHEMA_INFO는 CAS 측에서 하드 비활성화 되어 있다 (디스패치 사이트에서assert (false)). 하지만 지원 코드 (method_schema_info.{cpp,hpp},method_struct_schema_info.*) 는 여전히 컴파일된다. 같은 타입들이 컴파일타임 도우미들이 사용하기 때문이다. 의도된 것 이지만 dead code 로 오해하기 쉽다.xmethod_invoke_fold_constants는query_method.cpp말미 에서#if 0-d-out 되어 있고 현재 호출되지 않는다는 점이다. 메서드 호출의 서버 측 상수 폴더였다. 활성 호출 사이트는cubscan::method::scanner::next_scan이다.- 재귀 가드는 경로별로 다르다. Path A 는
tran_get_libcas_depth()를METHOD_MAX_RECURSION_DEPTH = 15와 비교하고, Path B 는 PL 세션의m_stack_map크기를 같은 상수와 비교한다. 둘 다ER_SP_TOO_MANY_NESTED_CALL을 발생 시킨다. 상수는 공유되지만 카운터는 독립적이다. 한 사용자 트랜잭션이 어느 한 경로에서도 15 콜백을 넘을 수 없지만, 두 경로를 번갈아가면 이론적으로 총 30 콜백에 도달할 수 있다. 현재 어떤 테스트에서도 관찰되지는 않는다. - Auth 토글링은 비대칭이다. Path A 는
INVOKE와CALLBACK디스패치를 무조건AU_SAVE_AND_ENABLE/AU_RESTORE로 감싼다. Path B 의callback_*핸들러는 스스로 토글하지 않는다. 이전 단계에서pl_executor가 SP 호출 setup 에서 정해둔 서버 워커 스레드의 auth 상태를 그대로 물려받는다. - Statement-cache 재사용 술어는 엄격하다.
query_handler는(occupied == false) && (tran_id 일치 또는 NULL_TRANID) && (현재 사용자 일치)일 때만 재사용된다. 즉, 같은 SQL 을 prepare 하는 병렬 재귀 콜백은 외부 콜백과 핸들러를 공유하지 않으며 (occupied 검사), CAS 세션 안의 다른 사용자도 공유 하지 않으며 (사용자 검사), cross-tran 재사용은 트랜잭션에 아직 바인딩되지 않은 핸들러에 대해서만 작동한다는 점이다. 엄격함이 동시 콜백 사슬 사이에서 결과셋 얽힘을 방지한다.
열린 질문
섹션 제목: “열린 질문”- Path A 은퇴 일정. JavaSP 와 PL/CSQL 둘 다 Path B 에 있고,
obsolete 한 서버 측 상수 폴더는
#if 0-d-out 되어 있다는 점에서, Path A 의 유일한 production 구동자는SCAN_TYPE_METHOD로 발사되는 C 빌트인 메서드만 남은 것 같다. 그것들도 은퇴 된다면 CAS 측callback_handler전체가 dead code 가 된다는 뜻이다. 별도 audit 가 필요한 지점이다. - Cross-path 카운터 통합. libcas-depth 와 PL-session-stack
카운터를 하나의
THREAD_ENTRY-scoped 깊이 카운터로 통합할 것인가. 현재 split 은 어느 한 경로도 안 넘는데도 총합으로 15 를 넘는 병적인 mixed-path nesting 을 허용한다. 현재 관찰 되지 않지만 막혀 있지도 않다. - JVM 측 디스패치 표.
CUBRIDServerSidePreparedStatement등의 Java 측이METHOD_CALLBACK_*오피코드로 변환되는 부분은pl_engine/pl_server/에 살고 여기서 다루지 않는다. 짧은 follow-up 절(또는cubrid-pl-javasp.md의 확장)이 JVM 측 오피코드 생성을 문서화할 수 있다. - 컴파일타임 시맨틱 검사 에러 보고.
get_sql_semantics와get_global_semantics는 런타임 콜백과 같은 방식으로 에러를 packed 형태로 돌려준다. 하지만 PL/CSQL 컴파일러는 이를 (원래 PL/CSQL 소스의 라인/컬럼이 붙은) 파서 수준 에러로 다룬다는 점이다. End-to-end 에러 경로 매핑은 완전히 문서화되어 있지 않다.cubrid-pl-plcsql.md의 열린 질문 참조.
Sources
섹션 제목: “Sources”src/method/method_callback.{cpp,hpp}— CAS 측 콜백 handlersrc/method/query_method.cpp— CAS 측 디스패치 진입 (method_dispatch,method_dispatch_internal)src/method/method_scan.{cpp,hpp}— Path A 를 구동하는 서버 측SCAN_TYPE_METHODscannersrc/method/method_struct_invoke.{cpp,hpp}— 외부 봉투 구조체 (header,prepare_args)src/method/method_struct_value.{cpp,hpp}— VOBJ/OID fixup 을 가진DB_VALUE패킹src/method/method_struct_query.{cpp,hpp},method_struct_oid_info.{cpp,hpp},method_struct_schema_info.{cpp,hpp}— 페이로드 타입src/method/method_query_handler.{cpp,hpp},method_oid_handler.{cpp,hpp},method_schema_info.{cpp,hpp}— CAS 측 자원별 handlersrc/sp/sp_constants.hpp— 요청과 콜백 오피코드 enum, 재귀 한도, 메서드 타입src/sp/pl_executor.cpp— Path B 디스패처 (response_callback_command) 와 12 개 핸들러src/sp/method_invoke_group.{cpp,hpp}—METHOD_REQUEST_INVOKE를 보내는 서버 측 그룹 추상src/method/AGENTS.md—src/method/agent 가이드; per-row 메서드 호출 비용에 대한 gotcha- 인접 문서들:
cubrid-pl-javasp.md(JavaSP 런타임, Path B 의SP_CODE_INTERNAL_JDBC봉투와 JVM 측CUBRIDServerSideDriver포함),cubrid-pl-plcsql.md(GET_SQL_SEMANTICS/GET_GLOBAL_SEMANTICS요청을 발사하는 PL/CSQL 컴파일 파이프 라인),cubrid-overview-pl-language.md(서브카테고리 router),cubrid-scan-manager.md(access-method 카탈로그 안의S_METHOD스캔 타입)