#include "catalog/pg_ts_template.h"
#include "catalog/pg_type.h"
#include "commands/dbcommands.h"
+#include "common/hashfn.h"
#include "funcapi.h"
#include "mb/pg_wchar.h"
#include "miscadmin.h"
* activeSearchPath is always the actually active path; it points to
* to baseSearchPath which is the list derived from namespace_search_path.
*
- * If baseSearchPathValid is false, then baseSearchPath (and other
- * derived variables) need to be recomputed from namespace_search_path.
- * We mark it invalid upon an assignment to namespace_search_path or receipt
- * of a syscache invalidation event for pg_namespace. The recomputation
- * is done during the next lookup attempt.
+ * If baseSearchPathValid is false, then baseSearchPath (and other derived
+ * variables) need to be recomputed from namespace_search_path, or retrieved
+ * from the search path cache if there haven't been any syscache
+ * invalidations. We mark it invalid upon an assignment to
+ * namespace_search_path or receipt of a syscache invalidation event for
+ * pg_namespace or pg_authid. The recomputation is done during the next
+ * lookup attempt.
*
* Any namespaces mentioned in namespace_search_path that are not readable
* by the current user ID are simply left out of baseSearchPath; so
/* The above four values are valid only if baseSearchPathValid */
static bool baseSearchPathValid = true;
+static bool searchPathCacheValid = false;
+static MemoryContext SearchPathCacheContext = NULL;
+
+typedef struct SearchPathCacheKey
+{
+ const char *searchPath;
+ Oid roleid;
+} SearchPathCacheKey;
+
+typedef struct SearchPathCacheEntry
+{
+ SearchPathCacheKey key;
+ List *oidlist; /* namespace OIDs that pass ACL checks */
+ List *finalPath; /* cached final computed search path */
+ Oid firstNS; /* first explicitly-listed namespace */
+ bool temp_missing;
+ bool forceRecompute; /* force recompute of finalPath */
+
+ /* needed for simplehash */
+ char status;
+} SearchPathCacheEntry;
/*
* myTempNamespace is InvalidOid until and unless a TEMP namespace is set up
bool include_out_arguments, int pronargs,
int **argnumbers);
+/*
+ * Recomputing the namespace path can be costly when done frequently, such as
+ * when a function has search_path set in proconfig. Add a search path cache
+ * that can be used by recomputeNamespacePath().
+ *
+ * The search path cache is based on a wrapper around a simplehash hash table
+ * (nsphash, defined below). The spcache wrapper deals with OOM while trying
+ * to initialize a key, and also offers a more convenient API.
+ */
+
+static inline uint32
+spcachekey_hash(SearchPathCacheKey key)
+{
+ const unsigned char *bytes = (const unsigned char *) key.searchPath;
+ int blen = strlen(key.searchPath);
+
+ return hash_combine(hash_bytes(bytes, blen),
+ hash_uint32(key.roleid));
+}
+
+static inline bool
+spcachekey_equal(SearchPathCacheKey a, SearchPathCacheKey b)
+{
+ return a.roleid == b.roleid &&
+ strcmp(a.searchPath, b.searchPath) == 0;
+}
+
+#define SH_PREFIX nsphash
+#define SH_ELEMENT_TYPE SearchPathCacheEntry
+#define SH_KEY_TYPE SearchPathCacheKey
+#define SH_KEY key
+#define SH_HASH_KEY(tb, key) spcachekey_hash(key)
+#define SH_EQUAL(tb, a, b) spcachekey_equal(a, b)
+#define SH_SCOPE static inline
+#define SH_DECLARE
+#define SH_DEFINE
+#include "lib/simplehash.h"
+
+/*
+ * We only expect a small number of unique search_path strings to be used. If
+ * this cache grows to an unreasonable size, reset it to avoid steady-state
+ * memory growth. Most likely, only a few of those entries will benefit from
+ * the cache, and the cache will be quickly repopulated with such entries.
+ */
+#define SPCACHE_RESET_THRESHOLD 256
+
+static nsphash_hash * SearchPathCache = NULL;
+
+/*
+ * Create search path cache.
+ */
+static void
+spcache_init(void)
+{
+ Assert(SearchPathCacheContext);
+
+ if (SearchPathCache)
+ return;
+
+ /* arbitrary initial starting size of 16 elements */
+ SearchPathCache = nsphash_create(SearchPathCacheContext, 16, NULL);
+ searchPathCacheValid = true;
+}
+
+/*
+ * Reset and reinitialize search path cache.
+ */
+static void
+spcache_reset(void)
+{
+ Assert(SearchPathCacheContext);
+ Assert(SearchPathCache);
+
+ MemoryContextReset(SearchPathCacheContext);
+ SearchPathCache = NULL;
+
+ spcache_init();
+}
+
+static uint32
+spcache_members(void)
+{
+ return SearchPathCache->members;
+}
+
+/*
+ * Look up or insert entry in search path cache.
+ *
+ * Initialize key safely, so that OOM does not leave an entry without a valid
+ * key. Caller must ensure that non-key contents are properly initialized.
+ */
+static SearchPathCacheEntry *
+spcache_insert(const char *searchPath, Oid roleid)
+{
+ SearchPathCacheEntry *entry;
+ bool found;
+ SearchPathCacheKey cachekey = {
+ .searchPath = searchPath,
+ .roleid = roleid
+ };
+
+ /*
+ * If a new entry is created, we must ensure that it's properly
+ * initialized. Set the cache invalid temporarily, so that if the
+ * MemoryContextStrdup() below raises an OOM, the cache will be reset on
+ * the next use, clearing the uninitialized entry.
+ */
+ searchPathCacheValid = false;
+
+ entry = nsphash_insert(SearchPathCache, cachekey, &found);
+
+ /* ensure that key is initialized and the rest is zeroed */
+ if (!found)
+ {
+ entry->key.searchPath = MemoryContextStrdup(SearchPathCacheContext, searchPath);
+ entry->key.roleid = roleid;
+ entry->oidlist = NIL;
+ entry->finalPath = NIL;
+ entry->firstNS = InvalidOid;
+ entry->temp_missing = false;
+ entry->forceRecompute = false;
+ /* do not touch entry->status, used by simplehash */
+ }
+
+ searchPathCacheValid = true;
+ return entry;
+}
/*
* RangeVarGetRelidExtended
*/
baseSearchPathValid = false; /* may need to rebuild list */
+ searchPathCacheValid = false;
}
}
/*
- * recomputeNamespacePath - recompute path derived variables if needed.
+ * Look up namespace IDs and perform ACL checks. Return newly-allocated list.
*/
-static void
-recomputeNamespacePath(void)
+static List *
+preprocessNamespacePath(const char *searchPath, Oid roleid,
+ bool *temp_missing)
{
- Oid roleid = GetUserId();
char *rawname;
List *namelist;
List *oidlist;
- List *newpath;
ListCell *l;
- bool temp_missing;
- Oid firstNS;
- bool pathChanged;
- MemoryContext oldcxt;
-
- /* Do nothing if path is already valid. */
- if (baseSearchPathValid && namespaceUser == roleid)
- return;
- /* Need a modifiable copy of namespace_search_path string */
- rawname = pstrdup(namespace_search_path);
+ /* Need a modifiable copy */
+ rawname = pstrdup(searchPath);
/* Parse string into list of identifiers */
if (!SplitIdentifierString(rawname, ',', &namelist))
* already been accepted.) Don't make duplicate entries, either.
*/
oidlist = NIL;
- temp_missing = false;
+ *temp_missing = false;
foreach(l, namelist)
{
char *curname = (char *) lfirst(l);
namespaceId = get_namespace_oid(rname, true);
ReleaseSysCache(tuple);
if (OidIsValid(namespaceId) &&
- !list_member_oid(oidlist, namespaceId) &&
object_aclcheck(NamespaceRelationId, namespaceId, roleid,
- ACL_USAGE) == ACLCHECK_OK &&
- InvokeNamespaceSearchHook(namespaceId, false))
+ ACL_USAGE) == ACLCHECK_OK)
oidlist = lappend_oid(oidlist, namespaceId);
}
}
{
/* pg_temp --- substitute temp namespace, if any */
if (OidIsValid(myTempNamespace))
- {
- if (!list_member_oid(oidlist, myTempNamespace) &&
- InvokeNamespaceSearchHook(myTempNamespace, false))
- oidlist = lappend_oid(oidlist, myTempNamespace);
- }
+ oidlist = lappend_oid(oidlist, myTempNamespace);
else
{
/* If it ought to be the creation namespace, set flag */
if (oidlist == NIL)
- temp_missing = true;
+ *temp_missing = true;
}
}
else
/* normal namespace reference */
namespaceId = get_namespace_oid(curname, true);
if (OidIsValid(namespaceId) &&
- !list_member_oid(oidlist, namespaceId) &&
object_aclcheck(NamespaceRelationId, namespaceId, roleid,
- ACL_USAGE) == ACLCHECK_OK &&
- InvokeNamespaceSearchHook(namespaceId, false))
+ ACL_USAGE) == ACLCHECK_OK)
oidlist = lappend_oid(oidlist, namespaceId);
}
}
+ pfree(rawname);
+ list_free(namelist);
+
+ return oidlist;
+}
+
+/*
+ * Remove duplicates, run namespace search hooks, and prepend
+ * implicitly-searched namespaces. Return newly-allocated list.
+ *
+ * If an object_access_hook is present, this must always be recalculated. It
+ * may seem that duplicate elimination is not dependent on the result of the
+ * hook, but if a hook returns different results on different calls for the
+ * same namespace ID, then it could affect the order in which that namespace
+ * appears in the final list.
+ */
+static List *
+finalNamespacePath(List *oidlist, Oid *firstNS)
+{
+ List *finalPath = NIL;
+ ListCell *lc;
+
+ foreach(lc, oidlist)
+ {
+ Oid namespaceId = lfirst_oid(lc);
+
+ if (!list_member_oid(finalPath, namespaceId))
+ {
+ if (InvokeNamespaceSearchHook(namespaceId, false))
+ finalPath = lappend_oid(finalPath, namespaceId);
+ }
+ }
+
/*
* Remember the first member of the explicit list. (Note: this is
* nominally wrong if temp_missing, but we need it anyway to distinguish
* explicit from implicit mention of pg_catalog.)
*/
- if (oidlist == NIL)
- firstNS = InvalidOid;
+ if (finalPath == NIL)
+ *firstNS = InvalidOid;
else
- firstNS = linitial_oid(oidlist);
+ *firstNS = linitial_oid(finalPath);
/*
* Add any implicitly-searched namespaces to the list. Note these go on
* the front, not the back; also notice that we do not check USAGE
* permissions for these.
*/
- if (!list_member_oid(oidlist, PG_CATALOG_NAMESPACE))
- oidlist = lcons_oid(PG_CATALOG_NAMESPACE, oidlist);
+ if (!list_member_oid(finalPath, PG_CATALOG_NAMESPACE))
+ finalPath = lcons_oid(PG_CATALOG_NAMESPACE, finalPath);
if (OidIsValid(myTempNamespace) &&
- !list_member_oid(oidlist, myTempNamespace))
- oidlist = lcons_oid(myTempNamespace, oidlist);
+ !list_member_oid(finalPath, myTempNamespace))
+ finalPath = lcons_oid(myTempNamespace, finalPath);
+
+ return finalPath;
+}
+
+/*
+ * Retrieve search path information from the cache; or if not there, fill
+ * it. The returned entry is valid only until the next call to this function.
+ *
+ * We also determine if the newly-computed finalPath is the same as the
+ * prevPath passed by the caller (i.e. a no-op or a real change?). It's more
+ * efficient to check for a change in this function than the caller, because
+ * we can avoid unnecessary temporary copies of the previous path.
+ */
+static const SearchPathCacheEntry *
+cachedNamespacePath(const char *searchPath, Oid roleid, List *prevPath,
+ bool *same)
+{
+ MemoryContext oldcxt;
+ SearchPathCacheEntry *entry;
+ List *prevPathCopy = NIL;
+
+ spcache_init();
+
+ /* invalidate cache if necessary */
+ if (!searchPathCacheValid || spcache_members() >= SPCACHE_RESET_THRESHOLD)
+ {
+ /* prevPath will be destroyed; make temp copy for later comparison */
+ prevPathCopy = list_copy(prevPath);
+ prevPath = prevPathCopy;
+ spcache_reset();
+ }
+
+ entry = spcache_insert(searchPath, roleid);
/*
- * We want to detect the case where the effective value of the base search
- * path variables didn't change. As long as we're doing so, we can avoid
- * copying the OID list unnecessarily.
+ * An OOM may have resulted in a cache entry with mising 'oidlist' or
+ * 'finalPath', so just compute whatever is missing.
*/
- if (baseCreationNamespace == firstNS &&
- baseTempCreationPending == temp_missing &&
- equal(oidlist, baseSearchPath))
+
+ if (entry->oidlist == NIL)
{
- pathChanged = false;
+ oldcxt = MemoryContextSwitchTo(SearchPathCacheContext);
+ entry->oidlist = preprocessNamespacePath(searchPath, roleid,
+ &entry->temp_missing);
+ MemoryContextSwitchTo(oldcxt);
}
- else
+
+ /*
+ * If a hook is set, we must recompute finalPath from the oidlist each
+ * time, because the hook may affect the result. This is still much faster
+ * than recomputing from the string (and doing catalog lookups and ACL
+ * checks).
+ */
+ if (entry->finalPath == NIL || object_access_hook ||
+ entry->forceRecompute)
{
- pathChanged = true;
+ /*
+ * Do not free the stale value of entry->finalPath until we've
+ * performed the comparison, in case it's aliased by prevPath (which
+ * can only happen when recomputing due to an object_access_hook).
+ */
+ List *finalPath;
- /* Must save OID list in permanent storage. */
- oldcxt = MemoryContextSwitchTo(TopMemoryContext);
- newpath = list_copy(oidlist);
+ oldcxt = MemoryContextSwitchTo(SearchPathCacheContext);
+ finalPath = finalNamespacePath(entry->oidlist,
+ &entry->firstNS);
MemoryContextSwitchTo(oldcxt);
- /* Now safe to assign to state variables. */
- list_free(baseSearchPath);
- baseSearchPath = newpath;
- baseCreationNamespace = firstNS;
- baseTempCreationPending = temp_missing;
+ *same = equal(prevPath, finalPath);
+
+ list_free(entry->finalPath);
+ entry->finalPath = finalPath;
+
+ /*
+ * If an object_access_hook set when finalPath is calculated, the
+ * result may be affected by the hook. Force recomputation of
+ * finalPath the next time this cache entry is used, even if the
+ * object_access_hook is not set at that time.
+ */
+ entry->forceRecompute = object_access_hook ? true : false;
+ }
+ else
+ {
+ /* use cached version of finalPath */
+ *same = equal(prevPath, entry->finalPath);
+ }
+
+ list_free(prevPathCopy);
+
+ return entry;
+}
+
+/*
+ * recomputeNamespacePath - recompute path derived variables if needed.
+ */
+static void
+recomputeNamespacePath(void)
+{
+ Oid roleid = GetUserId();
+ bool newPathEqual;
+ bool pathChanged;
+ const SearchPathCacheEntry *entry;
+
+ /* Do nothing if path is already valid. */
+ if (baseSearchPathValid && namespaceUser == roleid)
+ return;
+
+ entry = cachedNamespacePath(namespace_search_path, roleid, baseSearchPath,
+ &newPathEqual);
+
+ if (baseCreationNamespace == entry->firstNS &&
+ baseTempCreationPending == entry->temp_missing &&
+ newPathEqual)
+ {
+ pathChanged = false;
+ }
+ else
+ {
+ pathChanged = true;
}
+ /* Now safe to assign to state variables. */
+ baseSearchPath = entry->finalPath;
+ baseCreationNamespace = entry->firstNS;
+ baseTempCreationPending = entry->temp_missing;
+
/* Mark the path valid. */
baseSearchPathValid = true;
namespaceUser = roleid;
*/
if (pathChanged)
activePathGeneration++;
-
- /* Clean up. */
- pfree(rawname);
- list_free(namelist);
- list_free(oidlist);
}
/*
myTempNamespaceSubID = GetCurrentSubTransactionId();
baseSearchPathValid = false; /* need to rebuild list */
+ searchPathCacheValid = false;
}
/*
myTempNamespace = InvalidOid;
myTempToastNamespace = InvalidOid;
baseSearchPathValid = false; /* need to rebuild list */
+ searchPathCacheValid = false;
/*
* Reset the temporary namespace flag in MyProc. We assume that
myTempNamespace = InvalidOid;
myTempToastNamespace = InvalidOid;
baseSearchPathValid = false; /* need to rebuild list */
+ searchPathCacheValid = false;
/*
* Reset the temporary namespace flag in MyProc. We assume that
* We mark the path as needing recomputation, but don't do anything until
* it's needed. This avoids trying to do database access during GUC
* initialization, or outside a transaction.
+ *
+ * This does not invalidate the search path cache, so if this value had
+ * been previously set and no syscache invalidations happened,
+ * recomputation may not be necessary.
*/
baseSearchPathValid = false;
}
}
else
{
+ SearchPathCacheContext = AllocSetContextCreate(
+ TopMemoryContext, "search_path processing cache",
+ ALLOCSET_DEFAULT_SIZES);
+
/*
* In normal mode, arrange for a callback on any syscache invalidation
* of pg_namespace or pg_authid rows. (Changing a role name may affect
(Datum) 0);
/* Force search path to be recomputed on next use */
baseSearchPathValid = false;
+ searchPathCacheValid = false;
}
}
static void
NamespaceCallback(Datum arg, int cacheid, uint32 hashvalue)
{
- /* Force search path to be recomputed on next use */
+ /*
+ * Force search path to be recomputed on next use, also invalidating the
+ * search path cache (because namespace names, ACLs, or role names may
+ * have changed).
+ */
baseSearchPathValid = false;
+ searchPathCacheValid = false;
}
/*