Skip to content
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

Merged
merged 28 commits into from
Sep 5, 2023

Conversation

emelsimsek
Copy link
Contributor

@emelsimsek emelsimsek commented Aug 16, 2023

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.

@emelsimsek emelsimsek requested a review from JelteF August 16, 2023 10:36
@emelsimsek emelsimsek marked this pull request as draft August 16, 2023 10:36
@emelsimsek emelsimsek changed the title Cache a new flag, extensionNotLoaded, to improve the performance of CitusHasBeenLoaded function for the databases that does not create the citus extension. Cache a new flag, extensionNotLoaded, to improve the performance of CitusHasBeenLoaded function. Aug 16, 2023
* MetadataCache.extensionLoaded and MetadataCache.extensionNotLoaded
* cannot be true at the same time.
*/
Assert(!(MetadataCache.extensionLoaded && MetadataCache.extensionNotLoaded));
Copy link
Contributor

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)

Copy link
Contributor

@JelteF JelteF left a 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)
Copy link
Contributor

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.

Copy link
Contributor Author

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)
Copy link
Contributor

@JelteF JelteF Aug 17, 2023

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.

Copy link
Contributor Author

@emelsimsek emelsimsek Aug 21, 2023

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.

Copy link
Contributor Author

@emelsimsek emelsimsek Aug 29, 2023

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.

Copy link
Contributor Author

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

  1. 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.

  2. When pg_dist_partition gets invalidated we unnecessarily nuke the entire MetadataCache.

@codecov
Copy link

codecov bot commented Aug 18, 2023

Codecov Report

Merging #7123 (d3c3fb8) into main (1d540b6) will decrease coverage by 0.02%.
The diff coverage is 100.00%.

@@            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     

@emelsimsek emelsimsek force-pushed the MetadataCacheExtensionNotLoaded branch from 7d80c17 to c0ea89a Compare August 21, 2023 11:27
@emelsimsek emelsimsek changed the title Cache a new flag, extensionNotLoaded, to improve the performance of CitusHasBeenLoaded function. Improve the performance of CitusHasBeenLoaded function for databases that does not CREATE citus extension but load the shared library citus.so. Aug 28, 2023
@emelsimsek emelsimsek changed the title Improve the performance of CitusHasBeenLoaded function for databases that does not CREATE citus extension but load the shared library citus.so. Improve the performance of CitusHasBeenLoaded function for a database that does not do CREATE EXTENSION citus but load citus.so. Aug 29, 2023
static bool
NeedsMetadataCacheInvalidation(Oid relationId)
{
if (relationId == MetadataCache.distPartitionRelationId ||
Copy link
Contributor

@JelteF JelteF Aug 31, 2023

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:

  1. The oid of the table
  2. The oid of the old index
  3. 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:

  1. Let's keep the current behaviour in this PR, flush the full cache on distPartitionRelationId invalidations. At least we don't regress on REINDEX SCHEMA CONCURRENLTY pg_catalog, while also not regressing on performance by flushing more often than before to handle this pretty edge-casy bug.
  2. 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).
  3. 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 to InvalidOid.

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;

Comment on lines -932 to -936
/*
* 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] */
Copy link
Contributor

@JelteF JelteF Sep 4, 2023

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.

Copy link
Contributor

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);

Copy link
Contributor

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);
Copy link
Contributor

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).

Copy link
Contributor

@JelteF JelteF left a 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)

Comment on lines 11 to 17
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;")
Copy link
Contributor

@JelteF JelteF Sep 4, 2023

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.

Suggested 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;")
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();")

@emelsimsek emelsimsek force-pushed the MetadataCacheExtensionNotLoaded branch from be0aeca to 8904630 Compare September 4, 2023 12:38
@emelsimsek emelsimsek marked this pull request as ready for review September 5, 2023 09:57
@emelsimsek emelsimsek force-pushed the MetadataCacheExtensionNotLoaded branch from 0311dd4 to d3c3fb8 Compare September 5, 2023 10:00
@emelsimsek emelsimsek merged commit a849570 into main Sep 5, 2023
37 checks passed
@emelsimsek emelsimsek deleted the MetadataCacheExtensionNotLoaded branch September 5, 2023 10:29
francisjodi pushed a commit that referenced this pull request Nov 13, 2023
… 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants