CUBRID Server Session — Per-Client State, Prepared-Statement Registry, and TDES Binding
Contents:
- Theoretical Background
- Common DBMS Design
- CUBRID’s Approach
- Source Walkthrough
- Cross-check Notes
- Open Questions
- Sources
Theoretical Background
Section titled “Theoretical Background”A relational database engine has to talk to clients, and the moment a client connects three orthogonal pieces of state spring into existence: a connection (a TCP/UDS socket plus the buffers and queues hung off it), a session (everything the client expects to persist between statements — prepared statements, autocommit mode, last insert id, locale settings, isolation default), and a transaction (the unit the engine atomically commits or aborts). Database Internals (Petrov, ch. 5 §“Transactions”) is careful to keep these three layers distinct. The connection is owned by the network layer and dies with the socket. The transaction is owned by the recovery and lock managers and dies on commit/abort. The session sits in between: it outlives a single transaction (a session executes many transactions) and may outlive a single connection (a client that loses its TCP socket can reconnect, present its old session id, and resume its prepared-statement cache and SET-variable bindings without recompiling or rebinding).
Two implementation choices shape every concrete server-session module:
-
Where the session state lives and how it is named. The textbook answer is “in a hash table keyed by an opaque integer that the server hands the client at connection setup”. The variants are how that table is sized (static slot table vs. elastic hash), what the lookup latency budget is per request, and what survives a connection drop. CUBRID picks an elastic lock-free hash keyed by
SESSION_ID(an unsigned 32-bit integer), with the session’s actualSESSION_STATE *cached on the connection entry so that subsequent requests on the same socket pay no hash lookup. -
How the session binds to a transaction descriptor. Every query needs both a
SESSION_STATE(to find prepared statements, row counts, last insert id, autocommit) and aLOG_TDES(to find the transaction’s lock set, log range, MVCC snapshot). CUBRID does this by routing a request through the connection layer, which already owns one transaction index per connection; the worker thread copies that index into itsTHREAD_ENTRYsoLOG_FIND_THREAD_TRAN_INDEX(thread_p)returns the rightLOG_TDES. The session is the bookkeeping container and the TDES is the transactional container; the connection entry is what binds them.
Once those choices are named, every other piece of the session module — the timeout reaper, the reference-counted hand-off when a thread leaves the conn entry alone, the holdable-cursor list, the prepared-statement cache, the lock-free hash internals — is in service of one of them.
Common DBMS Design
Section titled “Common DBMS Design”Every relational engine that supports prepared statements, session-scoped settings (autocommit, isolation default, locale), and reconnect uses the same handful of patterns.
Per-connection vs. per-session state. Postgres collapses
connection and session almost entirely: each backend process is a
single client; MyProc (its PGPROC slot), MyBackendId, and
the per-process pg_stat_session row are the session, and they
die when the backend exits. MySQL similarly puts everything per
connection on a single THD (thread descriptor) — open tables,
prepared statements, session variables, transaction context — and
the THD dies with the connection. Oracle takes the opposite
extreme: a session is addressable (v$session.SID,SERIAL#) and
can be migrated between connections (shared-server, multitenant),
with a shared cursor cache living at the SGA level. CUBRID sits
closer to Oracle in intent (sessions are first-class identifiable
objects, and a client that loses its socket can present its old
SESSION_ID on reconnect to recover prepared statements and
session variables) but closer to Postgres/MySQL in implementation
(no shared-server, one worker pool, the session lives in a
server-wide lock-free hash rather than tied to a specific process
or thread). The SESSION_ID is the address; the SESSION_STATE
is the container; the CSS_CONN_ENTRY is the per-physical-connection
cache pointing at that container.
Session lookup on every request. Whatever the topology, every
engine has to answer “which session does this packet belong to?”
before dispatching to the SQL pipeline. Postgres skips it — the
backend is the session. MySQL caches the THD pointer in
thread-local storage. Oracle puts the SID in the network packet.
CUBRID does what MySQL does but at the connection-entry
granularity: each CSS_CONN_ENTRY carries session_id and
session_p fields, and after the first xsession_check_session
succeeds the worker reads session_p directly without paying for
a hash lookup.
Prepared-statement registry as a session-scoped cache. Prepared
statements have to live somewhere whose lifecycle outlives a
transaction but not the session. Postgres puts them on the backend;
MySQL on the THD; Oracle two-tiers them — statements owned by the
session, parsed/optimised plan shared at the SGA. CUBRID is again
hybrid: the named statement entry (PREPARED_STATEMENT — name,
alias_print, sha1, serialised info blob) lives on the session, and
the XASL plan is in a server-wide cache (xasl_cache_ent) keyed
by the SHA-1 of the plan source. The session retains the right to
look up its plans through that cache via xcache_find_sha1.
Reference counting + timeout reaping. Because a session can be
referenced by multiple worker threads concurrently, no engine
destroys a session purely on socket close. CUBRID has an explicit
ref_count on SESSION_STATE, incremented in
session_state_increase_ref_count when a thread binds the session
into its conn entry and decremented when the thread releases the
binding. A periodic reaper (session_remove_expired_sessions, run
by the session_control_daemon every 60 s) walks the table and
destroys sessions whose active_time is older than
PRM_ID_SESSION_STATE_TIMEOUT and whose ref_count is zero
and whose is_keep_session flag is clear.
Session vs. transaction lifetime. A session can have at most
one active transaction at a time, and the transaction begins
inside the session and ends inside the session. CUBRID enforces
this implicitly: the LOG_TDES is allocated by
logtb_assign_tran_index when the worker first needs it (lazily,
at the first transactional request after a commit/abort) and the
session’s only role in the lifecycle is to hold the autocommit
flag (SESSION_STATE::auto_commit) that decides whether each
statement implicitly closes the transaction.
Theory ↔ CUBRID mapping
Section titled “Theory ↔ CUBRID mapping”| Theoretical concept | CUBRID name |
|---|---|
| Session identifier | SESSION_ID (typedef’d unsigned int in compat/dbtype_def.h) |
| Session state container | SESSION_STATE (session.c) |
| Server-wide session table | ACTIVE_SESSIONS::states_hashmap (session.c) |
| Empty / unbound session | DB_EMPTY_SESSION = 0 (compat/dbtype_def.h) |
| Connection entry session cache | CSS_CONN_ENTRY::session_p + ::session_id (connection_defs.h) |
| Per-session prepared statement | PREPARED_STATEMENT (session.c) |
| Holdable cursor / query result | SESSION_QUERY_ENTRY (session.c) |
| Server-wide XASL plan cache | XASL_CACHE_ENTRY (xasl_cache.h) — keyed by SHA-1 |
| Per-session locale region | SESSION_STATE::session_tz_region |
| Per-session sysparam overrides | SESSION_STATE::session_parameters (array of SESSION_PARAM) |
| Session ↔ transaction binding | CSS_CONN_ENTRY::transaction_id (set by set_tran_index) |
| Session ↔ thread binding | THREAD_ENTRY::conn_entry->session_p |
| Reaper daemon | session_Control_daemon running session_remove_expired_sessions |
| Server entry — find or create | xsession_create_new, xsession_check_session (session_sr.c) |
| Server entry — end | xsession_end_session (session_sr.c) |
| Network handler — connect | ssession_find_or_create_session (network_interface_sr.cpp) |
| Network handler — disconnect | ssession_end_session (network_interface_sr.cpp) |
CUBRID’s Approach
Section titled “CUBRID’s Approach”The session module’s four moving parts are the active sessions
table that holds every live SESSION_STATE, the SESSION_STATE
itself with its embedded prepared-statement, query, and variable
lists, the lifecycle state machine the table’s entries
traverse, and the binding to thread/connection/TDES that turns
“a request just landed” into “execute this on the right session and
the right transaction”. We walk them in that order.
Overall structure
Section titled “Overall structure”flowchart LR
subgraph CL["Client side"]
DBSES["db_Session_id\n(per-process, in CS/SA mode)"]
end
subgraph NET["Network entry (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["Server core (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 counter)"]
NHC["num_holdable_cursors"]
end
subgraph CONN["Connection layer (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
The active sessions table
Section titled “The active sessions table”The whole module is anchored in one static ACTIVE_SESSIONS value,
declared at file scope in session.c:
// active_sessions — session.ctypedef 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;The hashmap is the typedef-aliased lock-free hash:
// session_hashmap_type — session.cusing session_hashmap_type = cubthread::lockfree_hashmap<SESSION_ID, session_state>;It is initialised once at server boot in session_states_init and
torn down in 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 ();#endifThe hashmap descriptor wires offsets for the freelist link, the chain link, the lock-free delete-id, the key, and the per-entry mutex. Three points are worth lingering on:
- The hash bucket count is
1000and the free-list growth params are(2, 50)— meaning the hash auto-grows by 2x once 50% full. LF_EM_USING_MUTEXselects mutex-based entry locking (eachSESSION_STATEowns its ownpthread_mutex_t mutex) rather than hazard pointers, because a session needs to be held latched across multi-step manipulations (insert+initialise, look-up +ref-count update).last_session_idis bumped atomically withATOMIC_INC_32on every create. The hashmap’s internalkey_incrementis registered (session_key_increment) so that if two threads collide on the same proposed id the hash transparently bumps to the next free slot, and the caller updates the global counter viaATOMIC_CAS_32afterwards.
// 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);This pattern — propose-via-counter, let the hashmap pick the
actual id, write back the actual id — is how CUBRID keeps the
counter monotonically non-decreasing even when two threads race
on xsession_create_new for two distinct clients.
The SESSION_STATE struct
Section titled “The SESSION_STATE struct”// 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 */};Five sub-systems hang off this struct:
- Identity / hash plumbing.
id,stack,next,mutex,del_id. The descriptor passed tolockfree_hashmapreads these byoffsetof. - Per-statement convenience state.
cur_insert_id,last_insert_id,row_count,is_last_insert_id_generated,is_trigger_involved. Maintained across statements within a transaction;LAST_INSERT_ID(),ROW_COUNT()SQL functions read them. - Catalogues of named session-scoped objects.
session_variables(SET @v = ...),statements(PREPARE name FROM ...), andqueries(holdable cursors that survive commit). Each is a bare singly-linked list because the per-session count is capped (MAX_SESSION_VARIABLES_COUNT = 20,MAX_PREPARED_STATEMENTS_COUNT = 20). - Per-session sysparam + locale.
session_parameters,session_tz_region. The session can locally overrideintl_*,tz_*, isolation defaults, etc.; the array is unpacked from the connect packet inssession_find_or_create_sessionviasysprm_session_init_session_parameters. - Sub-sessions.
load_session_p(the bulk-loader’s parser-and-interrupt context for aloaddbinvocation) andpl_session_p(the PL/JavaSP execution context — seecubrid-pl-javasp.md). These are owned by the session and destroyed insession_state_uninit; they exist on the session rather than the transaction precisely because they may outlive a single statement boundary.
Lifecycle
Section titled “Lifecycle”stateDiagram-v2 [*] --> NEW : xsession_create_new NEW --> ACTIVE : session_state_create \n insert into hash + bind to conn_entry ACTIVE --> ACTIVE : xsession_check_session \n touch active_time, refresh conn binding ACTIVE --> SLEEPING : worker finishes request \n session_state_decrease_ref_count SLEEPING --> ACTIVE : new request on same conn \n ref_count++ SLEEPING --> EXPIRED : reaper sees \n now - active_time > PRM_ID_SESSION_STATE_TIMEOUT \n AND ref_count == 0 EXPIRED --> KEPT : is_keep_session == true KEPT --> ACTIVE : reconnect with same SESSION_ID EXPIRED --> DEAD : session_state_uninit + erase from hash ACTIVE --> DEAD : xsession_end_session (is_keep_session==false) DEAD --> [*]
Three transitions deserve closer inspection.
NEW → ACTIVE. The ssession_find_or_create_session network
handler is the only path for a fresh client. The handler’s
internal logic is “try to revive the old session id, fall back to
creating a new one”:
// 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); }The server_session_key is a per-server-boot 16-byte cookie
returned by xboot_get_server_session_key(); it forces a fresh
session creation if the client’s cached session id was issued
before this server’s last boot. Without that key check a
client could trick a freshly-restarted server into thinking it
held a session it never created.
After session creation succeeds, session_state_create does the
binding work:
// 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);#endifThree side effects matter beyond the hash insert:
- A per-session private LRU is allocated in the buffer pool
(
pgbuf_assign_private_lru). The session gets its own LRU chain so heap-scan-heavy clients do not blow out the shared LRU. - The connection entry is back-pointed to the session
(
session_set_conn_entry_datawritesconn_entry->session_pandconn_entry->session_id). All later requests on this connection bypass the hash lookup. - The TDES is marked user-active
(
logtb_set_current_user_active(thread_p, true)flipstdes->is_user_active) so that the connection-monitor and shutdown paths know the TDES is owned by a live user.
ACTIVE → SLEEPING → EXPIRED. The reaper is a cubthread::daemon
registered at boot:
// session_control_daemon_execute — session.cif (!BO_IS_SERVER_RESTARTED ()) return;session_remove_expired_sessions (&thread_ref);It runs once every 60 seconds (cubthread::looper(std::chrono::seconds(60)))
and walks the hashmap. The decision rule per entry is in
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; }Two things are notable:
- A session whose
active_timeis stale but which still has a live connection incss_Active_conn_anchoris refreshed in place, not removed. This handles the case where a long query simply held the session past the timeout without re-touchingactive_time. - The actual deletion is two-phase to dodge a lock-free hash
trap:
session_state_uninitis called inside the iterator, butstates_hashmap.eraseis called outside the iterator (the comment insession_remove_expired_sessionssays “lf_hash_delete may have to retry, which also resets the lock-free transaction. And resetting lock-free transaction can break our iterator.”). Up to 1024 expirations are buffered per pass, then erased in a tight loop, then the iterator is restarted.
ACTIVE → KEPT. A client that knows it is going to disconnect
but want to keep its session warm sets is_keep_session = true
through xsession_set_is_keep_session before calling
xsession_end_session. session_state_destroy honours the flag:
// 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; }The session is left in the hash, the conn entry is detached
(thread_p->conn_entry->session_p = NULL), and the reaper
ignores it (the timeout branch checks is_keep_session and skips
cleanup). The next reconnect with the same SESSION_ID sees
xsession_check_session succeed.
Per-session prepared-statement registry
Section titled “Per-session prepared-statement registry”The SESSION_STATE::statements field is a singly-linked list of
PREPARED_STATEMENT:
// PREPARED_STATEMENT — session.ctypedef 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 walks the list looking for an
existing entry with the same name (case-insensitive), drops the
old one if found, and prepends the new one. The list is capped at
MAX_PREPARED_STATEMENTS_COUNT = 20; overflow returns
ER_SES_TOO_MANY_STATEMENTS.
Lookup is the symmetric 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);Two layers of cache, then. The session owns the binding — name
to SHA-1 plus the parameter-info blob. The server-wide
XASL cache owns the plan keyed by SHA-1. A second session
that prepares a syntactically identical statement gets a fresh
PREPARED_STATEMENT entry on its own list but reuses the same
XASL_CACHE_ENTRY. This is exactly the Oracle two-tier model.
Holdable cursors as session-scoped query results
Section titled “Holdable cursors as session-scoped query results”Some queries produce results the client wants to read across a
commit boundary (the JDBC HOLD_CURSORS_OVER_COMMIT flag).
Without session storage the result list file would be deleted at
commit by the file manager. The session captures a snapshot:
// SESSION_QUERY_ENTRY — session.ctypedef 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;};session_store_query_entry_info is called by the query manager
for each holdable result; it copies the query manager’s entry into
the session and steals the list_id and temp_vfid pointers
(they are nulled in the source so the query manager will not
reclaim them). The temp files are also flagged preserved via
file_temp_preserve so the file manager will not delete them at
commit:
// 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; }Symmetric session_load_query_entry_info and
session_remove_query_entry_info deal with the fetch and the
final close. A global counter
sessions.num_holdable_cursors tracks how many such results live
across all sessions; the boot path uses this to decide how much
list-file space to reserve.
Binding to TDES — the request entry path
Section titled “Binding to TDES — the request entry path”The single most important thing about the session module is what
happens at request entry. CUBRID’s network dispatcher
(net_server_request in network_sr.c) is invoked once per
client packet by the connection worker. It does not do a
session lookup — the session is already bound:
// 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);The chain of pointers that gets a request handler to “the right state” looks like this:
flowchart LR PKT["network packet\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 vars)"] TDES["LOG_TDES\n(trid, isolation,\nsavepoints, lock set,\nMVCC snapshot)"] 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
The connection entry is the single hub. It carries:
session_id(the int) andsession_p(the resolved pointer). Set bysession_set_conn_entry_dataat create/check time.transaction_id(the per-transaction index intolog_Gl.trantable.all_tdes[]). Manipulated throughset_tran_index/get_tran_indexoncss_conn_entry. The TDES is then trivially resolved withLOG_FIND_TDES(tran_index).
When the worker thread picks up a packet, the dispatcher is
called with thread_p already bound to thread_p->conn_entry.
The session look-up is therefore 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; }This pattern — the session is cached on the connection entry, the
thread carries the connection entry, every request handler is a
function of thread_p — is why server-side handlers like
xsession_set_row_count take only a THREAD_ENTRY * and never
a SESSION_ID. The session is implicit in the call context.
Reference counting
Section titled “Reference counting”The session’s ref_count is incremented on every successful
bind:
// session_state_increase_ref_count — session.cATOMIC_INC_32 (&state_p->ref_count, 1);and decremented when the conn entry releases its hold:
// session_state_decrease_ref_count — session.cATOMIC_INC_32 (&state_p->ref_count, -1);The relevant places are:
session_state_create: bumps for the new conn entry.xsession_check_session: drops the previous session held by the conn entry (if any), then bumps for the new one.xsession_end_session/session_state_destroy: drops on successful end; ifref_count > 0at destroy time the session is left alone ("This session_state is busy, I can't remove") and only the conn-entry binding is severed.css_shutdown_conn: drops if the dying connection still held a session.
A debug-only verifier session_state_verify_ref_count walks
css_Active_conn_anchor and counts how many conns reference the
session; assertion fires if the bookkeeping is off.
Per-connection-entry sub-sessions
Section titled “Per-connection-entry sub-sessions”SESSION_STATE carries two pointer-typed sub-session containers:
load_session_p— populated byxloaddb_initfor aloaddbclient. The bulk loader is long-running, partial-state, and needs to expose interrupt; the session owns the cancel handle.pl_session_p— created insession_state_createat session birth (new PL_SESSION (session_p->id)). The PL session carries the stored-procedure execution stack and the cross-thread interrupt flag for the JavaSP / PL-CSQL invoker. Documented incubrid-pl-javasp.md.
session_stop_attached_threads (called in session_state_uninit)
walks both:
// 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 (); }This is why ending a session is a non-trivial operation: it must unwind any embedded sub-session’s worker threads before destroying the state container.
Cleanup on commit/rollback
Section titled “Cleanup on commit/rollback”The session is not the unit of commit. Most session state survives commit/rollback unchanged: prepared statements, session variables, last insert id, autocommit flag, isolation default, locale.
Three things move with the transaction:
- Holdable cursors that weren’t actually holdable. The query
manager closes its own non-holdable lists at commit; only
the ones that made it into
state_p->queriessurvive. - The TDES itself. Commit/rollback returns the
tran_indexto the trantable’s free list (seecubrid-transaction.md) but the conn entry’stransaction_idis not zeroed — the next transactional request picks the same index back up atlogtb_assign_tran_indextime, or the conn entry’s cached id is reused if still allocated. - The
is_user_activeflag on the TDES is toggled around user activity so the shutdown path can wait.
The reverse boundary — connection-down — is more aggressive.
net_server_conn_down (called by the connection layer when a
TCP socket dies under a worker) calls
session_remove_query_entry_all to pre-emptively close every
holdable cursor for the session. The session itself survives
unless the connection’s xsession_end_session packet was
received with is_keep_session == false.
Concurrency model
Section titled “Concurrency model”A session can be referenced by:
- The conn-worker thread that’s processing the current packet (the common case).
- A vacuum worker that walks the trantable for cleanup but does not touch session state.
- The session-control daemon during a reaper pass.
- A foreign worker thread invoked through the PL session for an in-progress stored procedure.
The mutex on SESSION_STATE is held only across
hash-internal operations (find, insert, erase) and brief reads
of mutable list heads. The list traversals
(session_get_prepared_statement etc.) execute without the
mutex once the cached conn_entry->session_p is dereferenced —
the assumption being that “the conn entry’s session is only
mutated from this conn entry’s worker”, which holds because:
- A second worker for the same conn would have to first bind the session, which goes through the hashmap and takes the mutex.
- The reaper either finds
ref_count > 0(and skips deletion) or finds it== 0(so by definition no other thread is walking the lists).
This is why the prepared-statement and session-variable lists can be simple singly-linked lists with no per-list lock.
Source Walkthrough
Section titled “Source Walkthrough”Symbols grouped by sub-system. Line numbers are observed values
as of this updated: date and decay; anchor on the symbol name.
Lifecycle / hash plumbing
Section titled “Lifecycle / hash plumbing”| Symbol | Role |
|---|---|
ACTIVE_SESSIONS | The static sessions value: hashmap, last id, holdable-cursor counter |
SESSION_STATE | Per-session container struct |
session_state_Descriptor | LF_ENTRY_DESCRIPTOR configuring the lockfree-hashmap behaviours |
session_state_alloc / session_state_free | Hashmap entry malloc/free hooks |
session_state_init / session_state_uninit | Entry recycle hooks (init on insert, uninit on erase) |
session_key_copy / session_key_compare / session_key_hash / session_key_increment | Lockfree-hashmap key callbacks |
session_states_init / session_states_finalize | Module init/teardown (called from server boot/shutdown) |
session_state_create | The actual create-and-insert routine |
session_state_destroy | The actual erase routine (honours is_keep_session) |
session_check_session | Touch active_time; rebind conn entry |
session_remove_expired_sessions | Reaper inner loop |
session_check_timeout | Per-entry expiry decision (consults css_get_session_ids_for_active_connections) |
session_control_daemon_execute / _init / _destroy | The 60-s reaper daemon plumbing |
Server entries (extern from session_sr.c → registered in network_sr.c)
Section titled “Server entries (extern from session_sr.c → registered in network_sr.c)”| Symbol | Role |
|---|---|
xsession_create_new | Spawn a fresh session |
xsession_check_session | Validate / revive an existing one |
xsession_end_session | Tear down (or mark is_keep_session) |
xsession_set_is_keep_session | Flip the keep flag |
xsession_set_row_count / xsession_get_row_count | Per-statement row count |
xsession_set_cur_insert_id / xsession_get_last_insert_id / xsession_reset_cur_insert_id | LAST_INSERT_ID() backing |
xsession_create_prepared_statement / xsession_get_prepared_statement / xsession_delete_prepared_statement | Prepared-statement registry |
xlogin_user | Push username into TDES (tdes->client.set_user) |
xsession_set_session_variables / xsession_get_session_variable / xsession_drop_session_variables | SET @v = … |
xsession_store_query_entry_info / xsession_load_query_entry_info / xsession_remove_query_entry_info / xsession_clear_query_entry_info | Holdable-cursor lifecycle |
xsession_set_tran_auto_commit | Flip session’s autocommit flag |
Network handlers (network_interface_sr.cpp)
Section titled “Network handlers (network_interface_sr.cpp)”| Symbol | Role |
|---|---|
ssession_find_or_create_session | Connect path: try check, fall back to create; ships server_session_key, sysparam blob, db_user/host/program back to client |
ssession_end_session | Disconnect path |
ssession_set_row_count / ssession_get_row_count | Backing for the wire requests |
ssession_get_last_insert_id / ssession_reset_cur_insert_id | Same |
ssession_create_prepared_statement / ssession_get_prepared_statement / ssession_delete_prepared_statement | Same |
ssession_set_session_variables / ssession_get_session_variable / ssession_drop_session_variables | Same |
Network dispatch table (network_sr.c)
Section titled “Network dispatch table (network_sr.c)”| Symbol | Role |
|---|---|
net_Requests[NET_SERVER_SES_CHECK_SESSION].processing_function = ssession_find_or_create_session | The handshake request |
net_Requests[NET_SERVER_SES_END_SESSION] = ssession_end_session | The disconnect request |
NET_SERVER_SES_* family | Wire codes 211 onward (see network.h) |
net_server_request | Dispatcher; reads thread_p->conn_entry, calls handler |
net_server_conn_down | Connection-loss callback; calls session_remove_query_entry_all |
Connection layer integration (connection_sr.c)
Section titled “Connection layer integration (connection_sr.c)”| Symbol | Role |
|---|---|
CSS_CONN_ENTRY::session_id | Cached session id |
CSS_CONN_ENTRY::session_p | Cached resolved SESSION_STATE * |
CSS_CONN_ENTRY::transaction_id (via set_tran_index / get_tran_index) | Cached tran_index for LOG_FIND_TDES |
css_initialize_conn | Initialises session_id = DB_EMPTY_SESSION, session_p = NULL |
css_shutdown_conn | Decrements the session’s ref_count if conn was bound |
css_find_conn_by_tran_index | Reverse lookup (used by interrupt / kill) |
css_get_session_ids_for_active_connections | Reaper helper: lists every connection’s session id |
css_Active_conn_anchor (+ rwlock) | The connection list the reaper walks |
Per-session cache plumbing in session.c
Section titled “Per-session cache plumbing in session.c”| Symbol | Role |
|---|---|
session_get_session_state | The hot-path getter: returns thread_p->conn_entry->session_p |
session_set_conn_entry_data | Writes back conn_entry->session_p and conn_entry->session_id, sets thread_p->private_lru_index, calls pgbuf_thread_variables_init |
session_state_increase_ref_count / session_state_decrease_ref_count | Atomic ref count |
session_state_verify_ref_count (NDEBUG-disabled) | Sanity walk over css_Active_conn_anchor |
Per-session catalogues
Section titled “Per-session catalogues”| Symbol | Role |
|---|---|
PREPARED_STATEMENT + session_create_prepared_statement / session_get_prepared_statement / session_delete_prepared_statement / session_free_prepared_statement | Prepared-statement list (cap 20) |
SESSION_VARIABLE + session_add_variable / session_drop_variable / update_session_variable / free_session_variable | SET @v = … list (cap 20) |
SESSION_QUERY_ENTRY + qentry_to_sentry / sentry_to_qentry / session_preserve_temporary_files / session_free_sentry_data / session_remove_query_entry_all | Holdable-cursor list |
TDES binding (transaction/log_tran_table.c)
Section titled “TDES binding (transaction/log_tran_table.c)”| Symbol | Role |
|---|---|
LOG_FIND_TDES(tran_index) | Macro: log_Gl.trantable.all_tdes[tran_index] |
LOG_FIND_THREAD_TRAN_INDEX(thread_p) | thread_p->tran_index |
logtb_assign_tran_index | Allocates a TDES slot for a session-bound thread on first transactional request |
logtb_set_current_user_active | Toggled by session_state_create / _destroy to mark the TDES user-bound |
logtb_set_current_user_name | Called by ssession_find_or_create_session to put db_user on the TDES |
Position hints (as observed for this revision)
Section titled “Position hints (as observed for this revision)”| Symbol | File | Line |
|---|---|---|
SESSION_STATE | src/session/session.c | 115 |
ACTIVE_SESSIONS / sessions | src/session/session.c | 188 / 205 |
session_state_Descriptor | src/session/session.c | 163 |
session_states_init | src/session/session.c | 609 |
session_states_finalize | src/session/session.c | 634 |
session_state_create | src/session/session.c | 664 |
session_state_destroy | src/session/session.c | 778 |
session_check_session | src/session/session.c | 855 |
session_remove_expired_sessions | src/session/session.c | 929 |
session_check_timeout | src/session/session.c | 1037 |
session_get_session_state | src/session/session.c | 2847 |
session_set_conn_entry_data | src/session/session.c | 2780 |
session_state_increase_ref_count | src/session/session.c | 3138 |
session_state_decrease_ref_count | src/session/session.c | 3163 |
session_control_daemon_execute | src/session/session.c | 561 |
session_create_prepared_statement | src/session/session.c | 1750 |
session_get_prepared_statement | src/session/session.c | 1862 |
session_store_query_entry_info | src/session/session.c | 2507 |
session_get_session_tz_region | src/session/session.c | 3052 |
session_stop_attached_threads | src/session/session.c | 3315 |
xsession_create_new | src/session/session_sr.c | 39 |
xsession_check_session | src/session/session_sr.c | 54 |
xsession_end_session | src/session/session_sr.c | 67 |
xsession_create_prepared_statement | src/session/session_sr.c | 185 |
xsession_get_prepared_statement | src/session/session_sr.c | 204 |
ssession_find_or_create_session | src/communication/network_interface_sr.cpp | 9181 |
ssession_end_session | src/communication/network_interface_sr.cpp | 9315 |
ssession_create_prepared_statement | src/communication/network_interface_sr.cpp | 9484 |
ssession_get_prepared_statement | src/communication/network_interface_sr.cpp | 9584 |
net_server_request | src/communication/network_sr.c | 790 |
net_Requests[NET_SERVER_SES_CHECK_SESSION] = | src/communication/network_sr.c | 616 |
net_server_conn_down | src/communication/network_sr.c | 1040 |
CSS_CONN_ENTRY::session_p | src/connection/connection_defs.h | 478 |
CSS_CONN_ENTRY::session_id | src/connection/connection_defs.h | 480 |
CSS_CONN_ENTRY::set_tran_index / get_tran_index | src/connection/connection_defs.h | 495 / 496 |
css_initialize_conn | src/connection/connection_sr.c | 254 |
css_shutdown_conn (session decref) | src/connection/connection_sr.c | 402 |
css_get_session_ids_for_active_connections | src/connection/connection_sr.c | 1267 |
logtb_set_current_user_active | src/transaction/log_tran_table.c | 2086 |
DB_EMPTY_SESSION | src/compat/dbtype_def.h | 504 |
Cross-check Notes
Section titled “Cross-check Notes”-
vs. cubrid-transaction.md. That document covers
LOG_TDES,TRANTABLE, the savepoint/topops stack, isolation enforcement. This document covers session and explicitly does not re-derive TDES internals; the binding is one direction —conn_entry->transaction_idis an int,LOG_FIND_TDESresolves it. The session has no field of typeLOG_TDES *. Likewise the TDES has noSESSION_IDfield. The connection entry is the only object that carries both. If one says “the session’s transaction” one really means “the TDES whose index is cached on the session’s current connection entry”; the relationship is therefore not strictly 1:1 across reconnects (a kept-alive session reconnects through a fresh conn entry that picks up a freshtran_index, since the previous TDES was already freed at the earlier disconnect’sxtran_server_commit/_abortboundary). -
vs. cubrid-pl-javasp.md. That document treats
PL_SESSIONas the PL/SP execution context. This document treats it as a pointer field onSESSION_STATE—SESSION_STATE::pl_session_pis created insession_state_createand torn down insession_stop_attached_threads. The PL session is a sub-state of the server session, not a peer; PL invocation flows through the session’spl_session_pand inherits the session’s lifetime. -
The
db_Session_idfield. In CS-mode and SA-mode (i.e., not SERVER_MODE),session_get_session_idreturnsdb_Session_idrather thanthread_p->conn_entry->session_id, because there is no connection entry in single-process modes. The hashmap is still used; the absence ofconn_entry->session_pcache means that everysession_get_session_statecall in SA-mode pays for a hashmap lookup. This is fine because there is exactly one session in SA-mode. -
Cached
session_pvs. expired session. If the reaper expires a session whose conn entry still cachedsession_p, the next request via that conn entry would use a dangling pointer. The guard is the reaper rule: a session withref_count > 0is refreshed, not removed. The conn entry’s bind is ref-counted, so it is impossible for the reaper to free a session that is still cached. The verifiersession_state_verify_ref_countis the belt-and-braces.
Open Questions
Section titled “Open Questions”-
One session per connection or many? The data structure permits a single
SESSION_STATEto be referenced (viaref_count) by more than one connection entry simultaneously, but in practice the only path that incrementsref_countis the binding path of one conn entry; reconnect drops the old conn’s ref before the new conn’s ref takes hold. Whether two simultaneously-live connections can share a session id (some drivers ask for “session pinning” across a connection-pool reconnect storm) is not a documented behaviour.session_state_verify_ref_countwould still pass because it counts all conns matchingconn->session_id == session->id, but no assertion forbids duplicates. Worth a designed test. -
Broker process restart. When a
cub_casbroker process is recycled (BROKER_RESTART_TIMEetc.) the CAS exits holding any open server-side sessions. Whether the cub_server reaper sees the dropped TCP connection and immediately frees the session, or whether theis_keep_sessionflag (set by the CAS pool prior to graceful shutdown) holds it long enough for the next CAS to pick up, depends on whether broker shutdown is graceful or kill-9. Both paths exist. -
Statement registry per thread vs. per session. The prepared-statement list is per-session and unlocked, relying on the one-conn-↔-one-worker invariant. A future reorg that permits two workers of the same conn entry concurrently would require either a per-session lock around the list walks or promotion to lock-free structures.
-
Why
MAX_PREPARED_STATEMENTS_COUNT = 20? Large applications hitER_SES_TOO_MANY_STATEMENTS; the JDBC driver papers over it via LRU eviction, but the cap is a hard limit, not a soft hint. Bumping it would also linearly slow each list walk.
Sources
Section titled “Sources”src/session/session.c— session state hash, lifecycle, prepared-statement and holdable-cursor lists, daemonsrc/session/session.h— public extern surfacesrc/session/session_sr.c—xsession_*server entriessrc/connection/connection_defs.h—CSS_CONN_ENTRYdefinition withsession_pandsession_idfieldssrc/connection/connection_sr.c—css_initialize_conn,css_shutdown_conn,css_get_session_ids_for_active_connectionssrc/communication/network_sr.c—net_server_requestdispatcher,NET_SERVER_SES_*table population,net_server_conn_downsrc/communication/network_interface_sr.cpp—ssession_*network handlerssrc/transaction/log_tran_table.c— TDES table,LOG_FIND_TDES,logtb_set_current_user_activesrc/compat/dbtype_def.h—SESSION_IDtypedef,DB_EMPTY_SESSIONconstant- Sibling docs:
cubrid-transaction.md,cubrid-pl-javasp.md