Skip to content

CUBRID Server Session — Per-Client State, Prepared-Statement Registry, and TDES Binding

Contents:

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:

  1. 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 actual SESSION_STATE * cached on the connection entry so that subsequent requests on the same socket pay no hash lookup.

  2. 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 a LOG_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 its THREAD_ENTRY so LOG_FIND_THREAD_TRAN_INDEX(thread_p) returns the right LOG_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.

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.

Theoretical conceptCUBRID name
Session identifierSESSION_ID (typedef’d unsigned int in compat/dbtype_def.h)
Session state containerSESSION_STATE (session.c)
Server-wide session tableACTIVE_SESSIONS::states_hashmap (session.c)
Empty / unbound sessionDB_EMPTY_SESSION = 0 (compat/dbtype_def.h)
Connection entry session cacheCSS_CONN_ENTRY::session_p + ::session_id (connection_defs.h)
Per-session prepared statementPREPARED_STATEMENT (session.c)
Holdable cursor / query resultSESSION_QUERY_ENTRY (session.c)
Server-wide XASL plan cacheXASL_CACHE_ENTRY (xasl_cache.h) — keyed by SHA-1
Per-session locale regionSESSION_STATE::session_tz_region
Per-session sysparam overridesSESSION_STATE::session_parameters (array of SESSION_PARAM)
Session ↔ transaction bindingCSS_CONN_ENTRY::transaction_id (set by set_tran_index)
Session ↔ thread bindingTHREAD_ENTRY::conn_entry->session_p
Reaper daemonsession_Control_daemon running session_remove_expired_sessions
Server entry — find or createxsession_create_new, xsession_check_session (session_sr.c)
Server entry — endxsession_end_session (session_sr.c)
Network handler — connectssession_find_or_create_session (network_interface_sr.cpp)
Network handler — disconnectssession_end_session (network_interface_sr.cpp)

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.

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 whole module is anchored in one static ACTIVE_SESSIONS value, declared at file scope in 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;

The hashmap is the typedef-aliased lock-free hash:

// session_hashmap_type — session.c
using 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 ();
#endif

The 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 1000 and the free-list growth params are (2, 50) — meaning the hash auto-grows by 2x once 50% full.
  • LF_EM_USING_MUTEX selects mutex-based entry locking (each SESSION_STATE owns its own pthread_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_id is bumped atomically with ATOMIC_INC_32 on every create. The hashmap’s internal key_increment is 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 via ATOMIC_CAS_32 afterwards.
// 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.

// 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 to lockfree_hashmap reads these by offsetof.
  • 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 ...), and queries (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 override intl_*, tz_*, isolation defaults, etc.; the array is unpacked from the connect packet in ssession_find_or_create_session via sysprm_session_init_session_parameters.
  • Sub-sessions. load_session_p (the bulk-loader’s parser-and-interrupt context for a loaddb invocation) and pl_session_p (the PL/JavaSP execution context — see cubrid-pl-javasp.md). These are owned by the session and destroyed in session_state_uninit; they exist on the session rather than the transaction precisely because they may outlive a single statement boundary.
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);
#endif

Three side effects matter beyond the hash insert:

  1. 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.
  2. The connection entry is back-pointed to the session (session_set_conn_entry_data writes conn_entry->session_p and conn_entry->session_id). All later requests on this connection bypass the hash lookup.
  3. The TDES is marked user-active (logtb_set_current_user_active(thread_p, true) flips tdes->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.c
if (!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_time is stale but which still has a live connection in css_Active_conn_anchor is refreshed in place, not removed. This handles the case where a long query simply held the session past the timeout without re-touching active_time.
  • The actual deletion is two-phase to dodge a lock-free hash trap: session_state_uninit is called inside the iterator, but states_hashmap.erase is called outside the iterator (the comment in session_remove_expired_sessions says “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.

The SESSION_STATE::statements field is a singly-linked list of 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 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.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;
};

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) and session_p (the resolved pointer). Set by session_set_conn_entry_data at create/check time.
  • transaction_id (the per-transaction index into log_Gl.trantable.all_tdes[]). Manipulated through set_tran_index / get_tran_index on css_conn_entry. The TDES is then trivially resolved with LOG_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.

The session’s ref_count is incremented on every successful bind:

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

and decremented when the conn entry releases its hold:

// session_state_decrease_ref_count — session.c
ATOMIC_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; if ref_count > 0 at 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.

SESSION_STATE carries two pointer-typed sub-session containers:

  • load_session_p — populated by xloaddb_init for a loaddb client. The bulk loader is long-running, partial-state, and needs to expose interrupt; the session owns the cancel handle.
  • pl_session_p — created in session_state_create at 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 in cubrid-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.

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->queries survive.
  • The TDES itself. Commit/rollback returns the tran_index to the trantable’s free list (see cubrid-transaction.md) but the conn entry’s transaction_id is not zeroed — the next transactional request picks the same index back up at logtb_assign_tran_index time, or the conn entry’s cached id is reused if still allocated.
  • The is_user_active flag 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.

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.

Symbols grouped by sub-system. Line numbers are observed values as of this updated: date and decay; anchor on the symbol name.

SymbolRole
ACTIVE_SESSIONSThe static sessions value: hashmap, last id, holdable-cursor counter
SESSION_STATEPer-session container struct
session_state_DescriptorLF_ENTRY_DESCRIPTOR configuring the lockfree-hashmap behaviours
session_state_alloc / session_state_freeHashmap entry malloc/free hooks
session_state_init / session_state_uninitEntry recycle hooks (init on insert, uninit on erase)
session_key_copy / session_key_compare / session_key_hash / session_key_incrementLockfree-hashmap key callbacks
session_states_init / session_states_finalizeModule init/teardown (called from server boot/shutdown)
session_state_createThe actual create-and-insert routine
session_state_destroyThe actual erase routine (honours is_keep_session)
session_check_sessionTouch active_time; rebind conn entry
session_remove_expired_sessionsReaper inner loop
session_check_timeoutPer-entry expiry decision (consults css_get_session_ids_for_active_connections)
session_control_daemon_execute / _init / _destroyThe 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)”
SymbolRole
xsession_create_newSpawn a fresh session
xsession_check_sessionValidate / revive an existing one
xsession_end_sessionTear down (or mark is_keep_session)
xsession_set_is_keep_sessionFlip the keep flag
xsession_set_row_count / xsession_get_row_countPer-statement row count
xsession_set_cur_insert_id / xsession_get_last_insert_id / xsession_reset_cur_insert_idLAST_INSERT_ID() backing
xsession_create_prepared_statement / xsession_get_prepared_statement / xsession_delete_prepared_statementPrepared-statement registry
xlogin_userPush username into TDES (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_commitFlip session’s autocommit flag

Network handlers (network_interface_sr.cpp)

Section titled “Network handlers (network_interface_sr.cpp)”
SymbolRole
ssession_find_or_create_sessionConnect path: try check, fall back to create; ships server_session_key, sysparam blob, db_user/host/program back to client
ssession_end_sessionDisconnect path
ssession_set_row_count / ssession_get_row_countBacking for the wire requests
ssession_get_last_insert_id / ssession_reset_cur_insert_idSame
ssession_create_prepared_statement / ssession_get_prepared_statement / ssession_delete_prepared_statementSame
ssession_set_session_variables / ssession_get_session_variable / ssession_drop_session_variablesSame
SymbolRole
net_Requests[NET_SERVER_SES_CHECK_SESSION].processing_function = ssession_find_or_create_sessionThe handshake request
net_Requests[NET_SERVER_SES_END_SESSION] = ssession_end_sessionThe disconnect request
NET_SERVER_SES_* familyWire codes 211 onward (see network.h)
net_server_requestDispatcher; reads thread_p->conn_entry, calls handler
net_server_conn_downConnection-loss callback; calls session_remove_query_entry_all

Connection layer integration (connection_sr.c)

Section titled “Connection layer integration (connection_sr.c)”
SymbolRole
CSS_CONN_ENTRY::session_idCached session id
CSS_CONN_ENTRY::session_pCached resolved SESSION_STATE *
CSS_CONN_ENTRY::transaction_id (via set_tran_index / get_tran_index)Cached tran_index for LOG_FIND_TDES
css_initialize_connInitialises session_id = DB_EMPTY_SESSION, session_p = NULL
css_shutdown_connDecrements the session’s ref_count if conn was bound
css_find_conn_by_tran_indexReverse lookup (used by interrupt / kill)
css_get_session_ids_for_active_connectionsReaper helper: lists every connection’s session id
css_Active_conn_anchor (+ rwlock)The connection list the reaper walks
SymbolRole
session_get_session_stateThe hot-path getter: returns thread_p->conn_entry->session_p
session_set_conn_entry_dataWrites 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_countAtomic ref count
session_state_verify_ref_count (NDEBUG-disabled)Sanity walk over css_Active_conn_anchor
SymbolRole
PREPARED_STATEMENT + session_create_prepared_statement / session_get_prepared_statement / session_delete_prepared_statement / session_free_prepared_statementPrepared-statement list (cap 20)
SESSION_VARIABLE + session_add_variable / session_drop_variable / update_session_variable / free_session_variableSET @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_allHoldable-cursor list

TDES binding (transaction/log_tran_table.c)

Section titled “TDES binding (transaction/log_tran_table.c)”
SymbolRole
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_indexAllocates a TDES slot for a session-bound thread on first transactional request
logtb_set_current_user_activeToggled by session_state_create / _destroy to mark the TDES user-bound
logtb_set_current_user_nameCalled 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)”
SymbolFileLine
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. 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_id is an int, LOG_FIND_TDES resolves it. The session has no field of type LOG_TDES *. Likewise the TDES has no SESSION_ID field. 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 fresh tran_index, since the previous TDES was already freed at the earlier disconnect’s xtran_server_commit / _abort boundary).

  • vs. cubrid-pl-javasp.md. That document treats PL_SESSION as the PL/SP execution context. This document treats it as a pointer field on SESSION_STATESESSION_STATE::pl_session_p is created in session_state_create and torn down in session_stop_attached_threads. The PL session is a sub-state of the server session, not a peer; PL invocation flows through the session’s pl_session_p and inherits the session’s lifetime.

  • The db_Session_id field. In CS-mode and SA-mode (i.e., not SERVER_MODE), session_get_session_id returns db_Session_id rather than thread_p->conn_entry->session_id, because there is no connection entry in single-process modes. The hashmap is still used; the absence of conn_entry->session_p cache means that every session_get_session_state call in SA-mode pays for a hashmap lookup. This is fine because there is exactly one session in SA-mode.

  • Cached session_p vs. expired session. If the reaper expires a session whose conn entry still cached session_p, the next request via that conn entry would use a dangling pointer. The guard is the reaper rule: a session with ref_count > 0 is 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 verifier session_state_verify_ref_count is the belt-and-braces.

  • One session per connection or many? The data structure permits a single SESSION_STATE to be referenced (via ref_count) by more than one connection entry simultaneously, but in practice the only path that increments ref_count is 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_count would still pass because it counts all conns matching conn->session_id == session->id, but no assertion forbids duplicates. Worth a designed test.

  • Broker process restart. When a cub_cas broker process is recycled (BROKER_RESTART_TIME etc.) 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 the is_keep_session flag (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 hit ER_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.

  • src/session/session.c — session state hash, lifecycle, prepared-statement and holdable-cursor lists, daemon
  • src/session/session.h — public extern surface
  • src/session/session_sr.cxsession_* server entries
  • src/connection/connection_defs.hCSS_CONN_ENTRY definition with session_p and session_id fields
  • 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_* table population, net_server_conn_down
  • src/communication/network_interface_sr.cppssession_* network handlers
  • src/transaction/log_tran_table.c — TDES table, LOG_FIND_TDES, logtb_set_current_user_active
  • src/compat/dbtype_def.hSESSION_ID typedef, DB_EMPTY_SESSION constant
  • Sibling docs: cubrid-transaction.md, cubrid-pl-javasp.md