-
Notifications
You must be signed in to change notification settings - Fork 666
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Improve the performance of CitusHasBeenLoaded function for a database that does not do CREATE EXTENSION citus but load citus.so. #7123
Conversation
* MetadataCache.extensionLoaded and MetadataCache.extensionNotLoaded | ||
* cannot be true at the same time. | ||
*/ | ||
Assert(!(MetadataCache.extensionLoaded && MetadataCache.extensionNotLoaded)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: I think most of these checks become easier to understand with a 3 element enum
instead of 2 booleans. Something like this: EXTENSION_LOADED_UNKNOWN
EXTENSION_LOADED
EXTENSION_NOT_LOADED
(feel free to choose better names)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overall really nice and simple solution. If we can have a test for cross backend invalidation that would be great. You would probably need either isolation tests or the python testing framework for that.
@@ -2185,7 +2186,19 @@ HasOverlappingShardInterval(ShardInterval **shardIntervalArray, | |||
bool | |||
CitusHasBeenLoaded(void) | |||
{ | |||
if (!MetadataCache.extensionLoaded || creating_extension) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should probably call AcceptInvalidationMessages()
here. To make sure we've processed any outstanding messages.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have reorganized this part of the code such that when creating_extension == true
for citus we do not change the cached value of extensionLoadedState
but simply return false.
This change is to make the following scneario work:
BEGIN;
SET client_min_messages TO ERROR;
SET search_path TO public;
CREATE EXTENSION citus;
create table l1 (a int unique);
SELECT create_reference_table('l1');
When creating_extension=true
, If we change the extensionCreatedState to false during creating_extension, it stayed false after this line causing the subsequent create_reference_table
to fail.
This might be due to that we invalidate cache during preprocessing phase of the citus utility hook when
CREATE EXTENSION citus;
runs. And if we recreated the cache during creating_extension (we set extensionCreatedState to false), we do not get another cache invalidation message that would invalidate the cache.
AcceptInvalidationMessages();
did not eliminate the issue.
I opted for not changing the cache during creating_extension
phase.
This change should not have any impact on performance since
if (creating_extension)
used to run previously every time even when the cache is valid.
{ | ||
return; | ||
} | ||
|
||
CitusTableCacheEntrySlot *cacheSlot = | ||
hash_search(DistTableCacheHash, hashKey, HASH_FIND, &foundInCache); | ||
if (foundInCache) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we can now remove the InvalidateMetadataSystemCache call below here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removing InvalidateMetadataSystemCache call here causes check-vanilla
test suit to fail. Specifically check_index
is failing consistently in the CI if I remove this code. Let me investigate why, a bit further.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It turns out that
REINDEX SCHEMA CONCURRENTLY pg_catalog;
command causes the relcache invalidation of pg_dist tables which are under pg_catalog. If we do not invalidate Metadatacache, it will end up with stale oids causing the subsequent commands to fail.
For instance, the following command
\d
will run a SELECT command that invokes citus hooks and will cause failed cached lookups of pg_dist* tables.
So we need to invalidate the cache when one of the pg_dist* tables got invalidated.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have restored the previous code not to regress check-vanilla.
We will track the issue of pg_dist_ tables getting invalidated in a seperate PR.
Note that, even if I restore the original code there are likely two issues with the original code
-
When a pg_dist table other than
pg_dist_partition
gets invalidated for example using
REINDEX TABLE pg_dist_background_task;
we currently do not refresh its oid in the Metadata cache. -
When
pg_dist_partition
gets invalidated we unnecessarily nuke the entire MetadataCache.
Codecov Report
@@ Coverage Diff @@
## main #7123 +/- ##
==========================================
- Coverage 93.20% 93.19% -0.02%
==========================================
Files 274 274
Lines 59232 59240 +8
==========================================
- Hits 55208 55207 -1
- Misses 4024 4033 +9 |
7d80c17
to
c0ea89a
Compare
static bool | ||
NeedsMetadataCacheInvalidation(Oid relationId) | ||
{ | ||
if (relationId == MetadataCache.distPartitionRelationId || |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought it was surprising that this was checking the table Oids instead of the index Oids, because the index oids are actually changed when a REINDEX CONCURRENTLY happens.
So I had expected that an invalidation would be sent on the index its oid instead of the one for the table that it's an index for. As it turns out it's more interesting than that. Invalidations are sent for many things when a REINDEX cuncurrently happens:
- The oid of the table
- The oid of the old index
- The oid of the new index
One thing I'm worried about with the current change is that we might flush the cache more often than before, maybe even much more often depending on the workload. My suggestions:
- Let's keep the current behaviour in this PR, flush the full cache on
distPartitionRelationId
invalidations. At least we don't regress onREINDEX SCHEMA CONCURRENLTY pg_catalog
, while also not regressing on performance by flushing more often than before to handle this pretty edge-casy bug. - Let's create a second PR that fixes it for all indexes. (maybe only create an issue initially, and we can prioritise fixing the bug).
- I think it would be better if that PR would not listen for invalidations for table oid, but instead only for index oids. Then when a cached index gets an invalidation message, we should not invalidate the whole cache, but instead only set the matching
distXyzIndexId
field toInvalidOid
.
PS. The minimal example of the original issue that I could find is by running a REINDEX CONCURRENTLY twice on pg_dist_partition_logical_relid_index
, i.e.:
REINDEX INDEX CONCURRENTLY pg_catalog.pg_dist_partition_logical_relid_index;
REINDEX INDEX CONCURRENTLY pg_catalog.pg_dist_partition_logical_relid_index;
/* | ||
* Ensure value is valid, we can't do some checks during CREATE | ||
* EXTENSION. This is important to register some invalidation callbacks. | ||
*/ | ||
CitusHasBeenLoaded(); /* lgtm[cpp/return-value-ignored] */ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why remove this? The original comment makes it sounds like it is important.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, hmm I guess this is related to moving InitializeDistCache to _PG_init. Because we register the callback there, we don't do that here anymore.
CacheRegisterRelcacheCallback(InvalidateDistRelationCacheCallback, | ||
(Datum) 0); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why was this call moved here instead of staying in InitializeDistCache
@@ -122,6 +124,8 @@ CompressionTypeStr_type extern_CompressionTypeStr = NULL; | |||
IsColumnarTableAmTable_type extern_IsColumnarTableAmTable = NULL; | |||
ReadColumnarOptions_type extern_ReadColumnarOptions = NULL; | |||
|
|||
void InvalidateDistRelationCacheCallback(Datum argument, Oid relationId); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be in a header instead (if we want to keep calling it here, see other comment).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(pressed the wrong button, needs at one small change)
try: | ||
cur2.execute("SELECT citus_version();") | ||
# Conn1 dropped the extension. citus_version udf | ||
# cannot be found.sycopg.errors.UndefinedFunction | ||
# is expected here. | ||
except psycopg.errors.UndefinedFunction: | ||
cur2.execute("SELECT 1;") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should use pytest.raises instead to assert that the exception is thrown.
try: | |
cur2.execute("SELECT citus_version();") | |
# Conn1 dropped the extension. citus_version udf | |
# cannot be found.sycopg.errors.UndefinedFunction | |
# is expected here. | |
except psycopg.errors.UndefinedFunction: | |
cur2.execute("SELECT 1;") | |
with pytest.raises(psycopg.errors.UndefinedFunction): | |
# Conn1 dropped the extension. citus_version udf | |
# cannot be found.sycopg.errors.UndefinedFunction | |
# is expected here. | |
cur2.execute("SELECT citus_version();") |
be0aeca
to
8904630
Compare
0311dd4
to
d3c3fb8
Compare
… that does not do CREATE EXTENSION citus but load citus.so. (#7123) For a database that does not create the citus extension by running ` CREATE EXTENSION citus;` `CitusHasBeenLoaded ` function ends up querying the `pg_extension` table every time it is invoked. This is not an ideal situation for a such a database. The idea in this PR is as follows: ### A new field in MetadataCache. Add a new variable `extensionCreatedState `of the following type: ``` typedef enum ExtensionCreatedState { UNKNOWN = 0, CREATED = 1, NOTCREATED = 2, } ExtensionCreatedState; ``` When the MetadataCache is invalidated, `ExtensionCreatedState` will be set to UNKNOWN. ### Invalidate MetadataCache when CREATE/DROP/ALTER EXTENSION citus commands are run. - Register a callback function, named `InvalidateDistRelationCacheCallback`, for relcache invalidation during the shared library initialization for `citus.so`. This callback function is invoked in all the backends whenever the relcache is invalidated in one of the backends. (This could be caused many DDLs operations). - In the cache invalidation callback,` InvalidateDistRelationCacheCallback`, invalidate `MetadataCache` zeroing it out. - In `CitusHasBeenLoaded`, perform the costly citus is loaded check only if the `MetadataCache` is not valid. ### Downsides Any relcache invalidation (caused by various DDL operations) will case Citus MetadataCache to get invalidated. Most of the time it will be unnecessary. But we rely on that DDL operations on relations will not be too frequent.
For a database that does not create the citus extension by running
CREATE EXTENSION citus;
CitusHasBeenLoaded
function ends up querying thepg_extension
table every time it is invoked. This is not an ideal situation for a such a database.The idea in this PR is as follows:
A new field in MetadataCache.
Add a new variable
extensionCreatedState
of the following type:When the MetadataCache is invalidated,
ExtensionCreatedState
will be set to UNKNOWN.Invalidate MetadataCache when CREATE/DROP/ALTER EXTENSION citus commands are run.
Register a callback function, named
InvalidateDistRelationCacheCallback
, for relcache invalidation during the shared library initialization forcitus.so
. This callback function is invoked in all the backends whenever the relcache is invalidated in one of the backends. (This could be caused many DDLs operations).In the cache invalidation callback,
InvalidateDistRelationCacheCallback
, invalidateMetadataCache
zeroing it out.In
CitusHasBeenLoaded
, perform the costly citus is loaded check only if theMetadataCache
is not valid.Downsides
Any relcache invalidation (caused by various DDL operations) will case Citus MetadataCache to get invalidated. Most of the time it will be unnecessary. But we rely on that DDL operations on relations will not be too frequent.