From 89d5ccd1110dfb961d1031c4b969c2a112aacb27 Mon Sep 17 00:00:00 2001 From: "okbob@github.com" Date: Fri, 30 May 2025 22:44:58 +0200 Subject: [PATCH 07/15] local HASHTAB for currently used session variables and low level access functions Session variables are stored in session memory in a dedicated hash table. They are set by the LET command and read by the SELECT command. The access rights should be checked. Hash entries related to dropped session variables are not released. The memory cleaning is implemented in memory-cleaning-after-DROP-VARIABLE patch. --- doc/src/sgml/glossary.sgml | 5 +- src/backend/commands/session_variable.c | 428 ++++++++++++++++++++++++ src/include/commands/session_variable.h | 3 + src/tools/pgindent/typedefs.list | 2 + 4 files changed, 436 insertions(+), 2 deletions(-) diff --git a/doc/src/sgml/glossary.sgml b/doc/src/sgml/glossary.sgml index 8f697bb2266..b29e3dde1ac 100644 --- a/doc/src/sgml/glossary.sgml +++ b/doc/src/sgml/glossary.sgml @@ -1732,8 +1732,9 @@ A persistent database object that holds a value in session memory. This value is private to each session and is released when the session ends. - Read or write access to session variables is controlled by privileges, - similar to other database objects. + The default value of the session variable is null. Read or write access + to session variables is controlled by privileges, similar to other database + objects. For more information, see . diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c index f641e00c1ac..dbc054795bb 100644 --- a/src/backend/commands/session_variable.c +++ b/src/backend/commands/session_variable.c @@ -14,14 +14,442 @@ */ #include "postgres.h" +#include "access/htup_details.h" #include "catalog/pg_variable.h" #include "catalog/namespace.h" #include "catalog/pg_type.h" #include "commands/session_variable.h" #include "miscadmin.h" #include "parser/parse_type.h" +#include "storage/lmgr.h" +#include "storage/proc.h" #include "utils/builtins.h" +#include "utils/datum.h" +#include "utils/inval.h" #include "utils/lsyscache.h" +#include "utils/memutils.h" +#include "utils/syscache.h" + +/* + * The values of session variables are stored in the backend's private memory + * in the dedicated memory context SVariableMemoryContext in binary format. + * They are stored in the "sessionvars" hash table, whose key is the OID of the + * variable. However, the OID is not good enough to identify a session + * variable: concurrent sessions could drop the session variable and create a + * new one, which could be assigned the same OID. To ensure that the values + * stored in memory and the catalog definition match, we also keep track of + * the "create_lsn". Before any access to the variable values, we need to + * check if the LSN stored in memory matches the LSN in the catalog. If there + * is a mismatch between the LSNs, or if the OID is not present in pg_variable + * at all, the value stored in memory is released. + */ +typedef struct SVariableData +{ + Oid varid; /* pg_variable OID of the variable (hash key) */ + XLogRecPtr create_lsn; + + bool isnull; + Datum value; + + Oid typid; + int16 typlen; + bool typbyval; + + bool is_domain; + + /* + * domain_check_extra holds cached domain metadata. This "extra" is + * usually stored in fn_mcxt. We do not have access to that memory + * context for session variables, but we can use TopTransactionContext + * instead. A fresh value is forced when we detect we are in a different + * transaction (the local transaction ID differs from + * domain_check_extra_lxid). + */ + void *domain_check_extra; + LocalTransactionId domain_check_extra_lxid; + + /* + * Stored value and type description can be outdated when we receive a + * sinval message. We then have to check if the stored data are still + * trustworthy. + */ + bool is_valid; + + uint32 hashvalue; /* used for pairing sinval message */ +} SVariableData; + +typedef SVariableData *SVariable; + +static HTAB *sessionvars = NULL; /* hash table for session variables */ + +static MemoryContext SVariableMemoryContext = NULL; + +/* + * Callback function for session variable invalidation. + */ +static void +pg_variable_cache_callback(Datum arg, int cacheid, uint32 hashvalue) +{ + HASH_SEQ_STATUS status; + SVariable svar; + + elog(DEBUG1, "pg_variable_cache_callback %u %u", cacheid, hashvalue); + + Assert(sessionvars); + + /* + * If the hashvalue is not specified, we have to recheck all currently + * used session variables. Since we can't tell the exact session variable + * from its hashvalue, we have to iterate over all items in the hash + * bucket. + */ + hash_seq_init(&status, sessionvars); + + while ((svar = (SVariable) hash_seq_search(&status)) != NULL) + { + if (hashvalue == 0 || svar->hashvalue == hashvalue) + { + svar->is_valid = false; + } + } +} + +/* + * Release stored value, free memory + */ +static void +free_session_variable_value(SVariable svar) +{ + /* clean the current value */ + if (!svar->isnull) + { + if (!svar->typbyval) + pfree(DatumGetPointer(svar->value)); + + svar->isnull = true; + } + + svar->value = (Datum) 0; +} + +/* + * Returns true when the entry in pg_variable is consistent with the given + * session variable. + */ +static bool +is_session_variable_valid(SVariable svar) +{ + HeapTuple tp; + bool result = false; + + Assert(OidIsValid(svar->varid)); + + tp = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(svar->varid)); + + if (HeapTupleIsValid(tp)) + { + /* + * The OID alone is not enough as an unique identifier, because OID + * values get recycled, and a new session variable could have got the + * same OID. We do a second check against the 64-bit LSN when the + * variable was created. + */ + if (svar->create_lsn == ((Form_pg_variable) GETSTRUCT(tp))->varcreate_lsn) + result = true; + + ReleaseSysCache(tp); + } + + return result; +} + +/* + * Initialize attributes cached in "svar" + */ +static void +setup_session_variable(SVariable svar, Oid varid) +{ + HeapTuple tup; + Form_pg_variable varform; + + Assert(OidIsValid(varid)); + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for session variable %u", varid); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + svar->varid = varid; + svar->create_lsn = varform->varcreate_lsn; + + svar->typid = varform->vartype; + + get_typlenbyval(svar->typid, &svar->typlen, &svar->typbyval); + + svar->is_domain = (get_typtype(varform->vartype) == TYPTYPE_DOMAIN); + svar->domain_check_extra = NULL; + svar->domain_check_extra_lxid = InvalidLocalTransactionId; + + svar->isnull = true; + svar->value = (Datum) 0; + + svar->is_valid = true; + + svar->hashvalue = GetSysCacheHashValue1(VARIABLEOID, + ObjectIdGetDatum(varid)); + + ReleaseSysCache(tup); +} + +/* + * Assign a new value to the session variable. It is copied to + * SVariableMemoryContext if necessary. + * + * If any error happens, the existing value won't be modified. + */ +static void +set_session_variable(SVariable svar, Datum value, bool isnull) +{ + Datum newval; + SVariableData locsvar, + *_svar; + + Assert(svar); + Assert(!isnull || value == (Datum) 0); + + /* + * Use typbyval, typbylen from session variable only when they are + * trustworthy (the invalidation message was not accepted for this + * variable). If the variable might be invalid, force setup. + * + * Do not overwrite the passed session variable until we can be certain + * that no error can be thrown. + */ + if (!svar->is_valid) + { + setup_session_variable(&locsvar, svar->varid); + _svar = &locsvar; + } + else + _svar = svar; + + if (!isnull) + { + MemoryContext oldcxt = MemoryContextSwitchTo(SVariableMemoryContext); + + newval = datumCopy(value, _svar->typbyval, _svar->typlen); + + MemoryContextSwitchTo(oldcxt); + } + else + newval = value; + + free_session_variable_value(svar); + + elog(DEBUG1, "session variable \"%s.%s\" (oid:%u) has new value", + get_namespace_name(get_session_variable_namespace(svar->varid)), + get_session_variable_name(svar->varid), + svar->varid); + + /* no more error expected, so we can overwrite the old variable now */ + if (svar != _svar) + memcpy(svar, _svar, sizeof(SVariableData)); + + svar->value = newval; + svar->isnull = isnull; +} + +/* + * Create the hash table for storing session variables. + */ +static void +create_sessionvars_hashtables(void) +{ + HASHCTL vars_ctl; + + Assert(!sessionvars); + + if (!SVariableMemoryContext) + { + /* read sinval messages */ + CacheRegisterSyscacheCallback(VARIABLEOID, + pg_variable_cache_callback, + (Datum) 0); + + /* we need our own long-lived memory context */ + SVariableMemoryContext = + AllocSetContextCreate(TopMemoryContext, + "session variables", + ALLOCSET_START_SMALL_SIZES); + } + + memset(&vars_ctl, 0, sizeof(vars_ctl)); + vars_ctl.keysize = sizeof(Oid); + vars_ctl.entrysize = sizeof(SVariableData); + vars_ctl.hcxt = SVariableMemoryContext; + + sessionvars = hash_create("Session variables", 64, &vars_ctl, + HASH_ELEM | HASH_BLOBS | HASH_CONTEXT); +} + +/* + * Search a session variable in the hash table given its OID. If it + * doesn't exist, then insert it there. + * + * The caller is responsible for doing permission checks. + * + * As a side effect, this function acquires a AccessShareLock on the + * session variable until the end of the transaction. + */ +static SVariable +get_session_variable(Oid varid) +{ + SVariable svar; + bool found; + + /* protect the used session variable against DROP */ + LockDatabaseObject(VariableRelationId, varid, 0, AccessShareLock); + + if (!sessionvars) + create_sessionvars_hashtables(); + + svar = (SVariable) hash_search(sessionvars, &varid, + HASH_ENTER, &found); + + if (found) + { + if (!svar->is_valid) + { + /* + * If there was an invalidation message, the variable might still + * be valid, but we have to check with the system catalog. + */ + if (is_session_variable_valid(svar)) + svar->is_valid = true; + else + /* if the value cannot be validated, we have to discard it */ + free_session_variable_value(svar); + } + } + else + svar->is_valid = false; + + /* + * Force setup for not yet initialized variables or variables that cannot + * be validated. + */ + if (!svar->is_valid) + { + setup_session_variable(svar, varid); + + elog(DEBUG1, "session variable \"%s.%s\" (oid:%u) has assigned entry in memory (emitted by READ)", + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid), + varid); + } + + /* ensure the returned data is still of the correct domain */ + if (svar->is_domain) + { + /* + * Store "extra" for domain_check() in TopTransactionContext. When we + * are in a new transaction, domain_check_extra cache is not valid any + * more. + */ + if (svar->domain_check_extra_lxid != MyProc->vxid.lxid) + svar->domain_check_extra = NULL; + + domain_check(svar->value, svar->isnull, + svar->typid, &svar->domain_check_extra, + TopTransactionContext); + + svar->domain_check_extra_lxid = MyProc->vxid.lxid; + } + + return svar; +} + +/* + * Store the given value in a session variable in the cache. + * + * The caller is responsible for doing permission checks. + * + * As a side effect, this function acquires a AccessShareLock on the session + * variable until the end of the transaction. + */ +void +SetSessionVariable(Oid varid, Datum value, bool isNull) +{ + SVariable svar; + bool found; + + /* protect used session variable against DROP */ + LockDatabaseObject(VariableRelationId, varid, 0, AccessShareLock); + + if (!sessionvars) + create_sessionvars_hashtables(); + + svar = (SVariable) hash_search(sessionvars, &varid, + HASH_ENTER, &found); + + if (!found) + { + setup_session_variable(svar, varid); + + elog(DEBUG1, "session variable \"%s.%s\" (oid:%u) has assigned entry in memory (emitted by WRITE)", + get_namespace_name(get_session_variable_namespace(svar->varid)), + get_session_variable_name(svar->varid), + varid); + } + + /* if this fails, it won't change the stored value */ + set_session_variable(svar, value, isNull); +} + +/* + * Returns a copy of the value stored in a variable. + */ +static inline Datum +copy_session_variable_value(SVariable svar, bool *isNull) +{ + Datum value; + + /* force copy of non NULL value */ + if (!svar->isnull) + { + value = datumCopy(svar->value, svar->typbyval, svar->typlen); + *isNull = false; + } + else + { + value = (Datum) 0; + *isNull = true; + } + + return value; +} + +/* + * Returns a copy of the value of the session variable (in the current memory + * context). The caller is responsible for permission checks. + */ +Datum +GetSessionVariable(Oid varid, bool *isNull) +{ + SVariable svar; + + svar = get_session_variable(varid); + + /* + * Although "svar" is freshly validated in this point, svar->is_valid can + * be false, if an invalidation message was processed during the domain + * check. But the variable and all its dependencies are locked now, so we + * don't need to repeat the validation. + */ + return copy_session_variable_value(svar, isNull); +} /* * Creates a new variable diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h index 49f36ac6885..9f5c6e30fbd 100644 --- a/src/include/commands/session_variable.h +++ b/src/include/commands/session_variable.h @@ -19,6 +19,9 @@ #include "parser/parse_node.h" #include "nodes/parsenodes.h" +extern void SetSessionVariable(Oid varid, Datum value, bool isNull); +extern Datum GetSessionVariable(Oid varid, bool *isNull); + extern ObjectAddress CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt); #endif diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 7f427231602..315c76ac03f 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -2665,6 +2665,8 @@ SSL_CTX STARTUPINFO STRLEN SV +SVariableData +SVariable SYNCHRONIZATION_BARRIER SYSTEM_INFO SampleScan -- 2.51.1