콘텐츠로 이동

(KO) CUBRID 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_plcub_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_INVOKEMETHOD_CALLBACK_QUERY_PREPAREMETHOD_CALLBACK_QUERY_EXECUTESP_CODE_RESULT). 이 문서가 설명하는 것은 브리지 메커니즘 그 자체 이다. 각 오피코드가 무엇을 하는지, 디스패치 표가 어떻게 짜여 있는지, CAS 측 query handler 가 무엇을 하는지, Path A 와 Path B 가 어디서 합쳐지는지.

실행 중 콜백 채널 이란, 데이터베이스 엔진이 자신의 실행 스레드가 아닌 런타임에서 도는 사용자 코드(저장 프로시저 본문, 사용자 정의 메서드, 커스텀 aggregate)가 SQL 쿼리, OID materialisation, 스키마 introspection, 메타데이터를 위해 다시 엔진 자체를 호출할 때 사용하는 IPC 메커니즘이다. 데이터베이스가 사용자 코드를 호스팅하는 어디에서나 등장하는 개념이다.

세 가지 디자인 선택지가 있다는 점이다:

  1. In-process 언어 런타임. 사용자 코드가 서버 프로세스 안에서 돌면(PostgreSQL 의 PL/pgSQL, Oracle 의 PL/SQL, SQL Server 의 T-SQL), 콜백 은 직접 C-수준 호출이 된다. IPC 가 없고, 서버는 자기 쿼리 실행기를 함수로 노출하기만 하면 된다는 뜻이다. 대가는 실패 격리이다. 사용자 코드의 버그가 서버 상태를 망칠 수 있다.

  2. 외부 프로세스 언어 런타임. 사용자 코드가 별도 프로세스에서 돌면(DB2 fenced-mode SP, Oracle EJB, CUBRID 의 JavaSP + PL/CSQL), 엔진은 사이드카 프로세스를 fork(혹은 미리 띄워두고) 하고 호출을 IPC 채널로 전달한다. 대가는 반대 방향이다. 실패 격리는 회복되지만, SQL 이나 메타데이터를 위한 모든 콜백이 같은 채널에서의 동기 round-trip 이 되어버린다는 점이다.

  3. 메서드 스캔. 특별히 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 이다.

엔진브리지 메커니즘재귀 가드콜백 오피코드
PostgreSQL PL/pgSQLIn-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/SQLIn-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 가 가능한 한 기존 핸들러를 재사용하도록 설계되었기 때문이다.

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.cpp
switch (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 을 이룬다는 점이다:

  1. ARG_PREPARE. 서버가 메서드 그룹의 id 로 키 잡힌 per-row 인자 벡터를 패킹한다. CAS 가 runtime_args[id] = args (std::unordered_map<UINT64, std::vector<DB_VALUE>>) 에 저장한다.
  2. INVOKE. 서버가 메서드를 기술하는 pl_signature 와 함께 실제 호출을 요청한다. CAS 가 runtime_args[group_id] 를 찾아 인스턴스 메서드를 obj_send_array 를 부르고, xs_send_queue (METHOD_SUCCESS, result) 로 결과를 큐잉한다.
  3. CALLBACK (0 회 이상). C 메서드 본문이 어떤 db_query_* SQL 이나 OID 호출을 실행했고, 그 호출이 서버로 돌아가, 서버가 요청을 패키지화해서 METHOD_REQUEST_CALLBACK 으로 돌려보낸다. CAS 는 내부 METHOD_CALLBACK_* 오피코드별로 callback_handler::callback_dispatch 로 디스패치한다.
  4. END. 서버가 CAS 에게 메서드 그룹이 끝났다고 알리고 free 할 query handler ID 들을 나열한다. CAS 가 각각에 free_query_handle() 를 부른다.

CAS 측 핸들러는 호출들 너머로 세션 단위 상태를 유지한다는 점이다:

// callback_handler — method_callback.hpp
class 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_SESSIONDB_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.cpp
query_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.cpp
switch (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.cpp
int 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.cpp
db_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, &parameter_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.g4PlcsqlSemanticsJavaCodeWriter → 인-프로세스 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.cpp
SCAN_CODE scan_code = S_SUCCESS;
next_value_array (vl); // prepare slot list for results
scan_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_constantsquery_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 크기를 쓴다는 점이다.

Path A 는 INVOKECALLBACK 봉투의 디스패치 동안 무조건 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::headermethod_struct_invoke.hpp(uint64_t id, int command) — Path A 의 외부 봉투
cubmethod::prepare_argsmethod_struct_invoke.hppgroup_id, tran_id, METHOD_TYPE, 인자 벡터 — METHOD_REQUEST_ARG_PREPARE 의 페이로드
cubmethod::query_handler_infomethod_struct_query.{cpp,hpp}Prepare/execute 요청과 응답 페이로드
cubmethod::oid_get_infomethod_struct_oid_info.{cpp,hpp}OID get/put/cmd 페이로드
cubmethod::schema_info_*method_struct_schema_info.{cpp,hpp}컬럼/테이블 디스크립터 구조체
cubmethod::dbvalue_packingmethod_struct_value.{cpp,hpp}브리지에 맞춰진 DB_VALUE 직렬화 (VOBJ → object, OID → object fixup 처리)

각 구조체가 표준 cubpacking::packer/unpackerpack, unpack, get_packed_size 를 구현한다. 두 경로가 이 같은 구조체 패밀리를 사용하기 때문에 cross-path 오피코드 공유가 가능하다는 점이다. 와이어 형식이 IPC 채널을 가로질러 portable 하기 때문이다.

호출 사슬이 종료되면(원래 메서드 스캔이 모든 행을 끝내거나, 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_dispatchquery_method.cpp 의 CAS 진입; tran_begin/end_libcas_function 으로 bracket, 재귀 깊이 검사, method_dispatch_internal 호출
method_dispatch_internalMETHOD_REQUEST_* 의 switch; 인자 prep / 빌트인 invoke / 콜백 디스패치 / end 로 디스패치
method_invoke_builtinruntime_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_argumentsVOBJ → object fixup (method_fixup_vobjs) 을 가진 args map mutator
method_fixup_vobjs / _fixup_set_vobjs / _has_set_vobjsOID/VOBJ 값을 사용자 메서드 본문에 넘기기 전에 materialise 된 객체로 변환
method_error디스패치 시작 전에 CAS 가 에러를 감지했을 때 서버로 METHOD_ERROR 전송
cubmethod::callback_handlerper-CAS-세션 상태 — query handler, OID handler, SQL→handler 캐시, 데이터 큐, error context
callback_handler::callback_dispatchMETHOD_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_cmdOID 핸들러
callback_handler::get_sql_semantics / get_global_semanticsPL/CSQL 컴파일러가 사용하는 컴파일타임 시맨틱 검사 핸들러
callback_handler::change_rightsowner/invoker 모드를 위한 auth 오피코드
callback_handler::new_query_handler / free_query_handle / free_query_handle_all / get_query_handler_by_*슬롯 표와 SQL 캐시 관리
cubmethod::oid_handlerOID 핸들러가 사용하는 OID materialisation 캐시
cubmethod::query_handler한 prepared-statement 슬롯을 위해 DB_SESSION + DB_QUERY_RESULT 를 감쌈
cubmethod::header, prepare_args외부 봉투 구조체
cubmethod::dbvalue_packingVOBJ 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_commandinvoke_java 페이로드를 패킹하고 풀에서 claim 한 연결로 SP_CODE_INVOKE 전송
cubpl::executor::response_invoke_command응답을 읽는 루프; 결과, 에러, SP_CODE_INTERNAL_JDBC 봉투를 라우팅
cubpl::executor::response_callback_commandMETHOD_CALLBACK_* 의 switch; 오피코드별 핸들러로 디스패치
callback_get_db_parameter트랜잭션 격리, lock-wait, client ID 를 돌려줌; pl_session 에 메모이즈
callback_prepare서버 측 쿼리 prepare; prepared-statement handle ID 반환
callback_executeprepared statement 실행, query ID 반환
callback_fetchquery ID 로 커서 배치 fetch
callback_oid_get / _put / _cmdOID materialisation, mutation, class/instance 명령
callback_collection_cmdSet/multiset/sequence 동작
callback_make_outresult쿼리 결과를 SP 반환을 위한 out 파라미터로 승격
callback_get_generated_keys마지막 INSERT 의 자동 생성 키 반환
callback_end_transactionJVM 측 JDBC 가 트리거한 서버 측 commit/abort
callback_get_code_attr저장 프로시저의 카탈로그 행 속성 반환 — JVM 이 참조된 SP 코드를 로드할 때 사용
callback_set_pl_session_paramper-session JVM 측 파라미터(예: DBMS_OUTPUT.ENABLE) 변형
심볼파일역할
cubscan::method::scannersrc/method/method_scan.{cpp,hpp}SCAN_TYPE_METHOD access method; 행당 메서드 호출
scanner::open / close / next_scan / init / clearsrc/method/method_scan.cpp표준 SCAN_ID lifecycle 와 메서드 그룹 바인딩
scanner::get_single_tuple / next_value_arraysrc/method/method_scan.cpp상류 list-file 에서 인자 추출, 결과 슬롯 준비
cubmethod::method_invoke_groupsrc/sp/method_invoke_group.{cpp,hpp}pl_signature_array 를 감쌈; METHOD_REQUEST_INVOKE 를 발사하는 호출당 객체
method_invoke_group::execute / prepare / begin / end / resetsrc/sp/method_invoke_group.cpp메서드 요청을 CAS 로 보내는 실제 호출 사이트
심볼경로
method_dispatchsrc/method/query_method.cpp:113
method_dispatch_internalsrc/method/query_method.cpp:201
method_invoke_builtinsrc/method/query_method.cpp:253
method_invoke_builtin_internalsrc/method/query_method.cpp:341
method_prepare_argumentssrc/method/query_method.cpp:286
method_fixup_vobjssrc/method/query_method.cpp:541
cubmethod::callback_handler (class)src/method/method_callback.hpp:58
callback_handler::callback_dispatchsrc/method/method_callback.cpp:68
callback_handler::end_transactionsrc/method/method_callback.cpp:138
callback_handler::preparesrc/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_commandsrc/sp/pl_executor.cpp:511
executor::callback_get_db_parametersrc/sp/pl_executor.cpp:606
executor::callback_preparesrc/sp/pl_executor.cpp:651
cubscan::method::scanner::next_scansrc/method/method_scan.cpp:173
cubmethod::method_invoke_group (class)src/sp/method_invoke_group.hpp:66

심볼 이름이 정규 anchor 이고, 라인 번호는 updated: 날짜에 스코프된 힌트이다.

  • 두 콜백 경로, 한 분류 체계. 오피코드 표는 공유되지만 (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_constantsquery_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 는 INVOKECALLBACK 디스패치를 무조건 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_semanticsget_global_semantics 는 런타임 콜백과 같은 방식으로 에러를 packed 형태로 돌려준다. 하지만 PL/CSQL 컴파일러는 이를 (원래 PL/CSQL 소스의 라인/컬럼이 붙은) 파서 수준 에러로 다룬다는 점이다. End-to-end 에러 경로 매핑은 완전히 문서화되어 있지 않다. cubrid-pl-plcsql.md 의 열린 질문 참조.
  • src/method/method_callback.{cpp,hpp} — CAS 측 콜백 handler
  • src/method/query_method.cpp — CAS 측 디스패치 진입 (method_dispatch, method_dispatch_internal)
  • src/method/method_scan.{cpp,hpp} — Path A 를 구동하는 서버 측 SCAN_TYPE_METHOD scanner
  • src/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 측 자원별 handler
  • src/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.mdsrc/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 스캔 타입)