콘텐츠로 이동

(KO) CUBRID 서버 세션 — 클라이언트별 상태, prepared statement 레지스트리, 그리고 TDES 바인딩

목차

관계형 데이터베이스 엔진은 클라이언트의 요청을 받아 처리해야 하고, 클라이언트 가 접속하는 순간 서로 직교하는 세 가닥의 상태가 동시에 태어난다. connection (TCP/UDS 소켓과 그에 매달린 버퍼와 큐), session (prepared statement, autocommit 모드, last insert id, locale 설정, 기본 격리 수준 — 즉 statement 사이를 가로질러 클라이언트가 유지 되리라 기대하는 모든 것), 그리고 transaction (엔진이 atomic 하게 commit하거나 abort하는 단위) 이다. Database Internals (Petrov) 5장 §Transactions 는 이 세 층을 분명히 분리해서 다룬다. connection은 네트워크 층이 소유하고 소켓과 함께 죽는다. transaction은 복구 매니저와 lock 매니저가 소유하고 commit / abort 시점에 죽는다. session은 그 사이에 자리한다. 단일 transaction을 넘어서 살고 (한 session이 여러 transaction을 실행한다), 단일 connection까지도 넘어설 수 있다 (TCP 소켓을 잃어버린 클라이언트가 이전 session id를 들고 다시 접속하면 prepared statement 캐시와 SET 변수 바인딩을 다시 컴파일하거나 다시 바인드하지 않고도 재개할 수 있다).

구체적인 서버 세션 모듈의 모양을 결정하는 두 가지 구현 선택이 있다.

  1. 세션 상태가 어디에 살고 어떻게 명명되는가. 교과서적인 답은 “접속 셋업 시점에 서버가 클라이언트에게 건네 준 불투명한 정수 를 키로 하는 해시 테이블” 이다. 변형은 그 테이블이 정적 슬롯 테이블인지 탄력적 해시인지, 요청 한 건당 lookup 지연 예산이 얼마인지, connection이 끊어졌을 때 무엇이 살아남는지 정도다. CUBRID는 SESSION_ID (32비트 unsigned 정수) 를 키로 하는 탄력적 lock-free 해시를 채택했고, 같은 소켓 위 후속 요청들은 해시 lookup 비용을 내지 않도록 세션의 실제 SESSION_STATE * 를 connection entry에 캐시한다.

  2. 세션이 트랜잭션 descriptor와 어떻게 묶이는가. 모든 쿼리는 SESSION_STATE (prepared statement, row count, last insert id, autocommit 을 찾기 위해) LOG_TDES (트랜잭션의 lock 집합, 로그 범위, MVCC 스냅샷을 찾기 위해) 둘 다 필요하다. CUBRID는 connection 층으로 요청을 라우팅하는 방식으로 이를 해결한다. connection은 이미 자기당 트랜잭션 인덱스 하나를 소유하고 있고, worker 스레드가 그 인덱스를 자기 THREAD_ENTRY 에 복사해 둠으로써 LOG_FIND_THREAD_TRAN_INDEX(thread_p) 가 올바른 LOG_TDES 를 반환한다. 세션은 부기 컨테이너이고 TDES는 트랜잭션 컨테이너이며, 이 둘을 묶어 주는 것이 connection entry다.

이 두 선택이 분명해지면 session 모듈의 다른 모든 부분 — timeout reaper, 스레드가 conn entry를 두고 떠날 때의 ref-count 핸드오프, holdable cursor 리스트, prepared statement 캐시, lock-free 해시 내부 — 이 두 선택 중 하나를 위해 존재한다는 점이 보인다.

prepared statement, session 단위 설정 (autocommit, 기본 격리 수준, locale), 재접속을 지원하는 모든 관계형 엔진은 비슷한 한 줌의 패턴을 채택한다.

connection별 vs session별 상태. PostgreSQL은 connection과 session을 거의 한 덩어리로 합친다. 각 백엔드 프로세스가 단일 클라이언트에 대응하며, MyProc (그 프로세스의 PGPROC 슬롯), MyBackendId, 프로세스별 pg_stat_session session이고, 그것들은 백엔드가 종료될 때 함께 죽는다. MySQL도 비슷하게 모든 것을 connection별 단일 THD (thread descriptor) 위에 둔다. 열린 테이블, prepared statement, session 변수, 트랜잭션 컨텍스트 모두. THD는 connection과 함께 죽는다. Oracle은 정반대 극단으로 간다. session에 주소가 붙고 (v$session.SID,SERIAL#), connection 사이를 마이그레이션할 수도 있으며 (shared-server, multitenant), 공유 cursor 캐시는 SGA 수준에서 산다. CUBRID는 의도 면에서 Oracle 쪽에 가깝다 (session이 일급 식별 가능 객체 이고, 소켓을 잃은 클라이언트가 옛 SESSION_ID 를 제시해서 prepared statement와 session 변수를 복구할 수 있다) 는 점에서. 하지만 구현 면에서는 PostgreSQL / MySQL 쪽에 가깝다 (shared server가 없고, worker pool이 하나이며, session이 특정 프로세스 나 스레드에 묶이는 대신 서버 전역 lock-free 해시에 산다). SESSION_ID 가 주소이고, SESSION_STATE 가 컨테이너이며, CSS_CONN_ENTRY 가 그 컨테이너를 가리키는 물리 connection별 캐시다.

요청마다의 session lookup. 토폴로지가 어떻든 모든 엔진은 SQL 파이프라인으로 디스패치하기 전에 “이 패킷이 어느 session에 속하 는가” 라는 질문에 답해야 한다. PostgreSQL은 그 질문을 건너뛴다. 백엔드가 바로 session이기 때문이다. MySQL은 THD 포인터를 스레드 로컬 저장소에 캐시한다. Oracle은 SID를 네트워크 패킷에 넣어 보낸다. CUBRID는 MySQL이 하는 일을 connection-entry granularity로 한다. CSS_CONN_ENTRY 마다 session_idsession_p 필드를 가지며, 첫 xsession_check_session 이 성공한 이후로는 worker가 해시 lookup 비용 없이 session_p 를 직접 읽는다.

session 단위 캐시로서의 prepared statement 레지스트리. prepared statement는 트랜잭션보다는 길게 살아남되 session보다는 짧게 사는 어딘가에 보관해야 한다. PostgreSQL은 백엔드 위에 둔다. MySQL은 THD 위에 둔다. Oracle은 두 단계로 나눈다. statement는 session이 소유하지만 parsed / optimised plan은 SGA에서 공유된다. CUBRID는 다시 하이브리드다. 이름 붙은 statement entry (PREPARED_STATEMENT — name, alias_print, sha1, 직렬화된 info blob) 는 session 위에 살고, XASL plan 은 plan source의 SHA-1을 키로 하는 서버 전역 캐시 (xasl_cache_ent) 에 산다. session은 xcache_find_sha1 으로 그 캐시에서 자기 plan을 찾아 쓸 권리를 보유한다.

ref count + timeout reaping. 한 session은 여러 worker 스레드 에 의해 동시 참조될 수 있으므로, 어떤 엔진도 소켓이 닫혔다는 이유만으로 session을 파괴하지 않는다. CUBRID는 SESSION_STATE 위에 명시적인 ref_count 를 두고, 스레드가 conn entry에 session 을 바인딩할 때 session_state_increase_ref_count 로 증가시키며, 바인딩을 해제할 때 감소시킨다. 주기적인 reaper (session_remove_expired_sessions, session_control_daemon 이 60초마다 실행) 가 테이블을 걸어가며 active_timePRM_ID_SESSION_STATE_TIMEOUT 보다 오래되었고 동시에 ref_count 가 0이며 동시에 is_keep_session 플래그가 꺼진 session들을 파괴한다.

session vs 트랜잭션 lifetime. session은 한 시점에 최대 한 개 의 활성 트랜잭션만 가질 수 있고, 트랜잭션은 session 안에서 시작 되어 session 안에서 끝난다. CUBRID는 이를 암묵적으로 강제한다. LOG_TDES 는 worker가 처음 필요로 할 때 (commit / abort 후 첫 트랜잭셔널 요청에서 lazy하게) logtb_assign_tran_index 가 할당하며, 라이프사이클에서 session의 유일한 역할은 각 statement가 트랜잭션을 암묵적으로 닫을지를 결정하는 autocommit 플래그 (SESSION_STATE::auto_commit) 를 들고 있는 것뿐이다.

이론적 개념CUBRID 명칭
Session identifierSESSION_ID (compat/dbtype_def.h 에서 unsigned int 로 typedef)
Session state containerSESSION_STATE (session.c)
서버 전역 session 테이블ACTIVE_SESSIONS::states_hashmap (session.c)
빈 / 미바인딩 sessionDB_EMPTY_SESSION = 0 (compat/dbtype_def.h)
Connection entry session 캐시CSS_CONN_ENTRY::session_p + ::session_id (connection_defs.h)
Per-session prepared statementPREPARED_STATEMENT (session.c)
Holdable cursor / 쿼리 결과SESSION_QUERY_ENTRY (session.c)
서버 전역 XASL plan 캐시XASL_CACHE_ENTRY (xasl_cache.h) — SHA-1 키
Per-session locale 영역SESSION_STATE::session_tz_region
Per-session sysparam 오버라이드SESSION_STATE::session_parameters (SESSION_PARAM 배열)
Session ↔ 트랜잭션 바인딩CSS_CONN_ENTRY::transaction_id (set_tran_index 가 설정)
Session ↔ 스레드 바인딩THREAD_ENTRY::conn_entry->session_p
Reaper daemonsession_remove_expired_sessions 를 실행하는 session_Control_daemon
서버 진입점 — find or createxsession_create_new, xsession_check_session (session_sr.c)
서버 진입점 — endxsession_end_session (session_sr.c)
네트워크 핸들러 — connectssession_find_or_create_session (network_interface_sr.cpp)
네트워크 핸들러 — disconnectssession_end_session (network_interface_sr.cpp)

session 모듈의 움직이는 부분은 네 가지다. 살아 있는 SESSION_STATE 들을 모두 들고 있는 active sessions 테이블, prepared statement / query / 변수 리스트가 박혀 있는 SESSION_STATE 본체, 테이블 안의 entry가 거치는 lifecycle 상태 머신, 그리고 요청이 막 도착했다 를 “올바른 session, 올바른 트랜잭션 위에서 실행하라” 로 바꿔 주는 thread / connection / TDES 바인딩. 이 순서대로 본다.

flowchart LR
  subgraph CL["클라이언트 측"]
    DBSES["db_Session_id\n(프로세스별, CS/SA 모드)"]
  end
  subgraph NET["네트워크 진입 (network_interface_sr.cpp)"]
    SFC["ssession_find_or_create_session\n→ xsession_check_session\n→ xsession_create_new"]
    SES["ssession_end_session"]
    OTHER["ssession_set_row_count\nssession_create_prepared_statement\nssession_get_prepared_statement\n..."]
  end
  subgraph SR["서버 코어 (session.c / session_sr.c)"]
    XCREATE["xsession_create_new"]
    XCHECK["xsession_check_session"]
    XEND["xsession_end_session"]
    SCREATE["session_state_create"]
    SCHECK["session_check_session"]
    SDESTROY["session_state_destroy"]
    SGET["session_get_session_state"]
  end
  subgraph TBL["sessions (ACTIVE_SESSIONS)"]
    HASH["states_hashmap\n(lockfree_hashmap<SESSION_ID, session_state>)"]
    LSI["last_session_id (atomic 카운터)"]
    NHC["num_holdable_cursors"]
  end
  subgraph CONN["Connection 층 (connection_sr.c)"]
    CONNENT["CSS_CONN_ENTRY\n.session_id, .session_p, .transaction_id"]
  end
  subgraph THR["Thread / TDES"]
    TE["THREAD_ENTRY\n.conn_entry, .tran_index"]
    TDES["LOG_TDES\n(log_Gl.trantable.all_tdes[tran_index])"]
  end

  DBSES --> NET
  SFC --> XCHECK
  SFC --> XCREATE
  SES --> XEND
  OTHER --> SGET

  XCREATE --> SCREATE
  XCHECK --> SCHECK
  XEND --> SDESTROY

  SCREATE --> HASH
  SCHECK --> HASH
  SDESTROY --> HASH

  SCREATE --> CONNENT
  SCHECK --> CONNENT
  SGET --> CONNENT

  CONNENT --> TE
  TE --> TDES

모듈 전체가 단 하나의 static ACTIVE_SESSIONS 값을 닻으로 삼는다. session.c 파일 스코프에 선언되어 있다.

// active_sessions — session.c
typedef struct active_sessions
{
session_hashmap_type states_hashmap;
SESSION_ID last_session_id;
int num_holdable_cursors;
// ... ctor zero-initialises all three ...
} ACTIVE_SESSIONS;
static ACTIVE_SESSIONS sessions;

이 hashmap은 typedef alias가 붙은 lock-free 해시다.

// session_hashmap_type — session.c
using session_hashmap_type
= cubthread::lockfree_hashmap<SESSION_ID, session_state>;

서버 부팅 시에 session_states_init 에서 한 번 초기화되고, 종료 시 session_states_finalize 에서 정리된다.

// session_states_init — session.c (condensed)
sessions.last_session_id = 0;
sessions.num_holdable_cursors = 0;
sessions.states_hashmap.init (sessions_Ts, THREAD_TS_SESSIONS,
SESSIONS_HASH_SIZE /* 1000 */, 2, 50,
session_state_Descriptor);
#if defined (SERVER_MODE)
session_control_daemon_init ();
#endif

hashmap descriptor에는 freelist link, chain link, lock-free delete-id, key, entry별 mutex의 offset이 박혀 있다. 짚고 갈 포인트가 셋이다.

  • 해시 버킷 수는 1000, free-list 성장 파라미터는 (2, 50) — 50% 차면 2배로 자동 grow한다는 뜻이다.
  • entry locking 모드로 hazard pointer가 아니라 LF_EM_USING_MUTEX 를 골랐다 (각 SESSION_STATE 가 자기 pthread_mutex_t mutex 를 들고 있다). 이유는 session이 multi-step 조작 (insert + initialise, lookup + ref-count 갱신) 사이에 latch 된 상태로 잡혀 있어야 하기 때문이다.
  • last_session_id 는 create 시마다 ATOMIC_INC_32 로 증가한다. hashmap 내부에는 key_increment 콜백 (session_key_increment) 이 등록되어 있어 두 스레드가 동일한 후보 id에 부딪히면 hash가 투명하게 다음 빈 슬롯으로 bump하고, 호출자가 그 결과를 ATOMIC_CAS_32 로 글로벌 카운터에 다시 써 준다.
// session_state_create — session.c (condensed)
next_session_id = ATOMIC_INC_32 (&sessions.last_session_id, 1);
*id = next_session_id;
(void) sessions.states_hashmap.insert (thread_p, *id, session_p);
ATOMIC_CAS_32 (&sessions.last_session_id, next_session_id, *id);

이 패턴 — 카운터로 후보를 제안하고, hashmap이 실제 id를 정하고, 실제 id를 다시 쓰는 — 덕분에 두 스레드가 두 별개 클라이언트를 두고 xsession_create_new 에서 동시에 경합해도 카운터가 monotonic 단조 비감소 상태를 유지한다.

// session_state — session.c (condensed)
typedef struct session_state SESSION_STATE;
struct session_state
{
SESSION_ID id; /* session id (the hash key) */
SESSION_STATE *stack; /* used in freelist */
SESSION_STATE *next; /* used in hash table chain */
pthread_mutex_t mutex; /* state mutex */
UINT64 del_id; /* delete transaction id (lock-free) */
bool is_keep_session; /* survive timeout + disconnect */
bool is_trigger_involved;
bool is_last_insert_id_generated;
bool auto_commit;
DB_VALUE cur_insert_id;
DB_VALUE last_insert_id;
int row_count;
SESSION_VARIABLE *session_variables; /* SET @x = .. list */
PREPARED_STATEMENT *statements; /* PREPARE name AS .. list */
SESSION_QUERY_ENTRY *queries; /* holdable cursor results */
time_t active_time; /* used by reaper */
SESSION_PARAM *session_parameters; /* per-session sysprm overrides */
char *trace_stats;
char *plan_string;
int trace_format;
int ref_count; /* # threads / conns referencing */
TZ_REGION session_tz_region; /* locale */
int private_lru_index; /* private buffer-pool LRU */
load_session *load_session_p; /* loaddb sub-session */
PL_SESSION *pl_session_p; /* PL/SP sub-session */
};

이 구조체에는 다섯 갈래의 서브시스템이 매달려 있다.

  • Identity / hash 배관. id, stack, next, mutex, del_id. lockfree_hashmap 에 넘기는 descriptor가 이 필드들을 offsetof 로 읽는다.
  • statement별 편의 상태. cur_insert_id, last_insert_id, row_count, is_last_insert_id_generated, is_trigger_involved. 한 트랜잭션 안의 여러 statement에 걸쳐 유지되며, SQL 함수 LAST_INSERT_ID(), ROW_COUNT() 가 이 값을 읽어 간다.
  • 이름 붙은 session 단위 객체의 카탈로그. session_variables (SET @v = ...), statements (PREPARE name FROM ...), queries (commit을 넘어 살아남는 holdable cursor). session별 개수에 상한이 있기 때문에 (MAX_SESSION_VARIABLES_COUNT = 20, MAX_PREPARED_STATEMENTS_COUNT = 20) 단순한 단방향 연결 리스트로 충분하다.
  • session 단위 sysparam + locale. session_parameters, session_tz_region. session이 자기 영역에서 intl_*, tz_*, 격리 기본값 등을 로컬 오버라이드할 수 있고, 이 배열은 connect 패킷에서 ssession_find_or_create_session 안의 sysprm_session_init_session_parameters 가 풀어 준다.
  • 서브 session. load_session_p (한 번의 loaddb 호출에 대응하는 bulk loader의 parser-and-interrupt 컨텍스트) 와 pl_session_p (PL/JavaSP 실행 컨텍스트 — cubrid-pl-javasp.md 참조). 둘 다 session이 소유하고 session_state_uninit 에서 파괴된다. statement 경계를 넘어 살 수 있기 때문에 트랜잭션이 아니라 session 위에 산다.
stateDiagram-v2
  [*] --> NEW : xsession_create_new
  NEW --> ACTIVE : session_state_create \n hash insert + conn_entry 바인딩
  ACTIVE --> ACTIVE : xsession_check_session \n active_time 갱신, conn 바인딩 갱신
  ACTIVE --> SLEEPING : worker가 요청을 끝냄 \n session_state_decrease_ref_count
  SLEEPING --> ACTIVE : 같은 conn에 새 요청 \n ref_count++
  SLEEPING --> EXPIRED : reaper 판정 \n now - active_time > PRM_ID_SESSION_STATE_TIMEOUT \n AND ref_count == 0
  EXPIRED --> KEPT : is_keep_session == true
  KEPT --> ACTIVE : 같은 SESSION_ID 로 reconnect
  EXPIRED --> DEAD : session_state_uninit + hash에서 erase
  ACTIVE --> DEAD : xsession_end_session (is_keep_session==false)
  DEAD --> [*]

세 전이가 자세히 볼 만하다.

NEW → ACTIVE. 새 클라이언트의 유일한 진입로는 ssession_find_or_create_session 네트워크 핸들러다. 핸들러 내부 로직은 옛 session id를 살려 보고, 안 되면 새로 만든다 한 줄로 요약된다.

// ssession_find_or_create_session — network_interface_sr.cpp (condensed)
ptr = or_unpack_int (request, (int *) &id);
ptr = or_unpack_stream (ptr, server_session_key, SERVER_SESSION_KEY_SIZE);
ptr = sysprm_unpack_session_parameters (ptr, &session_params);
ptr = or_unpack_string_alloc (ptr, &db_user);
ptr = or_unpack_string_alloc (ptr, &host);
ptr = or_unpack_string_alloc (ptr, &program_name);
if (id == DB_EMPTY_SESSION
|| memcmp (server_session_key, xboot_get_server_session_key (), ...) != 0
|| (error = xsession_check_session (thread_p, id)) != NO_ERROR)
{
er_clear ();
error = xsession_create_new (thread_p, &id); /* fresh id */
}
else if (error == NO_ERROR)
{
xsession_set_is_keep_session (thread_p, false);
}

server_session_key 는 서버 부팅마다 xboot_get_server_session_key() 가 발행하는 16바이트 cookie다. 클라이언트가 캐시해 둔 session id가 지금 이 서버의 마지막 부팅 이전 에 발급된 것이라면 이 검사가 통과되지 않아 새 session이 강제로 생성된다. 이 key 검사가 없다면 재부팅된 서버에 옛 session id를 들이밀어 “내가 만든 적 없는 session을 내가 들고 있다고 믿게” 만드는 트릭이 가능해진다.

session 생성이 성공하면 session_state_create 가 바인딩 작업을 처리한다.

// session_state_create — session.c (condensed)
next_session_id = ATOMIC_INC_32 (&sessions.last_session_id, 1);
*id = next_session_id;
(void) sessions.states_hashmap.insert (thread_p, *id, session_p);
ATOMIC_CAS_32 (&sessions.last_session_id, next_session_id, *id);
session_p->pl_session_p = new PL_SESSION (session_p->id);
session_p->active_time = time (NULL);
#if defined (SERVER_MODE)
session_state_increase_ref_count (thread_p, session_p);
session_p->private_lru_index = pgbuf_assign_private_lru (thread_p);
session_set_conn_entry_data (thread_p, session_p);
logtb_set_current_user_active (thread_p, true);
#endif

hash insert 외에 의미 있는 부수 효과가 셋이다.

  1. session 단위 private LRU 가 buffer pool에 할당된다 (pgbuf_assign_private_lru). session에 자기 LRU chain이 따로 생기므로 heap-scan을 많이 하는 클라이언트가 공유 LRU를 휩쓸어 가는 일을 방지한다.
  2. connection entry가 session으로 역포인터 를 갖는다 (session_set_conn_entry_dataconn_entry->session_pconn_entry->session_id 를 쓴다). 이 connection 위의 후속 요청은 모두 hash lookup을 우회한다.
  3. TDES가 user-active 로 표시된다 (logtb_set_current_user_active(thread_p, true)tdes->is_user_active 를 토글). connection 모니터와 shutdown 경로가 이 TDES가 살아 있는 사용자에게 소유되어 있음을 알도록 한다.

ACTIVE → SLEEPING → EXPIRED. Reaper는 부팅 시에 등록되는 cubthread::daemon 이다.

// session_control_daemon_execute — session.c
if (!BO_IS_SERVER_RESTARTED ())
return;
session_remove_expired_sessions (&thread_ref);

60초마다 한 번씩 (cubthread::looper(std::chrono::seconds(60))) hashmap을 걸어가고, entry당 판정 규칙은 session_check_timeout 안에 있다.

// session_check_timeout — session.c (condensed)
if ((curr_time - session_p->active_time)
>= prm_get_integer_value (PRM_ID_SESSION_STATE_TIMEOUT))
{
/* extra safety: ask the connection layer for the active
* session ids; if our id is among them, refresh and skip. */
if (active_sessions->count == -1)
css_get_session_ids_for_active_connections (
&active_sessions->session_ids, &active_sessions->count);
for (i = 0; i < active_sessions->count; i++)
if (active_sessions->session_ids[i] == session_p->id)
{
session_p->active_time = time (NULL); /* refresh */
return err;
}
*remove = true;
}

두 가지가 눈에 띈다.

  • active_time 이 stale인데 css_Active_conn_anchor 위에 살아 있는 connection이 여전히 있다면 — 이때 entry는 제거되지 않고 제자리에서 갱신된다. 긴 쿼리가 단지 active_time 을 re-touch하지 못해 timeout을 넘긴 케이스를 이 분기가 처리한다.
  • 실제 삭제는 lock-free 해시의 함정을 피하느라 두 단계로 나뉜다. session_state_uninit 은 iterator 안에서 호출하지만, states_hashmap.erase 는 iterator 밖에서 호출한다 (session_remove_expired_sessions 의 주석에 “lf_hash_delete may have to retry, which also resets the lock-free transaction. And resetting lock-free transaction can break our iterator.” 라고 쓰여 있다). 한 번의 pass에서 최대 1024개의 만료 entry를 버퍼링한 뒤 짧은 루프로 erase하고, 그러고 나서 iterator를 재시작한다.

ACTIVE → KEPT. 자신이 곧 disconnect할 것을 알지만 session은 warm 상태로 남기고 싶은 클라이언트는 xsession_end_session 호출 이전에 xsession_set_is_keep_session 으로 is_keep_session = true 를 켠다. session_state_destroy 가 이 플래그를 존중한다.

// session_state_destroy — session.c (condensed)
if (is_keep_session == true)
{
session_p->is_keep_session = true;
pthread_mutex_unlock (&session_p->mutex);
return NO_ERROR;
}

session은 hash에 남고, conn entry는 분리되며 (thread_p->conn_entry->session_p = NULL), reaper도 이 entry를 무시한다 (timeout 분기가 is_keep_session 을 보고 cleanup을 건너뛴다). 같은 SESSION_ID 로 다시 접속해 오면 xsession_check_session 이 성공하면서 부활한다.

session 단위 prepared statement 레지스트리

섹션 제목: “session 단위 prepared statement 레지스트리”

SESSION_STATE::statements 필드는 PREPARED_STATEMENT 의 단방향 연결 리스트다.

// PREPARED_STATEMENT — session.c
typedef struct prepared_statement PREPARED_STATEMENT;
struct prepared_statement
{
char *name;
char *alias_print; /* decompiled SQL printed back */
SHA1Hash sha1; /* keys the XASL cache */
int info_length;
char *info; /* serialised parameter info */
PREPARED_STATEMENT *next;
};

session_create_prepared_statement 는 같은 name (대소문자 무시) 의 기존 entry를 찾아 리스트를 walk하고, 있으면 떨어내고 새 entry를 앞에 prepend한다. 리스트의 길이는 MAX_PREPARED_STATEMENTS_COUNT = 20 으로 막혀 있고 그 이상은 ER_SES_TOO_MANY_STATEMENTS 를 반환한다.

lookup은 동일한 walk다.

// session_get_prepared_statement — session.c (condensed)
for (stmt_p = state_p->statements; stmt_p != NULL; stmt_p = stmt_p->next)
if (intl_identifier_casecmp (stmt_p->name, name) == 0)
break;
...
err = xcache_find_sha1 (thread_p, &stmt_p->sha1,
XASL_CACHE_SEARCH_GENERIC,
xasl_entry, NULL);

요컨대 캐시가 두 층이다. session은 바인딩 을 소유한다. 이름과 SHA-1 그리고 파라미터 info blob의 묶음. 서버 전역 XASL 캐시 가 SHA-1을 키로 하는 plan 을 소유한다. 다른 session이 동일한 SQL로 prepare하면 자기 리스트에 새 PREPARED_STATEMENT entry는 하나 생기지만 같은 XASL_CACHE_ENTRY 를 재사용한다. 이는 정확히 Oracle의 2-tier 모델이다.

session 단위 쿼리 결과로서의 holdable cursor

섹션 제목: “session 단위 쿼리 결과로서의 holdable cursor”

JDBC의 HOLD_CURSORS_OVER_COMMIT 플래그가 켜진 쿼리는 commit 경계 를 넘어 클라이언트가 결과를 계속 읽을 수 있어야 한다. session 저장소가 없다면 file 매니저가 commit 시점에 결과 list file을 지워 버린다. session은 그 결과를 스냅샷으로 가져 둔다.

// SESSION_QUERY_ENTRY — session.c
typedef struct session_query_entry SESSION_QUERY_ENTRY;
struct session_query_entry
{
QUERY_ID query_id;
QFILE_LIST_ID *list_id;
QMGR_TEMP_FILE *temp_file;
int num_tmp;
int total_count;
QUERY_FLAG query_flag;
SESSION_QUERY_ENTRY *next;
};

query 매니저가 holdable 결과 하나마다 session_store_query_entry_info 를 호출한다. 함수는 query 매니저의 entry를 session 안으로 복사 하면서 list_idtemp_vfid 포인터를 훔쳐 오고 (원본 쪽에서 는 NULL로 비워서 query 매니저가 회수하지 못하게 한다), temp file은 file_temp_preservepreserved 플래그가 박혀 commit 시에 file 매니저가 지우지 못하게 만든다.

// session_preserve_temporary_files — session.c (condensed)
tfile_vfid_p = qentry_p->temp_file;
tfile_vfid_p->prev->next = NULL;
while (tfile_vfid_p)
{
if (!VFID_ISNULL (&tfile_vfid_p->temp_vfid))
if (!tfile_vfid_p->preserved)
{
file_temp_preserve (thread_p, &tfile_vfid_p->temp_vfid);
tfile_vfid_p->preserved = true;
}
tfile_vfid_p = tfile_vfid_p->next;
}

대칭적인 session_load_query_entry_info, session_remove_query_entry_info 가 fetch와 최종 close를 처리한다. 모든 session에 걸쳐 살아 있는 holdable 결과의 총 개수는 글로벌 카운터 sessions.num_holdable_cursors 가 추적한다. boot 경로가 list file 공간 예약량을 결정할 때 이 값을 본다.

session 모듈에서 단연 가장 중요한 사실은 요청 진입 시점에 무슨 일이 일어나는가 다. CUBRID의 네트워크 dispatcher (network_sr.c 안의 net_server_request) 는 connection worker가 클라이언트 패킷마다 한 번씩 호출한다. 이 함수는 *session lookup을 하지 않는다. session은 이미 바인딩되어 있다.

// net_server_request — network_sr.c (condensed)
conn = thread_p->conn_entry;
assert (conn != NULL);
if (IS_INVALID_SOCKET (conn->fd) || conn->status != CONN_OPEN)
goto end;
...
if (net_Requests[request].action_attribute & IN_TRANSACTION)
conn->in_transaction = true;
...
func = net_Requests[request].processing_function;
if (conn->invalidate_snapshot != 0)
logtb_invalidate_snapshot_data (thread_p);
(*func) (thread_p, rid, buffer, size);

request handler가 올바른 상태 에 도달하기까지의 포인터 사슬은 이렇다.

flowchart LR
  PKT["네트워크 패킷\n(rid, request_code, buffer)"]
  CONN["CSS_CONN_ENTRY\n.session_id, .session_p, .transaction_id"]
  TE["THREAD_ENTRY\n.conn_entry, .tran_index"]
  STATE["SESSION_STATE\n(prepared statements,\nrow count,\nsession 변수)"]
  TDES["LOG_TDES\n(trid, isolation,\nsavepoint, lock 집합,\nMVCC 스냅샷)"]

  PKT --> CONN
  CONN -->|conn->session_p| STATE
  CONN -->|conn->transaction_id| TE
  TE -->|LOG_FIND_TDES(tran_index)| TDES
  STATE -.PL_SESSION,\nload_session.-> TE

connection entry가 단일 hub다. 다음을 들고 있다.

  • session_id (정수) 와 session_p (resolve된 포인터). create / check 시점에 session_set_conn_entry_data 가 채워 둔다.
  • transaction_id (log_Gl.trantable.all_tdes[] 에 대한 트랜잭션 단위 인덱스). css_conn_entry 위의 set_tran_index / get_tran_index 로 조작한다. TDES는 그 인덱스를 받아 LOG_FIND_TDES(tran_index) 한 번에 resolve된다.

worker 스레드가 패킷을 집어들 때, dispatcher는 thread_p 가 이미 thread_p->conn_entry 에 바인딩된 상태로 호출된다. 그 결과 session lookup은 O(1)이다.

// session_get_session_state — session.c (condensed)
if (thread_p == NULL)
thread_p = thread_get_thread_entry_info ();
if (thread_p != NULL && thread_p->conn_entry != NULL
&& thread_p->conn_entry->session_p != NULL)
return thread_p->conn_entry->session_p;
else
{
if (thread_p->type == TT_WORKER)
er_set (ER_ERROR_SEVERITY, ARG_FILE_LINE, ER_SES_SESSION_EXPIRED, 0);
return NULL;
}

이 패턴 — *session은 connection entry에 캐시되어 있고, thread는 connection entry를 가지고 다니며, 모든 request handler는 thread_p 의 함수다. 가, xsession_set_row_count 같은 서버 측 핸들러가 THREAD_ENTRY * 만 받고 SESSION_ID 를 절대로 받지 않는 이유다. session은 호출 컨텍스트에 암시적으로 들어 있다.

성공적인 바인딩이 있을 때마다 session의 ref_count 가 올라간다.

// session_state_increase_ref_count — session.c
ATOMIC_INC_32 (&state_p->ref_count, 1);

conn entry가 hold를 놓으면 내려간다.

// session_state_decrease_ref_count — session.c
ATOMIC_INC_32 (&state_p->ref_count, -1);

관련 지점은 다음과 같다.

  • session_state_create: 새 conn entry를 위해 한 번 올린다.
  • xsession_check_session: conn entry가 들고 있던 이전 session이 있다면 내리고, 새 session을 위해 올린다.
  • xsession_end_session / session_state_destroy: 정상 종료 시 내린다. 만일 destroy 시점에 ref_count > 0 이라면 session은 손대지 않고 ("This session_state is busy, I can't remove") conn entry의 바인딩만 끊는다.
  • css_shutdown_conn: 죽어 가는 connection이 아직 session을 들고 있었다면 내린다.

debug 빌드에는 session_state_verify_ref_count 라는 검증기가 있어 css_Active_conn_anchor 를 walk하며 이 session을 참조하는 conn 수를 직접 세고, 부기와 어긋나면 assertion으로 터진다.

SESSION_STATE 는 두 개의 포인터-타입 sub-session 컨테이너를 들고 있다.

  • load_session_ploaddb 클라이언트를 xloaddb_init 이 채운다. bulk loader는 long-running이고 부분 상태가 있으며 interrupt 도 노출해야 하는데, 그 cancel 핸들을 session이 소유한다.
  • pl_session_p — session 탄생 시점에 session_state_create 안 에서 new PL_SESSION (session_p->id) 로 생성된다. PL session이 stored procedure 실행 stack과 JavaSP / PL-CSQL 호출자에 대한 cross-thread interrupt 플래그를 들고 있다. cubrid-pl-javasp.md 에 자세히 다룬다.

session_stop_attached_threads (session_state_uninit 안에서 호출) 가 둘 모두를 걸어 정리한다.

// session_stop_attached_threads — session.c (condensed)
if (session->load_session_p != NULL)
{
session->load_session_p->interrupt ();
session->load_session_p->wait_for_completion ();
delete session->load_session_p;
session->load_session_p = NULL;
}
if (session->pl_session_p)
if (thread_p && thread_p->type == TT_WORKER)
{
session->pl_session_p->set_interrupt (er_errid ());
session->pl_session_p->wait_until_pl_session_done ();
}

session 종료가 사소한 작업이 아닌 이유가 여기에 있다. state 컨테이너를 파괴하기 전에 그 안에 박혀 있는 sub-session worker 스레드들을 먼저 풀어 줘야 한다.

session은 commit의 단위가 아니다. 대부분의 session 상태는 commit / rollback 을 그대로 통과한다. prepared statement, session 변수, last insert id, autocommit flag, 격리 기본값, locale.

트랜잭션과 함께 움직이는 것은 셋이다.

  • 사실은 holdable 가 아니었던 holdable cursor. query 매니저가 자기 non-holdable list를 commit 시에 닫고, state_p->queries 까지 살아 들어간 것만 살아남는다.
  • TDES 그 자체. commit / rollback이 tran_index 를 trantable의 free list로 돌려보내지만 (cubrid-transaction.md 참조) conn entry의 transaction_id 는 *0으로 비워지지 않는다. 다음 트랜잭셔널 요청이 logtb_assign_tran_index 시점에 동일한 인덱스를 다시 집어 오거나, conn entry의 캐시된 id가 여전히 할당되어 있다면 그대로 재사용한다.
  • TDES의 is_user_active 플래그는 사용자 활동 주변에서 토글 되며, shutdown 경로가 이 플래그를 보고 기다린다.

반대 경계 — connection 끊김 — 은 더 적극적이다. net_server_conn_down (worker 아래에서 TCP 소켓이 죽었을 때 connection 층이 호출) 은 session_remove_query_entry_all 을 호출해 session의 모든 holdable cursor를 선제적으로 닫는다. session 본체는 disconnect 패킷에서 is_keep_session == falsexsession_end_session 이 들어왔을 때만 죽는다.

session은 다음의 주체로부터 참조될 수 있다.

  • 현재 패킷을 처리 중인 conn-worker 스레드 (보통의 케이스).
  • 정리를 위해 trantable을 walk하지만 session 상태는 건드리지 않는 vacuum worker.
  • reaper pass 중인 session-control daemon.
  • 진행 중인 stored procedure를 PL session으로 들어온 외부 worker 스레드.

SESSION_STATE 의 mutex는 hash 내부 연산 (find, insert, erase) 과 mutable list head를 짧게 읽는 동안에만 잡힌다. 리스트 walk (session_get_prepared_statement 등) 는 캐시된 conn_entry->session_p 를 dereference한 뒤로는 mutex 없이 진행된다. 가정은 “이 conn entry의 session은 이 conn entry의 worker에서만 mutate된다” 이고, 이 가정은 다음과 같이 성립한다.

  • 같은 conn에 대한 두 번째 worker가 들어오려면 먼저 session에 바인딩해야 하는데, 그 경로가 hashmap을 거치며 mutex를 잡는다.
  • reaper는 ref_count > 0 이면 삭제를 건너뛰고, == 0 이면 그 정의상 다른 스레드가 리스트를 walk하고 있을 수 없다.

이 덕분에 prepared statement 리스트와 session 변수 리스트는 별도 list-lock 없는 단순 단방향 연결 리스트로 충분하다.

심볼들을 서브시스템별로 묶었다. 라인 번호는 이 문서의 updated: 시점에 관찰된 값이며, 시간이 지나면 어긋난다. 심볼 이름을 닻으로 삼아라.

심볼역할
ACTIVE_SESSIONSstatic sessions 값: hashmap, last id, holdable-cursor 카운터
SESSION_STATEsession별 컨테이너 구조체
session_state_Descriptorlockfree-hashmap의 동작을 설정하는 LF_ENTRY_DESCRIPTOR
session_state_alloc / session_state_freehashmap의 entry malloc/free hook
session_state_init / session_state_uninitentry 재활용 hook (insert 시 init, erase 시 uninit)
session_key_copy / session_key_compare / session_key_hash / session_key_incrementlockfree-hashmap의 key 콜백
session_states_init / session_states_finalize모듈 init/teardown (서버 boot/shutdown에서 호출)
session_state_create실제 create-and-insert 루틴
session_state_destroy실제 erase 루틴 (is_keep_session 존중)
session_check_sessionactive_time touch + conn entry 재바인딩
session_remove_expired_sessionsreaper 안쪽 루프
session_check_timeoutentry별 만료 판정 (css_get_session_ids_for_active_connections 참고)
session_control_daemon_execute / _init / _destroy60초 reaper daemon 배관

서버 진입점 (session_sr.c → network_sr.c에서 등록)

섹션 제목: “서버 진입점 (session_sr.c → network_sr.c에서 등록)”
심볼역할
xsession_create_new새 session 발급
xsession_check_session기존 session 검증 / 부활
xsession_end_session종료 (또는 is_keep_session 마킹)
xsession_set_is_keep_sessionkeep 플래그 토글
xsession_set_row_count / xsession_get_row_countstatement별 row count
xsession_set_cur_insert_id / xsession_get_last_insert_id / xsession_reset_cur_insert_idLAST_INSERT_ID() 백엔드
xsession_create_prepared_statement / xsession_get_prepared_statement / xsession_delete_prepared_statementprepared statement 레지스트리
xlogin_userTDES에 사용자 이름 push (tdes->client.set_user)
xsession_set_session_variables / xsession_get_session_variable / xsession_drop_session_variablesSET @v = …
xsession_store_query_entry_info / xsession_load_query_entry_info / xsession_remove_query_entry_info / xsession_clear_query_entry_infoholdable cursor lifecycle
xsession_set_tran_auto_commitsession의 autocommit flag 토글

네트워크 핸들러 (network_interface_sr.cpp)

섹션 제목: “네트워크 핸들러 (network_interface_sr.cpp)”
심볼역할
ssession_find_or_create_sessionconnect 경로: check를 시도하고 실패하면 create. server_session_key, sysparam blob, db_user / host / program을 클라이언트로 회신
ssession_end_sessiondisconnect 경로
ssession_set_row_count / ssession_get_row_countwire request 백엔드
ssession_get_last_insert_id / ssession_reset_cur_insert_id동상
ssession_create_prepared_statement / ssession_get_prepared_statement / ssession_delete_prepared_statement동상
ssession_set_session_variables / ssession_get_session_variable / ssession_drop_session_variables동상

네트워크 dispatch 테이블 (network_sr.c)

섹션 제목: “네트워크 dispatch 테이블 (network_sr.c)”
심볼역할
net_Requests[NET_SERVER_SES_CHECK_SESSION].processing_function = ssession_find_or_create_session핸드셰이크 request
net_Requests[NET_SERVER_SES_END_SESSION] = ssession_end_sessiondisconnect request
NET_SERVER_SES_* 계열wire code 211 이후 (network.h 참조)
net_server_requestdispatcher; thread_p->conn_entry 를 읽고 핸들러 호출
net_server_conn_downconnection-loss 콜백; session_remove_query_entry_all 호출
심볼역할
CSS_CONN_ENTRY::session_id캐시된 session id
CSS_CONN_ENTRY::session_p캐시된 resolve된 SESSION_STATE *
CSS_CONN_ENTRY::transaction_id (set_tran_index / get_tran_index 경유)LOG_FIND_TDES 용 캐시된 tran_index
css_initialize_connsession_id = DB_EMPTY_SESSION, session_p = NULL 로 초기화
css_shutdown_connconn이 바인딩되어 있었다면 session의 ref_count 를 내림
css_find_conn_by_tran_index역방향 lookup (interrupt / kill에서 사용)
css_get_session_ids_for_active_connectionsreaper 헬퍼: 모든 connection의 session id 목록
css_Active_conn_anchor (+ rwlock)reaper가 walk하는 connection 리스트

session.c 안의 session 단위 캐시 배관

섹션 제목: “session.c 안의 session 단위 캐시 배관”
심볼역할
session_get_session_state핫 패스 getter: thread_p->conn_entry->session_p 반환
session_set_conn_entry_dataconn_entry->session_p, conn_entry->session_id 를 쓰고, thread_p->private_lru_index 를 설정하고, pgbuf_thread_variables_init 호출
session_state_increase_ref_count / session_state_decrease_ref_countatomic ref count
session_state_verify_ref_count (NDEBUG에서 비활성)css_Active_conn_anchor 를 walk하는 sanity check
심볼역할
PREPARED_STATEMENT + session_create_prepared_statement / session_get_prepared_statement / session_delete_prepared_statement / session_free_prepared_statementprepared statement 리스트 (상한 20)
SESSION_VARIABLE + session_add_variable / session_drop_variable / update_session_variable / free_session_variableSET @v = … 리스트 (상한 20)
SESSION_QUERY_ENTRY + qentry_to_sentry / sentry_to_qentry / session_preserve_temporary_files / session_free_sentry_data / session_remove_query_entry_allholdable cursor 리스트

TDES 바인딩 (transaction/log_tran_table.c)

섹션 제목: “TDES 바인딩 (transaction/log_tran_table.c)”
심볼역할
LOG_FIND_TDES(tran_index)매크로: log_Gl.trantable.all_tdes[tran_index]
LOG_FIND_THREAD_TRAN_INDEX(thread_p)thread_p->tran_index
logtb_assign_tran_indexsession에 바인딩된 thread가 첫 트랜잭셔널 요청을 낼 때 TDES 슬롯을 할당
logtb_set_current_user_activesession_state_create / _destroy 가 TDES의 user-bound 마킹을 토글하도록 호출
logtb_set_current_user_namessession_find_or_create_sessiondb_user 를 TDES에 박아 넣을 때 호출

위치 힌트 (이 리비전 시점 관찰값)

섹션 제목: “위치 힌트 (이 리비전 시점 관찰값)”
심볼파일라인
SESSION_STATEsrc/session/session.c115
ACTIVE_SESSIONS / sessionssrc/session/session.c188 / 205
session_state_Descriptorsrc/session/session.c163
session_states_initsrc/session/session.c609
session_states_finalizesrc/session/session.c634
session_state_createsrc/session/session.c664
session_state_destroysrc/session/session.c778
session_check_sessionsrc/session/session.c855
session_remove_expired_sessionssrc/session/session.c929
session_check_timeoutsrc/session/session.c1037
session_get_session_statesrc/session/session.c2847
session_set_conn_entry_datasrc/session/session.c2780
session_state_increase_ref_countsrc/session/session.c3138
session_state_decrease_ref_countsrc/session/session.c3163
session_control_daemon_executesrc/session/session.c561
session_create_prepared_statementsrc/session/session.c1750
session_get_prepared_statementsrc/session/session.c1862
session_store_query_entry_infosrc/session/session.c2507
session_get_session_tz_regionsrc/session/session.c3052
session_stop_attached_threadssrc/session/session.c3315
xsession_create_newsrc/session/session_sr.c39
xsession_check_sessionsrc/session/session_sr.c54
xsession_end_sessionsrc/session/session_sr.c67
xsession_create_prepared_statementsrc/session/session_sr.c185
xsession_get_prepared_statementsrc/session/session_sr.c204
ssession_find_or_create_sessionsrc/communication/network_interface_sr.cpp9181
ssession_end_sessionsrc/communication/network_interface_sr.cpp9315
ssession_create_prepared_statementsrc/communication/network_interface_sr.cpp9484
ssession_get_prepared_statementsrc/communication/network_interface_sr.cpp9584
net_server_requestsrc/communication/network_sr.c790
net_Requests[NET_SERVER_SES_CHECK_SESSION] =src/communication/network_sr.c616
net_server_conn_downsrc/communication/network_sr.c1040
CSS_CONN_ENTRY::session_psrc/connection/connection_defs.h478
CSS_CONN_ENTRY::session_idsrc/connection/connection_defs.h480
CSS_CONN_ENTRY::set_tran_index / get_tran_indexsrc/connection/connection_defs.h495 / 496
css_initialize_connsrc/connection/connection_sr.c254
css_shutdown_conn (session decref)src/connection/connection_sr.c402
css_get_session_ids_for_active_connectionssrc/connection/connection_sr.c1267
logtb_set_current_user_activesrc/transaction/log_tran_table.c2086
DB_EMPTY_SESSIONsrc/compat/dbtype_def.h504
  • vs. cubrid-transaction.md. 그 문서는 LOG_TDES, TRANTABLE, savepoint / topops 스택, 격리 수준 강제 등을 다룬다. 본 문서는 session을 다루며 TDES 내부를 다시 유도하지 않는다. 바인딩은 단방향이다. conn_entry->transaction_id 는 정수이고 LOG_FIND_TDES 가 그 정수를 resolve한다. session에는 LOG_TDES * 타입의 필드가 없고, TDES에도 SESSION_ID 필드가 없다. 양쪽을 함께 들고 있는 객체는 connection entry뿐이다. session의 트랜잭션 이라는 표현은 사실 “지금 이 session의 현재 connection entry에 캐시되어 있는 tran_index 의 TDES” 의 단축어다. reconnect를 끼면 엄밀하게는 1:1 관계가 아니다. keep-alive로 살려둔 session이 재접속하면 새 conn entry가 새 tran_index 를 잡아 오고, 이전 TDES는 이전 disconnect의 xtran_server_commit / _abort 경계 에서 이미 풀려 있기 때문이다.

  • vs. cubrid-pl-javasp.md. 그 문서는 PL_SESSION 을 PL/SP 실행 컨텍스트로 본다. 본 문서는 SESSION_STATE::pl_session_p 포인터 필드의 시각으로 본다. PL session은 session_state_create 에서 만들어져 session_stop_attached_threads 에서 정리된다. PL session은 서버 session의 peer가 아니라 sub-state 이며, PL 호출 은 session의 pl_session_p 로 흘러 들어가고 session의 lifetime을 상속받는다.

  • db_Session_id 필드. SERVER_MODE가 아닌 CS-mode와 SA-mode 에서는 session_get_session_idthread_p->conn_entry->session_id 대신 db_Session_id 를 반환한다. 단일 프로세스 모드에는 connection entry가 없기 때문이다. 그 경우에도 hashmap은 여전히 쓰이지만 conn_entry->session_p 캐시가 없으므로 SA-mode에서는 session_get_session_state 호출마다 hashmap lookup 비용을 낸다. SA-mode의 session이 정확히 한 개이므로 문제가 되지 않는다.

  • 캐시된 session_p vs. 만료된 session. reaper가 conn entry에 여전히 session_p 가 캐시된 session을 만료시키면, 그 conn entry의 다음 요청이 dangling pointer를 쓰게 된다. 보호 장치는 reaper의 규칙이다. ref_count > 0 인 session은 제거되지 않고 refresh된다. conn entry의 바인딩은 ref-count 되므로, 여전히 캐시되어 있는 session을 reaper가 free할 수 있는 경로는 존재할 수 없다. session_state_verify_ref_count 는 추가 belt-and-braces 가드다.

  • connection당 session 1개인가, 여러 개 가능한가? 자료구조 자체는 ref_count 로 둘 이상의 connection entry가 동일한 SESSION_STATE 를 동시에 참조하는 것을 허용 한다. 그러나 실제 로 ref_count 를 올리는 경로는 하나의 conn entry의 바인딩 경로뿐이다. reconnect는 새 conn의 ref가 잡히기 전에 옛 conn의 ref를 내린다. connection 풀의 reconnect storm 시 일부 드라이버가 요청하는 session pinning 처럼, 동시에 살아 있는 두 connection 이 동일 session id를 공유할 수 있는지는 문서화되어 있지 않다. session_state_verify_ref_countconn->session_id == session->id 인 conn을 모두 세므로 이 경우에도 통과는 하지만, 중복을 금지하는 assertion이 없다. 의도된 시나리오인지 검증할 설계 테스트가 필요하다.

  • broker 프로세스 재시작. cub_cas 브로커 프로세스가 재활용 될 때 (BROKER_RESTART_TIME 등), CAS는 서버 측에 열린 session들 을 들고 있는 채 종료한다. cub_server의 reaper가 끊긴 TCP를 보고 즉시 session을 free하는지, 아니면 graceful shutdown 직전에 CAS 풀이 켜 둔 is_keep_session 플래그가 다음 CAS가 받아 갈 때까지 session을 붙들어 두는지는 broker shutdown이 graceful이냐 kill -9이냐에 따라 갈린다. 두 경로 모두 존재한다.

  • statement 레지스트리 — thread별인가 session별인가. prepared-statement 리스트는 session별이며 lock이 없다. 이는 one-conn-↔-one-worker invariant에 의존한다. 향후 같은 conn entry 의 두 worker를 동시에 허용하는 재구성이 일어난다면 list walk 주변에 session 단위 lock을 두든지 lock-free 구조로 promote해야 한다.

  • MAX_PREPARED_STATEMENTS_COUNT = 20 인가? 큰 애플리케이션 은 ER_SES_TOO_MANY_STATEMENTS 에 부딪히고, JDBC 드라이버가 LRU eviction으로 가려 주긴 하지만 이 상한은 soft hint가 아니라 hard limit이다. 상한을 올리면 list walk의 비용도 선형으로 따라 오른다.

  • src/session/session.c — session state hash, lifecycle, prepared-statement / holdable-cursor 리스트, daemon
  • src/session/session.h — 외부 공개 surface
  • src/session/session_sr.cxsession_* 서버 진입점
  • src/connection/connection_defs.hsession_psession_id 필드를 가진 CSS_CONN_ENTRY 정의
  • src/connection/connection_sr.ccss_initialize_conn, css_shutdown_conn, css_get_session_ids_for_active_connections
  • src/communication/network_sr.cnet_server_request dispatcher, NET_SERVER_SES_* 테이블 채움, net_server_conn_down
  • src/communication/network_interface_sr.cppssession_* 네트워크 핸들러
  • src/transaction/log_tran_table.c — TDES 테이블, LOG_FIND_TDES, logtb_set_current_user_active
  • src/compat/dbtype_def.hSESSION_ID typedef, DB_EMPTY_SESSION 상수
  • 자매 문서: cubrid-transaction.md, cubrid-pl-javasp.md