public inbox for [email protected]help / color / mirror / Atom feed
[PATCH v24 7/7] Add support for PRESERVE 3+ messages / 3 participants [nested] [flat]
* [PATCH v24 7/7] Add support for PRESERVE @ 2021-02-10 06:19 Dilip Kumar <[email protected]> 0 siblings, 0 replies; 3+ messages in thread From: Dilip Kumar @ 2021-02-10 06:19 UTC (permalink / raw) Now the compression method can be changed without forcing a table rewrite, by including the old method in the PRESERVE list. �For supporting this the column will maintain the dependency with all the supported compression methods. �So whenever the compression method is altered the dependency is added with the new compression method and the dependency is removed for all the old compression methods which are not given in the preserve list. �If PRESERVE ALL is given then all the dependency is maintained. Dilip Kumar based on the patches from Ildus Kurbangaliev. Design input from Tomas Vondra and Robert Haas Reviewed by Robert Haas, Tomas Vondra, Alexander Korotkov and Justin Pryzby --- doc/src/sgml/ref/alter_table.sgml | 10 +- src/backend/catalog/pg_depend.c | 7 + src/backend/commands/Makefile | 1 + src/backend/commands/compressioncmds.c | 300 ++++++++++++++++++++ src/backend/commands/tablecmds.c | 126 ++++---- src/backend/executor/nodeModifyTable.c | 12 +- src/backend/nodes/copyfuncs.c | 17 +- src/backend/nodes/equalfuncs.c | 15 +- src/backend/nodes/outfuncs.c | 15 +- src/backend/parser/gram.y | 52 +++- src/backend/parser/parse_utilcmd.c | 2 +- src/bin/pg_dump/pg_dump.c | 101 +++++++ src/bin/pg_dump/pg_dump.h | 15 +- src/bin/psql/tab-complete.c | 7 + src/include/commands/defrem.h | 7 + src/include/commands/tablecmds.h | 2 + src/include/nodes/nodes.h | 1 + src/include/nodes/parsenodes.h | 16 +- src/test/regress/expected/compression.out | 37 ++- src/test/regress/expected/compression_1.out | 39 ++- src/test/regress/expected/create_index.out | 56 ++-- src/test/regress/sql/compression.sql | 9 + 22 files changed, 739 insertions(+), 108 deletions(-) create mode 100644 src/backend/commands/compressioncmds.c diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml index 0bd0c1a503..c9f443a59c 100644 --- a/doc/src/sgml/ref/alter_table.sgml +++ b/doc/src/sgml/ref/alter_table.sgml @@ -54,7 +54,7 @@ ALTER TABLE [ IF EXISTS ] <replaceable class="parameter">name</replaceable> ALTER [ COLUMN ] <replaceable class="parameter">column_name</replaceable> SET ( <replaceable class="parameter">attribute_option</replaceable> = <replaceable class="parameter">value</replaceable> [, ... ] ) ALTER [ COLUMN ] <replaceable class="parameter">column_name</replaceable> RESET ( <replaceable class="parameter">attribute_option</replaceable> [, ... ] ) ALTER [ COLUMN ] <replaceable class="parameter">column_name</replaceable> SET STORAGE { PLAIN | EXTERNAL | EXTENDED | MAIN } - ALTER [ COLUMN ] <replaceable class="parameter">column_name</replaceable> SET COMPRESSION <replaceable class="parameter">compression_method</replaceable> + ALTER [ COLUMN ] <replaceable class="parameter">column_name</replaceable> SET COMPRESSION <replaceable class="parameter">compression_method</replaceable> [ PRESERVE (<replaceable class="parameter">compression_preserve_list</replaceable>) | PRESERVE ALL ] ADD <replaceable class="parameter">table_constraint</replaceable> [ NOT VALID ] ADD <replaceable class="parameter">table_constraint_using_index</replaceable> ALTER CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> [ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ] @@ -387,7 +387,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM <varlistentry> <term> - <literal>SET COMPRESSION <replaceable class="parameter">compression_method</replaceable></literal> + <literal>SET COMPRESSION <replaceable class="parameter">compression_method</replaceable> [ PRESERVE (<replaceable class="parameter">compression_preserve_list</replaceable>) | PRESERVE ALL ]</literal> </term> <listitem> <para> @@ -395,6 +395,12 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM methods are <literal>pglz</literal> and <literal>lz4</literal>. <literal>lz4</literal> is available only if <literal>--with-lz4</literal> was used when building <productname>PostgreSQL</productname>. + The <literal>PRESERVE</literal> list contains a list of compression + methods used in the column and determines which of them may be kept. + Without <literal>PRESERVE</literal> or if any of the pre-existing + compression methods are not preserved, the table will be rewritten. If + <literal>PRESERVE ALL</literal> is specified, then all of the existing + methods will be preserved and the table will not be rewritten. </para> </listitem> </varlistentry> diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 63da24322d..dd376484b7 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -17,6 +17,7 @@ #include "access/genam.h" #include "access/htup_details.h" #include "access/table.h" +#include "catalog/pg_am.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_collation.h" @@ -125,6 +126,12 @@ recordMultipleDependencies(const ObjectAddress *depender, if (referenced->objectId == DEFAULT_COLLATION_OID) ignore_systempin = true; } + /* + * Record the dependency on compression access method for handling + * preserve. + */ + if (referenced->classId == AccessMethodRelationId) + ignore_systempin = true; } else Assert(!version); diff --git a/src/backend/commands/Makefile b/src/backend/commands/Makefile index e8504f0ae4..a7395ad77d 100644 --- a/src/backend/commands/Makefile +++ b/src/backend/commands/Makefile @@ -21,6 +21,7 @@ OBJS = \ cluster.o \ collationcmds.o \ comment.o \ + compressioncmds.o \ constraint.o \ conversioncmds.o \ copy.o \ diff --git a/src/backend/commands/compressioncmds.c b/src/backend/commands/compressioncmds.c new file mode 100644 index 0000000000..fd6db24e7f --- /dev/null +++ b/src/backend/commands/compressioncmds.c @@ -0,0 +1,300 @@ +/*------------------------------------------------------------------------- + * + * compressioncmds.c + * Routines for SQL commands for attribute compression methods + * + * Portions Copyright (c) 1996-2017, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * + * IDENTIFICATION + * src/backend/commands/compressioncmds.c + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "access/compressamapi.h" +#include "access/heapam.h" +#include "access/htup_details.h" +#include "access/reloptions.h" +#include "catalog/catalog.h" +#include "catalog/dependency.h" +#include "catalog/indexing.h" +#include "catalog/pg_attribute.h" +#include "catalog/pg_depend.h" +#include "commands/defrem.h" +#include "commands/tablecmds.h" +#include "miscadmin.h" +#include "nodes/parsenodes.h" +#include "utils/builtins.h" +#include "utils/fmgroids.h" +#include "utils/lsyscache.h" + +/* + * Get list of all supported compression methods for the given attribute. + * + * We maintain dependency of the attribute on the pg_am row for the current + * compression AM and all the preserved compression AM. So scan pg_depend and + * find the column dependency on the pg_am. Collect the list of access method + * oids on which this attribute has a dependency. + */ +static List * +lookup_attribute_compression(Oid attrelid, AttrNumber attnum, List *oldcmoids) +{ + LOCKMODE lock = AccessShareLock; + HeapTuple tup; + Relation rel; + SysScanDesc scan; + ScanKeyData key[3]; + List *cmoids = NIL; + + rel = table_open(DependRelationId, lock); + + ScanKeyInit(&key[0], + Anum_pg_depend_classid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(RelationRelationId)); + ScanKeyInit(&key[1], + Anum_pg_depend_objid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(attrelid)); + ScanKeyInit(&key[2], + Anum_pg_depend_objsubid, + BTEqualStrategyNumber, F_INT4EQ, + Int32GetDatum((int32) attnum)); + + scan = systable_beginscan(rel, DependDependerIndexId, true, + NULL, 3, key); + + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_depend depform = (Form_pg_depend) GETSTRUCT(tup); + + if (depform->refclassid == AccessMethodRelationId) + cmoids = list_append_unique_oid(cmoids, depform->refobjid); + } + + systable_endscan(scan); + table_close(rel, lock); + + return cmoids; +} + +/* + * Remove the attribute dependency on the old compression methods + * + * Scan the pg_depend and search the attribute dependency on the pg_am. Remove + * dependency on previous am which is not preserved. The list of non-preserved + * AMs is given in cmoids. + */ +static void +remove_old_dependencies(Oid attrelid, AttrNumber attnum, List *cmoids) +{ + LOCKMODE lock = AccessShareLock; + HeapTuple tup; + Relation rel; + SysScanDesc scan; + ScanKeyData key[3]; + + rel = table_open(DependRelationId, lock); + + ScanKeyInit(&key[0], + Anum_pg_depend_classid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(RelationRelationId)); + ScanKeyInit(&key[1], + Anum_pg_depend_objid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(attrelid)); + ScanKeyInit(&key[2], + Anum_pg_depend_objsubid, + BTEqualStrategyNumber, F_INT4EQ, + Int32GetDatum((int32) attnum)); + + scan = systable_beginscan(rel, DependDependerIndexId, true, + NULL, 3, key); + + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_depend depform = (Form_pg_depend) GETSTRUCT(tup); + + if (list_member_oid(cmoids, depform->refobjid)) + { + Assert(depform->refclassid == AccessMethodRelationId); + CatalogTupleDelete(rel, &tup->t_self); + } + } + + systable_endscan(scan); + table_close(rel, lock); +} + +/* + * Check whether the given compression method oid is supported by + * the target attribute. + */ +bool +IsCompressionSupported(Form_pg_attribute att, Oid cmoid) +{ + List *cmoids = NIL; + + /* Check whether it is same as the current compression oid */ + if (cmoid == att->attcompression) + return true; + + /* Check the oid in all preserved compresion methods */ + cmoids = lookup_attribute_compression(att->attrelid, att->attnum, NULL); + if (list_member_oid(cmoids, cmoid)) + return true; + else + return false; +} + +/* + * In binary upgrade mode add the dependencies for all the preserved compression + * method. + */ +static void +BinaryUpgradeAddPreserve(Form_pg_attribute att, List *preserve) +{ + ListCell *cell; + + foreach(cell, preserve) + { + char *cmname_p = strVal(lfirst(cell)); + Oid cmoid_p = get_compression_am_oid(cmname_p, false); + + add_column_compression_dependency(att->attrelid, att->attnum, cmoid_p); + } +} + +/* + * Get the compression method oid based on the compression method name. When + * compression is not specified returns default attribute compression. It is + * possible case for CREATE TABLE and ADD COLUMN commands where COMPRESSION + * syntax is optional. + * + * For ALTER command, check all the supported compression methods for the + * attribute and if the preserve list is not passed or some of the old + * compression methods are not given in the preserved list then delete + * dependency from the old compression methods and force the table rewrite. + */ +Oid +GetAttributeCompression(Form_pg_attribute att, ColumnCompression *compression, + bool *need_rewrite) +{ + Oid cmoid; + char typstorage = get_typstorage(att->atttypid); + ListCell *cell; + + /* + * No compression for the plain/external storage, refer comments atop + * attcompression parameter in pg_attribute.h + */ + if (!IsStorageCompressible(typstorage)) + { + if (compression == NULL) + return InvalidOid; + + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("column data type %s does not support compression", + format_type_be(att->atttypid)))); + } + + /* fallback to default compression if it's not specified */ + if (compression == NULL) + return GetDefaultToastCompression(); + + cmoid = get_compression_am_oid(compression->cmname, false); + +#ifndef HAVE_LIBLZ4 + if (cmoid == LZ4_COMPRESSION_AM_OID) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("not built with lz4 support"))); +#endif + + /* + * Determine if the column needs rewrite or not. Rewrite conditions: SET + * COMPRESSION without PRESERVE - SET COMPRESSION with PRESERVE but not + * with full list of previous access methods. + */ + if (need_rewrite != NULL) + { + List *previous_cmoids = NIL; + + *need_rewrite = false; + + /* + * In binary upgrade mode, just create a dependency on all preserved + * methods. + */ + if (IsBinaryUpgrade) + { + BinaryUpgradeAddPreserve(att, compression->preserve); + return cmoid; + } + + /* If we have preserved all then rewrite is not required */ + if (compression->preserve_all) + return cmoid; + + previous_cmoids = + lookup_attribute_compression(att->attrelid, att->attnum, NULL); + + foreach(cell, compression->preserve) + { + char *cmname_p = strVal(lfirst(cell)); + Oid cmoid_p = get_compression_am_oid(cmname_p, false); + + if (!list_member_oid(previous_cmoids, cmoid_p)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("\"%s\" compression access method cannot be preserved", cmname_p))); + + /* + * Remove from previous list, also protect from duplicate + * entries in the PRESERVE list + */ + previous_cmoids = list_delete_oid(previous_cmoids, cmoid_p); + } + + /* delete the current cmoid from the list */ + previous_cmoids = list_delete_oid(previous_cmoids, cmoid); + + /* + * If the list of previous Oids is not empty after deletions then + * we need to rewrite tuples in the table. Also remove the dependency + * on the old compression methods which are no longer preserved. + */ + if (list_length(previous_cmoids) != 0) + { + remove_old_dependencies(att->attrelid, att->attnum, + previous_cmoids); + *need_rewrite = true; + } + + /* Cleanup */ + list_free(previous_cmoids); + } + + return cmoid; +} + +/* + * Construct ColumnCompression node from the compression method oid. + */ +ColumnCompression * +MakeColumnCompression(Oid attcompression) +{ + ColumnCompression *node; + + if (!OidIsValid(attcompression)) + return NULL; + + node = makeNode(ColumnCompression); + node->cmname = get_am_name(attcompression); + + return node; +} diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 586a92f0c1..2a1841c353 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -530,7 +530,9 @@ static void ATExecEnableRowSecurity(Relation rel); static void ATExecDisableRowSecurity(Relation rel); static void ATExecForceNoForceRowSecurity(Relation rel, bool force_rls); static ObjectAddress ATExecSetCompression(AlteredTableInfo *tab, Relation rel, - const char *column, Node *newValue, LOCKMODE lockmode); + const char *column, + ColumnCompression *compression, + LOCKMODE lockmode); static void index_copy_data(Relation rel, RelFileNode newrnode); static const char *storage_name(char c); @@ -562,7 +564,6 @@ static void refuseDupeIndexAttach(Relation parentIdx, Relation partIdx, static List *GetParentedForeignKeyRefs(Relation partition); static void ATDetachCheckNoForeignKeyRefs(Relation partition); static void ATExecAlterCollationRefreshVersion(Relation rel, List *coll); -static Oid GetAttributeCompression(Form_pg_attribute att, char *compression); /* ---------------------------------------------------------------- * DefineRelation @@ -587,6 +588,7 @@ ObjectAddress DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId, ObjectAddress *typaddress, const char *queryString) { + int i; char relname[NAMEDATALEN]; Oid namespaceId; Oid relationId; @@ -865,7 +867,7 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId, relkind == RELKIND_PARTITIONED_TABLE || relkind == RELKIND_MATVIEW) attr->attcompression = - GetAttributeCompression(attr, colDef->compression); + GetAttributeCompression(attr, colDef->compression, NULL); else attr->attcompression = InvalidOid; } @@ -935,6 +937,20 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId, */ rel = relation_open(relationId, AccessExclusiveLock); + /* + * Add the dependency on the respective compression AM for the relation + * attributes. + */ + for (i = 0; i < (RelationGetDescr(rel))->natts; i++) + { + Form_pg_attribute attr; + + attr = TupleDescAttr(RelationGetDescr(rel), i); + if (OidIsValid(attr->attcompression)) + add_column_compression_dependency(attr->attrelid, attr->attnum, + attr->attcompression); + } + /* * Now add any newly specified column default and generation expressions * to the new relation. These are passed to us in the form of raw @@ -2415,16 +2431,17 @@ MergeAttributes(List *schema, List *supers, char relpersistence, /* Copy/check compression parameter */ if (OidIsValid(attribute->attcompression)) { - char *compression = get_am_name(attribute->attcompression); + ColumnCompression *compression = + MakeColumnCompression(attribute->attcompression); if (!def->compression) def->compression = compression; - else if (strcmp(def->compression, compression) != 0) + else if (strcmp(def->compression->cmname, compression->cmname)) ereport(ERROR, (errcode(ERRCODE_DATATYPE_MISMATCH), errmsg("column \"%s\" has a compression method conflict", attributeName), - errdetail("%s versus %s", def->compression, compression))); + errdetail("%s versus %s", def->compression->cmname, compression->cmname))); } def->inhcount++; @@ -2461,7 +2478,8 @@ MergeAttributes(List *schema, List *supers, char relpersistence, def->collOid = attribute->attcollation; def->constraints = NIL; def->location = -1; - def->compression = get_am_name(attribute->attcompression); + def->compression = MakeColumnCompression( + attribute->attcompression); inhSchema = lappend(inhSchema, def); newattmap->attnums[parent_attno - 1] = ++child_attno; } @@ -2712,12 +2730,12 @@ MergeAttributes(List *schema, List *supers, char relpersistence, def->compression = newdef->compression; else if (newdef->compression) { - if (strcmp(def->compression, newdef->compression)) + if (strcmp(def->compression->cmname, newdef->compression->cmname)) ereport(ERROR, (errcode(ERRCODE_DATATYPE_MISMATCH), errmsg("column \"%s\" has a compression method conflict", attributeName), - errdetail("%s versus %s", def->compression, newdef->compression))); + errdetail("%s versus %s", def->compression->cmname, newdef->compression->cmname))); } /* Mark the column as locally defined */ @@ -4908,7 +4926,8 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab, Relation rel, ATExecAlterCollationRefreshVersion(rel, cmd->object); break; case AT_SetCompression: - address = ATExecSetCompression(tab, rel, cmd->name, cmd->def, + address = ATExecSetCompression(tab, rel, cmd->name, + (ColumnCompression *) cmd->def, lockmode); break; default: /* oops */ @@ -6414,7 +6433,8 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel, if (rel->rd_rel->relkind == RELKIND_RELATION || rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) attribute.attcompression = GetAttributeCompression(&attribute, - colDef->compression); + colDef->compression, + NULL); else attribute.attcompression = InvalidOid; @@ -6589,6 +6609,7 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel, */ add_column_datatype_dependency(myrelid, newattnum, attribute.atttypid); add_column_collation_dependency(myrelid, newattnum, attribute.attcollation); + add_column_compression_dependency(myrelid, newattnum, attribute.attcompression); /* * Propagate to children as appropriate. Unlike most other ALTER @@ -6736,6 +6757,28 @@ add_column_collation_dependency(Oid relid, int32 attnum, Oid collid) } } +/* + * Install a dependency for compression on its column. + * + * This is used for identifying all the supported compression methods + * (current and preserved) for a attribute. + * + * If dependency is already there the whole thing is skipped. + */ +void +add_column_compression_dependency(Oid relid, int32 attnum, Oid cmoid) +{ + ObjectAddress acref, + attref; + + Assert(relid > 0 && attnum > 0); + + ObjectAddressSet(acref, AccessMethodRelationId, cmoid); + ObjectAddressSubSet(attref, RelationRelationId, relid, attnum); + + recordMultipleDependencies(&attref, &acref, 1, DEPENDENCY_NORMAL, true); +} + /* * ALTER TABLE ALTER COLUMN DROP NOT NULL */ @@ -11867,7 +11910,8 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, foundDep->refobjid == attTup->attcollation) && !(foundDep->refclassid == RelationRelationId && foundDep->refobjid == RelationGetRelid(rel) && - foundDep->refobjsubid != 0) + foundDep->refobjsubid != 0) && + foundDep->refclassid != AccessMethodRelationId ) elog(ERROR, "found unexpected dependency for column: %s", getObjectDescription(&foundObject, false)); @@ -11982,6 +12026,11 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, add_column_datatype_dependency(RelationGetRelid(rel), attnum, targettype); add_column_collation_dependency(RelationGetRelid(rel), attnum, targetcollid); + /* Create dependency for new attribute compression */ + if (OidIsValid(attTup->attcompression)) + add_column_compression_dependency(RelationGetRelid(rel), attnum, + attTup->attcompression); + /* * Drop any pg_statistic entry for the column, since it's now wrong type */ @@ -15086,24 +15135,21 @@ static ObjectAddress ATExecSetCompression(AlteredTableInfo *tab, Relation rel, const char *column, - Node *newValue, + ColumnCompression *compression, LOCKMODE lockmode) { Relation attrel; HeapTuple tuple; Form_pg_attribute atttableform; AttrNumber attnum; - char *compression; char typstorage; Oid cmoid; + bool need_rewrite; Datum values[Natts_pg_attribute]; bool nulls[Natts_pg_attribute]; bool replace[Natts_pg_attribute]; ObjectAddress address; - Assert(IsA(newValue, String)); - compression = strVal(newValue); - attrel = table_open(AttributeRelationId, RowExclusiveLock); tuple = SearchSysCacheAttName(RelationGetRelid(rel), column); @@ -15136,11 +15182,16 @@ ATExecSetCompression(AlteredTableInfo *tab, memset(replace, false, sizeof(replace)); /* Get the attribute compression method. */ - cmoid = GetAttributeCompression(atttableform, compression); + cmoid = GetAttributeCompression(atttableform, compression, &need_rewrite); if (atttableform->attcompression != cmoid) + add_column_compression_dependency(atttableform->attrelid, + atttableform->attnum, cmoid); + if (need_rewrite) tab->rewrite |= AT_REWRITE_ALTER_COMPRESSION; + atttableform->attcompression = cmoid; + atttableform->attcompression = cmoid; CatalogTupleUpdate(attrel, &tuple->t_self, tuple); @@ -17865,42 +17916,3 @@ ATExecAlterCollationRefreshVersion(Relation rel, List *coll) index_update_collation_versions(rel->rd_id, get_collation_oid(coll, false)); CacheInvalidateRelcache(rel); } - -/* - * resolve column compression specification to an OID. - */ -static Oid -GetAttributeCompression(Form_pg_attribute att, char *compression) -{ - char typstorage = get_typstorage(att->atttypid); - Oid amoid; - - /* - * No compression for the plain/external storage, refer comments atop - * attcompression parameter in pg_attribute.h - */ - if (!IsStorageCompressible(typstorage)) - { - if (compression == NULL) - return InvalidOid; - - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("column data type %s does not support compression", - format_type_be(att->atttypid)))); - } - - /* fallback to default compression if it's not specified */ - if (compression == NULL) - return GetDefaultToastCompression(); - - amoid = get_compression_am_oid(compression, false); - -#ifndef HAVE_LIBLZ4 - if (amoid == LZ4_COMPRESSION_AM_OID) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("not built with lz4 support"))); -#endif - return amoid; -} diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c index ea82a05591..90d092671e 100644 --- a/src/backend/executor/nodeModifyTable.c +++ b/src/backend/executor/nodeModifyTable.c @@ -44,6 +44,7 @@ #include "access/tableam.h" #include "access/xact.h" #include "catalog/catalog.h" +#include "commands/defrem.h" #include "commands/trigger.h" #include "executor/execPartition.h" #include "executor/executor.h" @@ -2068,8 +2069,8 @@ CompareCompressionMethodAndDecompress(TupleTableSlot *slot, /* * Loop over all the attributes in the tuple and check if any attribute is - * compressed and its compression method is not same as the target - * atrribute's compression method then decompress it. + * compressed and its compression method is not is not supported by the + * target attribute then we need to decompress */ for (i = 0; i < natts; i++) { @@ -2094,12 +2095,13 @@ CompareCompressionMethodAndDecompress(TupleTableSlot *slot, DatumGetPointer(slot->tts_values[attnum - 1]); /* - * Get the compression method Oid stored in the toast header and - * compare it with the compression method of the target. + * Get the compression method stored in the toast header and if the + * compression method is not supported by the target attribute then + * we need to decompress it. */ cmoid = toast_get_compression_oid(new_value); if (OidIsValid(cmoid) && - targetTupDesc->attrs[i].attcompression != cmoid) + !IsCompressionSupported(&targetTupDesc->attrs[i], cmoid)) { new_value = detoast_attr(new_value); slot->tts_values[attnum - 1] = PointerGetDatum(new_value); diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c index 1338e04409..6a11f8eb60 100644 --- a/src/backend/nodes/copyfuncs.c +++ b/src/backend/nodes/copyfuncs.c @@ -2966,7 +2966,7 @@ _copyColumnDef(const ColumnDef *from) COPY_STRING_FIELD(colname); COPY_NODE_FIELD(typeName); - COPY_STRING_FIELD(compression); + COPY_NODE_FIELD(compression); COPY_SCALAR_FIELD(inhcount); COPY_SCALAR_FIELD(is_local); COPY_SCALAR_FIELD(is_not_null); @@ -2986,6 +2986,18 @@ _copyColumnDef(const ColumnDef *from) return newnode; } +static ColumnCompression * +_copyColumnCompression(const ColumnCompression *from) +{ + ColumnCompression *newnode = makeNode(ColumnCompression); + + COPY_STRING_FIELD(cmname); + COPY_SCALAR_FIELD(preserve_all); + COPY_NODE_FIELD(preserve); + + return newnode; +} + static Constraint * _copyConstraint(const Constraint *from) { @@ -5675,6 +5687,9 @@ copyObjectImpl(const void *from) case T_ColumnDef: retval = _copyColumnDef(from); break; + case T_ColumnCompression: + retval = _copyColumnCompression(from); + break; case T_Constraint: retval = _copyConstraint(from); break; diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c index f3592003da..26a9b85974 100644 --- a/src/backend/nodes/equalfuncs.c +++ b/src/backend/nodes/equalfuncs.c @@ -2599,7 +2599,7 @@ _equalColumnDef(const ColumnDef *a, const ColumnDef *b) { COMPARE_STRING_FIELD(colname); COMPARE_NODE_FIELD(typeName); - COMPARE_STRING_FIELD(compression); + COMPARE_NODE_FIELD(compression); COMPARE_SCALAR_FIELD(inhcount); COMPARE_SCALAR_FIELD(is_local); COMPARE_SCALAR_FIELD(is_not_null); @@ -2619,6 +2619,16 @@ _equalColumnDef(const ColumnDef *a, const ColumnDef *b) return true; } +static bool +_equalColumnCompression(const ColumnCompression *a, const ColumnCompression *b) +{ + COMPARE_STRING_FIELD(cmname); + COMPARE_SCALAR_FIELD(preserve_all); + COMPARE_NODE_FIELD(preserve); + + return true; +} + static bool _equalConstraint(const Constraint *a, const Constraint *b) { @@ -3724,6 +3734,9 @@ equal(const void *a, const void *b) case T_ColumnDef: retval = _equalColumnDef(a, b); break; + case T_ColumnCompression: + retval = _equalColumnCompression(a, b); + break; case T_Constraint: retval = _equalConstraint(a, b); break; diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 0605ef3f84..b584a58ba3 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -2863,7 +2863,7 @@ _outColumnDef(StringInfo str, const ColumnDef *node) WRITE_STRING_FIELD(colname); WRITE_NODE_FIELD(typeName); - WRITE_STRING_FIELD(compression); + WRITE_NODE_FIELD(compression); WRITE_INT_FIELD(inhcount); WRITE_BOOL_FIELD(is_local); WRITE_BOOL_FIELD(is_not_null); @@ -2881,6 +2881,16 @@ _outColumnDef(StringInfo str, const ColumnDef *node) WRITE_LOCATION_FIELD(location); } +static void +_outColumnCompression(StringInfo str, const ColumnCompression *node) +{ + WRITE_NODE_TYPE("COLUMNCOMPRESSION"); + + WRITE_STRING_FIELD(cmname); + WRITE_BOOL_FIELD(preserve_all); + WRITE_NODE_FIELD(preserve); +} + static void _outTypeName(StringInfo str, const TypeName *node) { @@ -4258,6 +4268,9 @@ outNode(StringInfo str, const void *obj) case T_ColumnDef: _outColumnDef(str, obj); break; + case T_ColumnCompression: + _outColumnCompression(str, obj); + break; case T_TypeName: _outTypeName(str, obj); break; diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 30acfe615d..9eb2b04d58 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -596,7 +596,9 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); %type <list> hash_partbound %type <defelt> hash_partbound_elem -%type <str> optColumnCompression +%type <node> optColumnCompression alterColumnCompression +%type <str> compressionClause +%type <list> optCompressionPreserve /* * Non-keyword token types. These are hard-wired into the "flex" lexer. @@ -2309,12 +2311,12 @@ alter_table_cmd: $$ = (Node *)n; } /* ALTER TABLE <name> ALTER [COLUMN] <colname> SET (COMPRESSION <cm>) */ - | ALTER opt_column ColId SET optColumnCompression + | ALTER opt_column ColId SET alterColumnCompression { AlterTableCmd *n = makeNode(AlterTableCmd); n->subtype = AT_SetCompression; n->name = $3; - n->def = (Node *) makeString($5); + n->def = $5; $$ = (Node *)n; } /* ALTER TABLE <name> DROP [COLUMN] IF EXISTS <colname> [RESTRICT|CASCADE] */ @@ -3437,7 +3439,7 @@ columnDef: ColId Typename optColumnCompression create_generic_options ColQualLis ColumnDef *n = makeNode(ColumnDef); n->colname = $1; n->typeName = $2; - n->compression = $3; + n->compression = (ColumnCompression *) $3; n->inhcount = 0; n->is_local = true; n->is_not_null = false; @@ -3492,13 +3494,43 @@ columnOptions: ColId ColQualList } ; +optCompressionPreserve: + PRESERVE '(' name_list ')' { $$ = $3; } + | /*EMPTY*/ { $$ = NULL; } + ; + +compressionClause: + COMPRESSION name { $$ = pstrdup($2); } + ; + optColumnCompression: - COMPRESSION name - { - $$ = $2; - } - | /*EMPTY*/ { $$ = NULL; } - ; + compressionClause + { + ColumnCompression *n = makeNode(ColumnCompression); + n->cmname = $1; + n->preserve = NIL; + $$ = (Node *) n; + } + | /*EMPTY*/ { $$ = NULL; } + ; + +alterColumnCompression: + compressionClause optCompressionPreserve + { + ColumnCompression *n = makeNode(ColumnCompression); + n->cmname = $1; + n->preserve = (List *) $2; + $$ = (Node *) n; + } + | compressionClause PRESERVE ALL + { + ColumnCompression *n = makeNode(ColumnCompression); + n->cmname = $1; + n->preserve_all = true; + n->preserve = NIL; + $$ = (Node *) n; + } + ; ColQualList: ColQualList ColConstraint { $$ = lappend($1, $2); } diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c index cf4413da64..45f4724a13 100644 --- a/src/backend/parser/parse_utilcmd.c +++ b/src/backend/parser/parse_utilcmd.c @@ -1086,7 +1086,7 @@ transformTableLikeClause(CreateStmtContext *cxt, TableLikeClause *table_like_cla /* Likewise, copy compression if requested */ if ((table_like_clause->options & CREATE_TABLE_LIKE_COMPRESSION) != 0 && OidIsValid(attribute->attcompression)) - def->compression = get_am_name(attribute->attcompression); + def->compression = MakeColumnCompression(attribute->attcompression); else def->compression = NULL; diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c index 46044cb92a..7bf345a4ac 100644 --- a/src/bin/pg_dump/pg_dump.c +++ b/src/bin/pg_dump/pg_dump.c @@ -9037,6 +9037,80 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables) } PQclear(res); } + + /* + * Get compression info + */ + if (fout->remoteVersion >= 140000 && dopt->binary_upgrade) + { + int i_amname; + int i_amoid; + int i_curattnum; + int start; + + pg_log_info("finding compression info for table \"%s.%s\"", + tbinfo->dobj.namespace->dobj.name, + tbinfo->dobj.name); + + tbinfo->attcompression = pg_malloc0(tbinfo->numatts * sizeof(AttrCompressionInfo *)); + + resetPQExpBuffer(q); + appendPQExpBuffer(q, + " SELECT attrelid::pg_catalog.regclass AS relname, attname," + " amname, am.oid as amoid, d.objsubid AS curattnum" + " FROM pg_depend d" + " JOIN pg_attribute a ON" + " (classid = 'pg_class'::pg_catalog.regclass::pg_catalog.oid AND a.attrelid = d.objid" + " AND a.attnum = d.objsubid AND d.deptype = 'n'" + " AND d.refclassid = 'pg_am'::pg_catalog.regclass::pg_catalog.oid)" + " JOIN pg_am am ON" + " (d.deptype = 'n' AND d.refobjid = am.oid)" + " WHERE (deptype = 'n' AND d.objid = %d AND a.attcompression != am.oid);", + tbinfo->dobj.catId.oid); + + res = ExecuteSqlQuery(fout, q->data, PGRES_TUPLES_OK); + ntups = PQntuples(res); + + if (ntups > 0) + { + int j; + int k; + + i_amname = PQfnumber(res, "amname"); + i_amoid = PQfnumber(res, "amoid"); + i_curattnum = PQfnumber(res, "curattnum"); + + start = 0; + + for (j = 0; j < ntups; j++) + { + int attnum = atoi(PQgetvalue(res, j, i_curattnum)); + + if ((j == ntups - 1) || atoi(PQgetvalue(res, j + 1, i_curattnum)) != attnum) + { + AttrCompressionInfo *cminfo = pg_malloc(sizeof(AttrCompressionInfo)); + + cminfo->nitems = j - start + 1; + cminfo->items = pg_malloc(sizeof(AttrCompressionItem *) * cminfo->nitems); + + for (k = start; k < start + cminfo->nitems; k++) + { + AttrCompressionItem *cmitem = pg_malloc0(sizeof(AttrCompressionItem)); + + cmitem->amname = pg_strdup(PQgetvalue(res, k, i_amname)); + cmitem->amoid = atooid(PQgetvalue(res, k, i_amoid)); + + cminfo->items[k - start] = cmitem; + } + + tbinfo->attcompression[attnum - 1] = cminfo; + start = j + 1; /* start from next */ + } + } + } + + PQclear(res); + } } destroyPQExpBuffer(q); @@ -16343,6 +16417,33 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo) qualrelname, fmtId(tbinfo->attnames[j]), tbinfo->attfdwoptions[j]); + + /* + * Dump per-column compression options + */ + if (tbinfo->attcompression && tbinfo->attcompression[j]) + { + AttrCompressionInfo *cminfo = tbinfo->attcompression[j]; + + appendPQExpBuffer(q, "ALTER TABLE %s ALTER COLUMN %s\nSET COMPRESSION %s", + qualrelname, fmtId(tbinfo->attnames[j]), tbinfo->attcmnames[j]); + + if (cminfo->nitems > 0) + { + appendPQExpBuffer(q, "\nPRESERVE ("); + for (int i = 0; i < cminfo->nitems; i++) + { + AttrCompressionItem *item = cminfo->items[i]; + + if (i == 0) + appendPQExpBuffer(q, "%s", item->amname); + else + appendPQExpBuffer(q, ", %s", item->amname); + } + appendPQExpBuffer(q, ")"); + } + appendPQExpBuffer(q, ";\n"); + } } if (ftoptions) diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h index 1789e18f46..a829528cd0 100644 --- a/src/bin/pg_dump/pg_dump.h +++ b/src/bin/pg_dump/pg_dump.h @@ -327,7 +327,8 @@ typedef struct _tableInfo bool needs_override; /* has GENERATED ALWAYS AS IDENTITY */ char *amname; /* relation access method */ char **attcmnames; /* per-attribute current compression method */ - + struct _attrCompressionInfo **attcompression; /* per-attribute all + compression data */ /* * Stuff computed only for dumpable tables. */ @@ -356,6 +357,18 @@ typedef struct _attrDefInfo bool separate; /* true if must dump as separate item */ } AttrDefInfo; +typedef struct _attrCompressionItem +{ + Oid amoid; /* attribute compression oid */ + char *amname; /* compression access method name */ +} AttrCompressionItem; + +typedef struct _attrCompressionInfo +{ + int nitems; + AttrCompressionItem **items; +} AttrCompressionInfo; + typedef struct _tableDataInfo { DumpableObject dobj; diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c index ffa8d05edf..869fd3676d 100644 --- a/src/bin/psql/tab-complete.c +++ b/src/bin/psql/tab-complete.c @@ -2103,6 +2103,13 @@ psql_completion(const char *text, int start, int end) else if (Matches("ALTER", "TABLE", MatchAny, "ALTER", "COLUMN", MatchAny, "SET", "(") || Matches("ALTER", "TABLE", MatchAny, "ALTER", MatchAny, "SET", "(")) COMPLETE_WITH("n_distinct", "n_distinct_inherited"); + /* ALTER TABLE ALTER [COLUMN] <foo> SET COMPRESSION */ + else if (Matches("ALTER", "TABLE", MatchAny, "ALTER", "COLUMN", MatchAny, "SET", "COMPRESSION", MatchAny) || + Matches("ALTER", "TABLE", MatchAny, "ALTER", MatchAny, "SET", "COMPRESSION", MatchAny)) + COMPLETE_WITH("PRESERVE"); + else if (Matches("ALTER", "TABLE", MatchAny, "ALTER", "COLUMN", MatchAny, "SET", "COMPRESSION", MatchAny, "PRESERVE") || + Matches("ALTER", "TABLE", MatchAny, "ALTER", MatchAny, "SET", "COMPRESSION", MatchAny, "PRESERVE")) + COMPLETE_WITH("( ", "ALL"); /* ALTER TABLE ALTER [COLUMN] <foo> SET STORAGE */ else if (Matches("ALTER", "TABLE", MatchAny, "ALTER", "COLUMN", MatchAny, "SET", "STORAGE") || Matches("ALTER", "TABLE", MatchAny, "ALTER", MatchAny, "SET", "STORAGE")) diff --git a/src/include/commands/defrem.h b/src/include/commands/defrem.h index e5aea8a240..bd53f9bb0f 100644 --- a/src/include/commands/defrem.h +++ b/src/include/commands/defrem.h @@ -143,6 +143,13 @@ extern Oid get_compression_am_oid(const char *amname, bool missing_ok); extern Oid get_am_oid(const char *amname, bool missing_ok); extern char *get_am_name(Oid amOid); +/* commands/compressioncmds.c */ +extern Oid GetAttributeCompression(Form_pg_attribute att, + ColumnCompression *compression, + bool *need_rewrite); +extern ColumnCompression *MakeColumnCompression(Oid atttcompression); +extern bool IsCompressionSupported(Form_pg_attribute att, Oid cmoid); + /* support routines in commands/define.c */ extern char *defGetString(DefElem *def); diff --git a/src/include/commands/tablecmds.h b/src/include/commands/tablecmds.h index b3d30acc35..e6c98e65d4 100644 --- a/src/include/commands/tablecmds.h +++ b/src/include/commands/tablecmds.h @@ -97,5 +97,7 @@ extern void RangeVarCallbackOwnsRelation(const RangeVar *relation, Oid relId, Oid oldRelId, void *arg); extern bool PartConstraintImpliedByRelConstraint(Relation scanrel, List *partConstraint); +extern void add_column_compression_dependency(Oid relid, int32 attnum, + Oid cmoid); #endif /* TABLECMDS_H */ diff --git a/src/include/nodes/nodes.h b/src/include/nodes/nodes.h index 20d6f96f62..24deaad253 100644 --- a/src/include/nodes/nodes.h +++ b/src/include/nodes/nodes.h @@ -481,6 +481,7 @@ typedef enum NodeTag T_PartitionBoundSpec, T_PartitionRangeDatum, T_PartitionCmd, + T_ColumnCompression, T_VacuumRelation, /* diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index f9a87dee02..ce0913e18a 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -623,6 +623,20 @@ typedef struct RangeTableSample int location; /* method name location, or -1 if unknown */ } RangeTableSample; +/* + * ColumnCompression - compression parameters for some attribute + * + * This represents compression information defined using clause: + * .. COMPRESSION <compression method> PRESERVE <compression methods> + */ +typedef struct ColumnCompression +{ + NodeTag type; + char *cmname; + bool preserve_all; + List *preserve; +} ColumnCompression; + /* * ColumnDef - column definition (used in various creates) * @@ -646,7 +660,7 @@ typedef struct ColumnDef NodeTag type; char *colname; /* name of column */ TypeName *typeName; /* type of column */ - char *compression; /* compression method for column */ + ColumnCompression *compression; /* column compression */ int inhcount; /* number of times column is inherited */ bool is_local; /* column has local (non-inherited) def'n */ bool is_not_null; /* NOT NULL constraint specified? */ diff --git a/src/test/regress/expected/compression.out b/src/test/regress/expected/compression.out index 21c1b451d2..3ed33b6534 100644 --- a/src/test/regress/expected/compression.out +++ b/src/test/regress/expected/compression.out @@ -226,12 +226,47 @@ SELECT pg_column_compression(f1) FROM cmpart; lz4 (2 rows) +-- preserve old compression method +ALTER TABLE cmdata ALTER COLUMN f1 SET COMPRESSION pglz PRESERVE (lz4); +INSERT INTO cmdata VALUES (repeat('1234567890',1004)); +\d+ cmdata + Table "public.cmdata" + Column | Type | Collation | Nullable | Default | Storage | Compression | Stats target | Description +--------+------+-----------+----------+---------+----------+-------------+--------------+------------- + f1 | text | | | | extended | pglz | | +Indexes: + "idx" btree (f1) + +SELECT pg_column_compression(f1) FROM cmdata; + pg_column_compression +----------------------- + lz4 + pglz +(2 rows) + +\d+ cmdata + Table "public.cmdata" + Column | Type | Collation | Nullable | Default | Storage | Compression | Stats target | Description +--------+------+-----------+----------+---------+----------+-------------+--------------+------------- + f1 | text | | | | extended | pglz | | +Indexes: + "idx" btree (f1) + +ALTER TABLE cmdata ALTER COLUMN f1 SET COMPRESSION lz4 PRESERVE ALL; +SELECT pg_column_compression(f1) FROM cmdata; + pg_column_compression +----------------------- + lz4 + pglz +(2 rows) + -- check data is ok SELECT length(f1) FROM cmdata; length -------- 10000 -(1 row) + 10040 +(2 rows) SELECT length(f1) FROM cmdata1; length diff --git a/src/test/regress/expected/compression_1.out b/src/test/regress/expected/compression_1.out index 64c5855bf7..36a5f8ba5e 100644 --- a/src/test/regress/expected/compression_1.out +++ b/src/test/regress/expected/compression_1.out @@ -205,12 +205,49 @@ SELECT pg_column_compression(f1) FROM cmpart; ERROR: relation "cmpart" does not exist LINE 1: SELECT pg_column_compression(f1) FROM cmpart; ^ +-- preserve old compression method +ALTER TABLE cmdata ALTER COLUMN f1 SET COMPRESSION pglz PRESERVE (lz4); +ERROR: "lz4" compression access method cannot be preserved +INSERT INTO cmdata VALUES (repeat('1234567890',1004)); +\d+ cmdata + Table "public.cmdata" + Column | Type | Collation | Nullable | Default | Storage | Compression | Stats target | Description +--------+------+-----------+----------+---------+----------+-------------+--------------+------------- + f1 | text | | | | extended | pglz | | +Indexes: + "idx" btree (f1) + +SELECT pg_column_compression(f1) FROM cmdata; + pg_column_compression +----------------------- + pglz + pglz +(2 rows) + +\d+ cmdata + Table "public.cmdata" + Column | Type | Collation | Nullable | Default | Storage | Compression | Stats target | Description +--------+------+-----------+----------+---------+----------+-------------+--------------+------------- + f1 | text | | | | extended | pglz | | +Indexes: + "idx" btree (f1) + +ALTER TABLE cmdata ALTER COLUMN f1 SET COMPRESSION lz4 PRESERVE ALL; +ERROR: not built with lz4 support +SELECT pg_column_compression(f1) FROM cmdata; + pg_column_compression +----------------------- + pglz + pglz +(2 rows) + -- check data is ok SELECT length(f1) FROM cmdata; length -------- 10000 -(1 row) + 10040 +(2 rows) SELECT length(f1) FROM cmdata1; ERROR: relation "cmdata1" does not exist diff --git a/src/test/regress/expected/create_index.out b/src/test/regress/expected/create_index.out index 830fdddf24..8f984510ac 100644 --- a/src/test/regress/expected/create_index.out +++ b/src/test/regress/expected/create_index.out @@ -2066,19 +2066,21 @@ WHERE classid = 'pg_class'::regclass AND 'concur_reindex_ind4'::regclass, 'concur_reindex_matview'::regclass) ORDER BY 1, 2; - obj | objref | deptype -------------------------------------------+------------------------------------------------------------+--------- - index concur_reindex_ind1 | constraint concur_reindex_ind1 on table concur_reindex_tab | i - index concur_reindex_ind2 | collation "default" | n - index concur_reindex_ind2 | column c2 of table concur_reindex_tab | a - index concur_reindex_ind3 | column c1 of table concur_reindex_tab | a - index concur_reindex_ind3 | table concur_reindex_tab | a - index concur_reindex_ind4 | collation "default" | n - index concur_reindex_ind4 | column c1 of table concur_reindex_tab | a - index concur_reindex_ind4 | column c2 of table concur_reindex_tab | a - materialized view concur_reindex_matview | schema public | n - table concur_reindex_tab | schema public | n -(10 rows) + obj | objref | deptype +-------------------------------------------------------+------------------------------------------------------------+--------- + column c2 of materialized view concur_reindex_matview | access method pglz | n + column c2 of table concur_reindex_tab | access method pglz | n + index concur_reindex_ind1 | constraint concur_reindex_ind1 on table concur_reindex_tab | i + index concur_reindex_ind2 | collation "default" | n + index concur_reindex_ind2 | column c2 of table concur_reindex_tab | a + index concur_reindex_ind3 | column c1 of table concur_reindex_tab | a + index concur_reindex_ind3 | table concur_reindex_tab | a + index concur_reindex_ind4 | collation "default" | n + index concur_reindex_ind4 | column c1 of table concur_reindex_tab | a + index concur_reindex_ind4 | column c2 of table concur_reindex_tab | a + materialized view concur_reindex_matview | schema public | n + table concur_reindex_tab | schema public | n +(12 rows) REINDEX INDEX CONCURRENTLY concur_reindex_ind1; REINDEX TABLE CONCURRENTLY concur_reindex_tab; @@ -2095,19 +2097,21 @@ WHERE classid = 'pg_class'::regclass AND 'concur_reindex_ind4'::regclass, 'concur_reindex_matview'::regclass) ORDER BY 1, 2; - obj | objref | deptype -------------------------------------------+------------------------------------------------------------+--------- - index concur_reindex_ind1 | constraint concur_reindex_ind1 on table concur_reindex_tab | i - index concur_reindex_ind2 | collation "default" | n - index concur_reindex_ind2 | column c2 of table concur_reindex_tab | a - index concur_reindex_ind3 | column c1 of table concur_reindex_tab | a - index concur_reindex_ind3 | table concur_reindex_tab | a - index concur_reindex_ind4 | collation "default" | n - index concur_reindex_ind4 | column c1 of table concur_reindex_tab | a - index concur_reindex_ind4 | column c2 of table concur_reindex_tab | a - materialized view concur_reindex_matview | schema public | n - table concur_reindex_tab | schema public | n -(10 rows) + obj | objref | deptype +-------------------------------------------------------+------------------------------------------------------------+--------- + column c2 of materialized view concur_reindex_matview | access method pglz | n + column c2 of table concur_reindex_tab | access method pglz | n + index concur_reindex_ind1 | constraint concur_reindex_ind1 on table concur_reindex_tab | i + index concur_reindex_ind2 | collation "default" | n + index concur_reindex_ind2 | column c2 of table concur_reindex_tab | a + index concur_reindex_ind3 | column c1 of table concur_reindex_tab | a + index concur_reindex_ind3 | table concur_reindex_tab | a + index concur_reindex_ind4 | collation "default" | n + index concur_reindex_ind4 | column c1 of table concur_reindex_tab | a + index concur_reindex_ind4 | column c2 of table concur_reindex_tab | a + materialized view concur_reindex_matview | schema public | n + table concur_reindex_tab | schema public | n +(12 rows) -- Check that comments are preserved CREATE TABLE testcomment (i int); diff --git a/src/test/regress/sql/compression.sql b/src/test/regress/sql/compression.sql index b9daa33b74..5774b55d82 100644 --- a/src/test/regress/sql/compression.sql +++ b/src/test/regress/sql/compression.sql @@ -96,6 +96,15 @@ ALTER TABLE cmpart1 ALTER COLUMN f1 SET COMPRESSION pglz; ALTER TABLE cmpart2 ALTER COLUMN f1 SET COMPRESSION lz4; SELECT pg_column_compression(f1) FROM cmpart; +-- preserve old compression method +ALTER TABLE cmdata ALTER COLUMN f1 SET COMPRESSION pglz PRESERVE (lz4); +INSERT INTO cmdata VALUES (repeat('1234567890',1004)); +\d+ cmdata +SELECT pg_column_compression(f1) FROM cmdata; +\d+ cmdata +ALTER TABLE cmdata ALTER COLUMN f1 SET COMPRESSION lz4 PRESERVE ALL; +SELECT pg_column_compression(f1) FROM cmdata; + -- check data is ok SELECT length(f1) FROM cmdata; SELECT length(f1) FROM cmdata1; -- 2.17.0 --YZ5djTAD1cGYuMQK-- ^ permalink raw reply [nested|flat] 3+ messages in thread
* Re: [PATCH] Reuse Workers and Replication Slots during Logical Replication @ 2023-01-11 08:31 Melih Mutlu <[email protected]> 2023-01-17 11:15 ` RE: [PATCH] Reuse Workers and Replication Slots during Logical Replication [email protected] <[email protected]> 0 siblings, 1 reply; 3+ messages in thread From: Melih Mutlu @ 2023-01-11 08:31 UTC (permalink / raw) To: Amit Kapila <[email protected]>; pgsql-hackers Hi hackers, Rebased the patch to resolve conflicts. Best, -- Melih Mutlu Microsoft Attachments: [application/octet-stream] v3-0001-Add-replication-protocol-cmd-to-create-a-snapshot.patch (20.5K, ../../CAGPVpCQ4ouACfhg191U6mj-zSP_WTxe=xxC55JnP6vqqCpm_vQ@mail.gmail.com/3-v3-0001-Add-replication-protocol-cmd-to-create-a-snapshot.patch) download | inline diff: From 0392f3e3f5c371e0b11c853a419f48b5e402660b Mon Sep 17 00:00:00 2001 From: Melih Mutlu <[email protected]> Date: Thu, 13 Oct 2022 17:05:45 +0300 Subject: [PATCH 1/2] Add replication protocol cmd to create a snapshot Introduced REPLICATION_SLOT_SNAPSHOT to be able to create and use a snapshot without creating a new replication slot, but by using an existing slot. REPLICATION_SLOT_SNAPSHOT simply does what CREATE_REPLICATION_SLOT does without creating a new replication slot. REPLICATION_SLOT_SNAPSHOT command imports the snapshot into the current transaction and returns consistent_point. The changes earlier than the consistent_point will be applied by importing the snapshot. All changes later than the consistent_point will be available to be consumed from the replication slot. This is useful for reusing replication slots in logical replication. Otherwise, tablesync workers cannot start from a consistent point to copy a relation and then apply changes by consuming from replication slot. --- doc/src/sgml/protocol.sgml | 32 ++++++ .../libpqwalreceiver/libpqwalreceiver.c | 69 ++++++++++++- src/backend/replication/logical/logical.c | 39 +++++++- .../replication/logical/logicalfuncs.c | 1 + src/backend/replication/repl_gram.y | 18 +++- src/backend/replication/repl_scanner.l | 2 + src/backend/replication/slotfuncs.c | 1 + src/backend/replication/walsender.c | 97 ++++++++++++++++++- src/include/nodes/replnodes.h | 11 +++ src/include/replication/logical.h | 1 + src/include/replication/walreceiver.h | 13 +++ src/tools/pgindent/typedefs.list | 1 + 12 files changed, 281 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/protocol.sgml b/doc/src/sgml/protocol.sgml index 93fc7167d4..93a3867996 100644 --- a/doc/src/sgml/protocol.sgml +++ b/doc/src/sgml/protocol.sgml @@ -2613,6 +2613,38 @@ psql "dbname=postgres replication=database" -c "IDENTIFY_SYSTEM;" </listitem> </varlistentry> + <varlistentry id="protocol-replication-replication-slot-snapshot"> + <term><literal>REPLICATION_SLOT_SNAPSHOT</literal> <replaceable class="parameter">slot_name</replaceable> [ ( <replaceable class="parameter">option</replaceable> [, ...] ) ] + <indexterm><primary>REPLICATION_SLOT_SNAPSHOT</primary></indexterm> + </term> + <listitem> + <para> + Creates a snapshot including all the changes from the replication slot until + the point at which the replication slot becomes consistent. Then the snapshot + is used in the currenct transaction. This command is currently only supported + for logical replication. + slots. + </para> + + <para> + In response to this command, the server will return a one-row result set, + containing the following field: + <variablelist> + <varlistentry> + <term><literal>consistent_point</literal> (<type>text</type>)</term> + <listitem> + <para> + The WAL location at which the slot became consistent. This is the + earliest location from which streaming can start on this replication + slot. + </para> + </listitem> + </varlistentry> + </variablelist> + </para> + </listitem> + </varlistentry> + <varlistentry id="protocol-replication-base-backup" xreflabel="BASE_BACKUP"> <term><literal>BASE_BACKUP</literal> [ ( <replaceable class="parameter">option</replaceable> [, ...] ) ] <indexterm><primary>BASE_BACKUP</primary></indexterm> diff --git a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c index c40c6220db..9213fd2b1b 100644 --- a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c +++ b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c @@ -80,6 +80,8 @@ static WalRcvExecResult *libpqrcv_exec(WalReceiverConn *conn, const int nRetTypes, const Oid *retTypes); static void libpqrcv_disconnect(WalReceiverConn *conn); +static void libpqrcv_slot_snapshot(WalReceiverConn *conn, char *slotname, + const WalRcvStreamOptions *options, XLogRecPtr *lsn); static WalReceiverFunctionsType PQWalReceiverFunctions = { .walrcv_connect = libpqrcv_connect, @@ -96,7 +98,8 @@ static WalReceiverFunctionsType PQWalReceiverFunctions = { .walrcv_create_slot = libpqrcv_create_slot, .walrcv_get_backend_pid = libpqrcv_get_backend_pid, .walrcv_exec = libpqrcv_exec, - .walrcv_disconnect = libpqrcv_disconnect + .walrcv_disconnect = libpqrcv_disconnect, + .walrcv_slot_snapshot = libpqrcv_slot_snapshot }; /* Prototypes for private functions */ @@ -968,6 +971,70 @@ libpqrcv_create_slot(WalReceiverConn *conn, const char *slotname, return snapshot; } +/* + * TODO + */ +static void +libpqrcv_slot_snapshot(WalReceiverConn *conn, + char *slotname, + const WalRcvStreamOptions *options, + XLogRecPtr *lsn) +{ + StringInfoData cmd; + PGresult *res; + char *pubnames_str; + List *pubnames; + char *pubnames_literal; + + initStringInfo(&cmd); + + /* Build the command. */ + appendStringInfo(&cmd, "REPLICATION_SLOT_SNAPSHOT \"%s\"", slotname); + appendStringInfoString(&cmd, " ("); + appendStringInfo(&cmd, " proto_version '%u'", + options->proto.logical.proto_version); + + /* Add publication names. */ + pubnames = options->proto.logical.publication_names; + pubnames_str = stringlist_to_identifierstr(conn->streamConn, pubnames); + if (!pubnames_str) + ereport(ERROR, + (errcode(ERRCODE_OUT_OF_MEMORY), /* likely guess */ + errmsg("could not start WAL streaming: %s", + pchomp(PQerrorMessage(conn->streamConn))))); + pubnames_literal = PQescapeLiteral(conn->streamConn, pubnames_str, + strlen(pubnames_str)); + if (!pubnames_literal) + ereport(ERROR, + (errcode(ERRCODE_OUT_OF_MEMORY), /* likely guess */ + errmsg("could not start WAL streaming: %s", + pchomp(PQerrorMessage(conn->streamConn))))); + appendStringInfo(&cmd, ", publication_names %s", pubnames_literal); + PQfreemem(pubnames_literal); + pfree(pubnames_str); + + appendStringInfoString(&cmd, " )"); + + /* Execute the command. */ + res = libpqrcv_PQexec(conn->streamConn, cmd.data); + pfree(cmd.data); + + if (PQresultStatus(res) != PGRES_TUPLES_OK) + { + PQclear(res); + ereport(ERROR, + (errcode(ERRCODE_PROTOCOL_VIOLATION), + errmsg("Could not create a snapshot by replication slot \"%s\": %s", + slotname, pchomp(PQerrorMessage(conn->streamConn))))); + } + + if (lsn) + *lsn = DatumGetLSN(DirectFunctionCall1Coll(pg_lsn_in, InvalidOid, + CStringGetDatum(PQgetvalue(res, 0, 0)))); + + PQclear(res); +} + /* * Return PID of remote backend process. */ diff --git a/src/backend/replication/logical/logical.c b/src/backend/replication/logical/logical.c index 52d1fe6269..b62babe359 100644 --- a/src/backend/replication/logical/logical.c +++ b/src/backend/replication/logical/logical.c @@ -461,6 +461,10 @@ CreateInitDecodingContext(const char *plugin, * fast_forward * bypass the generation of logical changes. * + * need_full_snapshot + * if true, create a snapshot able to read all tables, + * otherwise do not create any snapshot. + * * xl_routine * XLogReaderRoutine used by underlying xlogreader * @@ -479,6 +483,7 @@ LogicalDecodingContext * CreateDecodingContext(XLogRecPtr start_lsn, List *output_plugin_options, bool fast_forward, + bool need_full_snapshot, XLogReaderRoutine *xl_routine, LogicalOutputPluginWriterPrepareWrite prepare_write, LogicalOutputPluginWriterWrite do_write, @@ -487,6 +492,7 @@ CreateDecodingContext(XLogRecPtr start_lsn, LogicalDecodingContext *ctx; ReplicationSlot *slot; MemoryContext old_context; + TransactionId xmin_horizon = InvalidTransactionId; /* shorter lines... */ slot = MyReplicationSlot; @@ -533,8 +539,39 @@ CreateDecodingContext(XLogRecPtr start_lsn, start_lsn = slot->data.confirmed_flush; } + + /* + * We need to determine a safe xmin horizon to start decoding from if we + * want to create a snapshot too. Otherwise we would end up with a + * snapshot that cannot be imported since xmin value from the snapshot may + * be less than the oldest safe xmin. To avoid this call + * GetOldestSafeDecodingTransactionId() to return a safe xmin value, which + * can be used while exporting/importing the snapshot. + * + * So we have to acquire the ProcArrayLock to prevent computation of new + * xmin horizons by other backends, get the safe decoding xid, and inform + * the slot machinery about the new limit. Once that's done the + * ProcArrayLock can be released as the slot machinery now is protecting + * against vacuum. + */ + if (need_full_snapshot) + { + LWLockAcquire(ProcArrayLock, LW_EXCLUSIVE); + + SpinLockAcquire(&slot->mutex); + slot->effective_catalog_xmin = xmin_horizon; + slot->data.catalog_xmin = xmin_horizon; + slot->effective_xmin = xmin_horizon; + SpinLockRelease(&slot->mutex); + + xmin_horizon = GetOldestSafeDecodingTransactionId(!need_full_snapshot); + ReplicationSlotsComputeRequiredXmin(true); + + LWLockRelease(ProcArrayLock); + } + ctx = StartupDecodingContext(output_plugin_options, - start_lsn, InvalidTransactionId, false, + start_lsn, xmin_horizon, need_full_snapshot, fast_forward, xl_routine, prepare_write, do_write, update_progress); diff --git a/src/backend/replication/logical/logicalfuncs.c b/src/backend/replication/logical/logicalfuncs.c index fa1b641a2b..1191c70eb0 100644 --- a/src/backend/replication/logical/logicalfuncs.c +++ b/src/backend/replication/logical/logicalfuncs.c @@ -208,6 +208,7 @@ pg_logical_slot_get_changes_guts(FunctionCallInfo fcinfo, bool confirm, bool bin ctx = CreateDecodingContext(InvalidXLogRecPtr, options, false, + false, XL_ROUTINE(.page_read = read_local_xlog_page, .segment_open = wal_segment_open, .segment_close = wal_segment_close), diff --git a/src/backend/replication/repl_gram.y b/src/backend/replication/repl_gram.y index 0c874e33cf..e5f0235d1e 100644 --- a/src/backend/replication/repl_gram.y +++ b/src/backend/replication/repl_gram.y @@ -65,6 +65,7 @@ Node *replication_parse_result; %token K_CREATE_REPLICATION_SLOT %token K_DROP_REPLICATION_SLOT %token K_TIMELINE_HISTORY +%token K_REPLICATION_SLOT_SNAPSHOT %token K_WAIT %token K_TIMELINE %token K_PHYSICAL @@ -80,7 +81,7 @@ Node *replication_parse_result; %type <node> command %type <node> base_backup start_replication start_logical_replication create_replication_slot drop_replication_slot identify_system - read_replication_slot timeline_history show + read_replication_slot timeline_history show replication_slot_snapshot %type <list> generic_option_list %type <defelt> generic_option %type <uintval> opt_timeline @@ -114,6 +115,7 @@ command: | read_replication_slot | timeline_history | show + | replication_slot_snapshot ; /* @@ -307,6 +309,19 @@ timeline_history: } ; +/* + * REPLICATION_SLOT_SNAPSHOT %s options + */ +replication_slot_snapshot: + K_REPLICATION_SLOT_SNAPSHOT var_name plugin_options + { + ReplicationSlotSnapshotCmd *n = makeNode(ReplicationSlotSnapshotCmd); + n->slotname = $2; + n->options = $3; + $$ = (Node *) n; + } + ; + opt_physical: K_PHYSICAL | /* EMPTY */ @@ -400,6 +415,7 @@ ident_or_keyword: | K_CREATE_REPLICATION_SLOT { $$ = "create_replication_slot"; } | K_DROP_REPLICATION_SLOT { $$ = "drop_replication_slot"; } | K_TIMELINE_HISTORY { $$ = "timeline_history"; } + | K_REPLICATION_SLOT_SNAPSHOT { $$ = "replication_slot_snapshot"; } | K_WAIT { $$ = "wait"; } | K_TIMELINE { $$ = "timeline"; } | K_PHYSICAL { $$ = "physical"; } diff --git a/src/backend/replication/repl_scanner.l b/src/backend/replication/repl_scanner.l index cb467ca46f..1988a6203b 100644 --- a/src/backend/replication/repl_scanner.l +++ b/src/backend/replication/repl_scanner.l @@ -126,6 +126,7 @@ START_REPLICATION { return K_START_REPLICATION; } CREATE_REPLICATION_SLOT { return K_CREATE_REPLICATION_SLOT; } DROP_REPLICATION_SLOT { return K_DROP_REPLICATION_SLOT; } TIMELINE_HISTORY { return K_TIMELINE_HISTORY; } +REPLICATION_SLOT_SNAPSHOT { return K_REPLICATION_SLOT_SNAPSHOT; } PHYSICAL { return K_PHYSICAL; } RESERVE_WAL { return K_RESERVE_WAL; } LOGICAL { return K_LOGICAL; } @@ -303,6 +304,7 @@ replication_scanner_is_replication_command(void) case K_DROP_REPLICATION_SLOT: case K_READ_REPLICATION_SLOT: case K_TIMELINE_HISTORY: + case K_REPLICATION_SLOT_SNAPSHOT: case K_SHOW: /* Yes; push back the first token so we can parse later. */ repl_pushed_back_token = first_token; diff --git a/src/backend/replication/slotfuncs.c b/src/backend/replication/slotfuncs.c index 2f3c964824..b3ae11b2c8 100644 --- a/src/backend/replication/slotfuncs.c +++ b/src/backend/replication/slotfuncs.c @@ -478,6 +478,7 @@ pg_logical_replication_slot_advance(XLogRecPtr moveto) ctx = CreateDecodingContext(InvalidXLogRecPtr, NIL, true, /* fast_forward */ + false, XL_ROUTINE(.page_read = read_local_xlog_page, .segment_open = wal_segment_open, .segment_close = wal_segment_close), diff --git a/src/backend/replication/walsender.c b/src/backend/replication/walsender.c index 015ae2995d..70d926f4f0 100644 --- a/src/backend/replication/walsender.c +++ b/src/backend/replication/walsender.c @@ -238,6 +238,7 @@ static void CreateReplicationSlot(CreateReplicationSlotCmd *cmd); static void DropReplicationSlot(DropReplicationSlotCmd *cmd); static void StartReplication(StartReplicationCmd *cmd); static void StartLogicalReplication(StartReplicationCmd *cmd); +static void ReplicationSlotSnapshot(ReplicationSlotSnapshotCmd *cmd); static void ProcessStandbyMessage(void); static void ProcessStandbyReplyMessage(void); static void ProcessStandbyHSFeedbackMessage(void); @@ -1280,7 +1281,7 @@ StartLogicalReplication(StartReplicationCmd *cmd) * are reported early. */ logical_decoding_ctx = - CreateDecodingContext(cmd->startpoint, cmd->options, false, + CreateDecodingContext(cmd->startpoint, cmd->options, false, false, XL_ROUTINE(.page_read = logical_read_xlog_page, .segment_open = WalSndSegmentOpen, .segment_close = wal_segment_close), @@ -1332,6 +1333,91 @@ StartLogicalReplication(StartReplicationCmd *cmd) EndCommand(&qc, DestRemote, false); } +/* + * Create a snapshot from an existing replication slot. + */ +static void +ReplicationSlotSnapshot(ReplicationSlotSnapshotCmd *cmd) +{ + Snapshot snap; + LogicalDecodingContext *ctx; + char xloc[MAXFNAMELEN]; + DestReceiver *dest; + TupOutputState *tstate; + TupleDesc tupdesc; + Datum values[1]; + bool nulls[1] = {0}; + + Assert(!MyReplicationSlot); + + if (!IsTransactionBlock()) + ereport(ERROR, + (errmsg("%s must be called inside a transaction", + "REPLICATION_SLOT_SNAPSHOT ..."))); + + if (XactIsoLevel != XACT_REPEATABLE_READ) + ereport(ERROR, + (errmsg("%s must be called in REPEATABLE READ isolation mode transaction", + "REPLICATION_SLOT_SNAPSHOT ..."))); + + if (FirstSnapshotSet) + ereport(ERROR, + (errmsg("%s must be called before any query", + "REPLICATION_SLOT_SNAPSHOT ..."))); + + if (IsSubTransaction()) + ereport(ERROR, + (errmsg("%s must not be called in a subtransaction", + "REPLICATION_SLOT_SNAPSHOT ..."))); + + ReplicationSlotAcquire(cmd->slotname, false); + + ctx = CreateDecodingContext(MyReplicationSlot->data.restart_lsn, + cmd->options, + false, + true, + XL_ROUTINE(.page_read = logical_read_xlog_page, + .segment_open = WalSndSegmentOpen, + .segment_close = wal_segment_close), + WalSndPrepareWrite, WalSndWriteData, + WalSndUpdateProgress); + + /* + * Signal that we don't need the timeout mechanism. We're just creating + * the replication slot and don't yet accept feedback messages or send + * keepalives. As we possibly need to wait for further WAL the walsender + * would otherwise possibly be killed too soon. + */ + last_reply_timestamp = 0; + + /* build initial snapshot, might take a while */ + DecodingContextFindStartpoint(ctx); + + snap = SnapBuildInitialSnapshot(ctx->snapshot_builder); + RestoreTransactionSnapshot(snap, MyProc); + + /* Don't need the decoding context anymore */ + FreeDecodingContext(ctx); + + /* Create a tuple to send consisten WAL location */ + snprintf(xloc, sizeof(xloc), "%X/%X", + LSN_FORMAT_ARGS(MyReplicationSlot->data.confirmed_flush)); + + dest = CreateDestReceiver(DestRemoteSimple); + tupdesc = CreateTemplateTupleDesc(1); + TupleDescInitBuiltinEntry(tupdesc, (AttrNumber) 1, "consistent_point", + TEXTOID, -1, 0); + tstate = begin_tup_output_tupdesc(dest, tupdesc, &TTSOpsVirtual); + + /* consistent wal location */ + values[0] = CStringGetTextDatum(xloc); + + do_tup_output(tstate, values, nulls); + end_tup_output(tstate); + + ReplicationSlotRelease(); +} + /* * LogicalDecodingContext 'prepare_write' callback. * @@ -1860,6 +1946,15 @@ exec_replication_command(const char *cmd_string) } break; + case T_ReplicationSlotSnapshotCmd: + { + cmdtag = "REPLICATION_SLOT_SNAPSHOT"; + set_ps_display(cmdtag); + ReplicationSlotSnapshot((ReplicationSlotSnapshotCmd *) cmd_node); + EndReplicationCommand(cmdtag); + break; + } + default: elog(ERROR, "unrecognized replication command node tag: %u", cmd_node->type); diff --git a/src/include/nodes/replnodes.h b/src/include/nodes/replnodes.h index 4321ba8f86..44a4580671 100644 --- a/src/include/nodes/replnodes.h +++ b/src/include/nodes/replnodes.h @@ -108,4 +108,15 @@ typedef struct TimeLineHistoryCmd TimeLineID timeline; } TimeLineHistoryCmd; +/* ---------------------- + * REPLICATION_SLOT_SNAPSHOT command + * ---------------------- + */ +typedef struct ReplicationSlotSnapshotCmd +{ + NodeTag type; + char *slotname; + List *options; +} ReplicationSlotSnapshotCmd; + #endif /* REPLNODES_H */ diff --git a/src/include/replication/logical.h b/src/include/replication/logical.h index 5f49554ea0..6535786a0e 100644 --- a/src/include/replication/logical.h +++ b/src/include/replication/logical.h @@ -125,6 +125,7 @@ extern LogicalDecodingContext *CreateInitDecodingContext(const char *plugin, extern LogicalDecodingContext *CreateDecodingContext(XLogRecPtr start_lsn, List *output_plugin_options, bool fast_forward, + bool need_full_snapshot, XLogReaderRoutine *xl_routine, LogicalOutputPluginWriterPrepareWrite prepare_write, LogicalOutputPluginWriterWrite do_write, diff --git a/src/include/replication/walreceiver.h b/src/include/replication/walreceiver.h index decffe352d..bd11f9f31e 100644 --- a/src/include/replication/walreceiver.h +++ b/src/include/replication/walreceiver.h @@ -384,6 +384,16 @@ typedef WalRcvExecResult *(*walrcv_exec_fn) (WalReceiverConn *conn, */ typedef void (*walrcv_disconnect_fn) (WalReceiverConn *conn); +/* + * walrcv_slot_snapshot_fn + * + * Create a snapshot by an existing replication slot + */ +typedef void (*walrcv_slot_snapshot_fn) (WalReceiverConn *conn, + char *slotname, + const WalRcvStreamOptions *options, + XLogRecPtr *lsn); + typedef struct WalReceiverFunctionsType { walrcv_connect_fn walrcv_connect; @@ -401,6 +411,7 @@ typedef struct WalReceiverFunctionsType walrcv_get_backend_pid_fn walrcv_get_backend_pid; walrcv_exec_fn walrcv_exec; walrcv_disconnect_fn walrcv_disconnect; + walrcv_slot_snapshot_fn walrcv_slot_snapshot; } WalReceiverFunctionsType; extern PGDLLIMPORT WalReceiverFunctionsType *WalReceiverFunctions; @@ -435,6 +446,8 @@ extern PGDLLIMPORT WalReceiverFunctionsType *WalReceiverFunctions; WalReceiverFunctions->walrcv_exec(conn, exec, nRetTypes, retTypes) #define walrcv_disconnect(conn) \ WalReceiverFunctions->walrcv_disconnect(conn) +#define walrcv_slot_snapshot(conn, slotname, options, lsn) \ + WalReceiverFunctions->walrcv_slot_snapshot(conn, slotname, options, lsn) static inline void walrcv_clear_result(WalRcvExecResult *walres) diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 23bafec5f7..209918c380 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -2322,6 +2322,7 @@ ReplicationSlotCtlData ReplicationSlotOnDisk ReplicationSlotPersistency ReplicationSlotPersistentData +ReplicationSlotSnapshotCmd ReplicationState ReplicationStateCtl ReplicationStateOnDisk -- 2.25.1 [application/octet-stream] v7-0002-Reuse-Logical-Replication-Background-worker.patch (75.4K, ../../CAGPVpCQ4ouACfhg191U6mj-zSP_WTxe=xxC55JnP6vqqCpm_vQ@mail.gmail.com/4-v7-0002-Reuse-Logical-Replication-Background-worker.patch) download | inline diff: From fcac0624791d965687d284898237a2721afec4ab Mon Sep 17 00:00:00 2001 From: Melih Mutlu <[email protected]> Date: Thu, 2 Jun 2022 17:39:37 +0300 Subject: [PATCH 2/2] Reuse Logical Replication Background worker This commit allows tablesync workers to move to another table that needs synchronization, when they're done with the current table in tablesync phase of Logical Replication. Before this commit, tablesync workers were capable of syncing only one relation. A new worker, replication slot and origin were needed for each relation in the subscription. Now, tablesync workers are not only limited with one relation and can move to another relation and reuse existing replication slots and origins This reduces the overhead of launching/killing a new background worker for each relation. By reusing tablesync workers, replication slots and origins created for tablesync can be reused as well. Removing the burden of creating/dropping replication slot/origin improves tablesync speed significantly especially for empty or small tables. Discussion: http://postgr.es/m/CAGPVpCTq=rUDd4JUdaRc1XUWf4BrH2gdSNf3rtOMUGj9rPpfzQ@mail.gmail.com --- doc/src/sgml/catalogs.sgml | 29 ++ src/backend/catalog/pg_subscription.c | 284 +++++++++++- src/backend/commands/subscriptioncmds.c | 238 ++++++---- .../replication/logical/applyparallelworker.c | 7 +- src/backend/replication/logical/launcher.c | 9 +- src/backend/replication/logical/tablesync.c | 434 +++++++++++++----- src/backend/replication/logical/worker.c | 395 ++++++++++------ src/include/catalog/pg_subscription.h | 6 + src/include/catalog/pg_subscription_rel.h | 15 +- src/include/replication/slot.h | 3 +- src/include/replication/worker_internal.h | 33 +- src/test/regress/expected/misc_sanity.out | 30 +- 12 files changed, 1113 insertions(+), 370 deletions(-) diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml index c1e4048054..e34c129f0f 100644 --- a/doc/src/sgml/catalogs.sgml +++ b/doc/src/sgml/catalogs.sgml @@ -8002,6 +8002,17 @@ SCRAM-SHA-256$<replaceable><iteration count></replaceable>:<replaceable>&l origin. </para></entry> </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>sublastusedid</structfield> <type>int8</type> + </para> + <para> + The last used ID for tablesync workers. This ID is used to + create replication slots. The last used ID needs to be stored + to make logical replication can safely proceed after any interruption. + </para></entry> + </row> </tbody> </tgroup> </table> @@ -8086,6 +8097,24 @@ SCRAM-SHA-256$<replaceable><iteration count></replaceable>:<replaceable>&l otherwise null </para></entry> </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>srrelslotname</structfield> <type>text</type> + </para> + <para> + Replication slot name that is used for synchronization of relation + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>srreloriginname</structfield> <type>text</type> + </para> + <para> + Origin name that is used for tracking synchronization of relation + </para></entry> + </row> </tbody> </tgroup> </table> diff --git a/src/backend/catalog/pg_subscription.c b/src/backend/catalog/pg_subscription.c index a56ae311c3..98d627ff49 100644 --- a/src/backend/catalog/pg_subscription.c +++ b/src/backend/catalog/pg_subscription.c @@ -114,6 +114,14 @@ GetSubscription(Oid subid, bool missing_ok) Assert(!isnull); sub->origin = TextDatumGetCString(datum); + /* Get last used id */ + datum = SysCacheGetAttr(SUBSCRIPTIONOID, + tup, + Anum_pg_subscription_sublastusedid, + &isnull); + Assert(!isnull); + sub->lastusedid = DatumGetInt64(datum); + ReleaseSysCache(tup); return sub; @@ -205,6 +213,44 @@ DisableSubscription(Oid subid) table_close(rel, NoLock); } +/* + * Update the last used replication slot ID for the given subscription. + */ +void +UpdateSubscriptionLastSlotId(Oid subid, int64 lastusedid) +{ + Relation rel; + bool nulls[Natts_pg_subscription]; + bool replaces[Natts_pg_subscription]; + Datum values[Natts_pg_subscription]; + HeapTuple tup; + + /* Look up the subscription in the catalog */ + rel = table_open(SubscriptionRelationId, RowExclusiveLock); + tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for subscription %u", subid); + + LockSharedObject(SubscriptionRelationId, subid, 0, AccessShareLock); + + /* Form a new tuple. */ + memset(values, 0, sizeof(values)); + memset(nulls, false, sizeof(nulls)); + memset(replaces, false, sizeof(replaces)); + + replaces[Anum_pg_subscription_sublastusedid - 1] = true; + values[Anum_pg_subscription_sublastusedid- 1] = Int64GetDatum(lastusedid); + + /* Update the catalog */ + tup = heap_modify_tuple(tup, RelationGetDescr(rel), values, nulls, + replaces); + CatalogTupleUpdate(rel, &tup->t_self, tup); + heap_freetuple(tup); + + table_close(rel, NoLock); +} + /* * Convert text array to list of strings. * @@ -234,7 +280,7 @@ textarray_to_stringlist(ArrayType *textarray) */ void AddSubscriptionRelState(Oid subid, Oid relid, char state, - XLogRecPtr sublsn) + XLogRecPtr sublsn, char *relslotname, char *reloriginname) { Relation rel; HeapTuple tup; @@ -263,6 +309,14 @@ AddSubscriptionRelState(Oid subid, Oid relid, char state, values[Anum_pg_subscription_rel_srsublsn - 1] = LSNGetDatum(sublsn); else nulls[Anum_pg_subscription_rel_srsublsn - 1] = true; + if (relslotname) + values[Anum_pg_subscription_rel_srrelslotname - 1] = CStringGetTextDatum(relslotname); + else + nulls[Anum_pg_subscription_rel_srrelslotname - 1] = true; + if (reloriginname) + values[Anum_pg_subscription_rel_srreloriginname - 1] = CStringGetTextDatum(reloriginname); + else + nulls[Anum_pg_subscription_rel_srreloriginname - 1] = true; tup = heap_form_tuple(RelationGetDescr(rel), values, nulls); @@ -275,6 +329,58 @@ AddSubscriptionRelState(Oid subid, Oid relid, char state, table_close(rel, NoLock); } +/* + * Internal function to modify columns for relation state update + */ +static void +UpdateSubscriptionRelState_internal(Datum *values, + bool *nulls, + bool *replaces, + char state, + XLogRecPtr sublsn) +{ + replaces[Anum_pg_subscription_rel_srsubstate - 1] = true; + values[Anum_pg_subscription_rel_srsubstate - 1] = CharGetDatum(state); + + replaces[Anum_pg_subscription_rel_srsublsn - 1] = true; + if (sublsn != InvalidXLogRecPtr) + values[Anum_pg_subscription_rel_srsublsn - 1] = LSNGetDatum(sublsn); + else + nulls[Anum_pg_subscription_rel_srsublsn - 1] = true; +} + +/* + * Internal function to modify columns for replication slot update + */ +static void +UpdateSubscriptionRelReplicationSlot_internal(Datum *values, + bool *nulls, + bool *replaces, + char *relslotname) +{ + replaces[Anum_pg_subscription_rel_srrelslotname - 1] = true; + if (relslotname) + values[Anum_pg_subscription_rel_srrelslotname - 1] = CStringGetTextDatum(relslotname); + else + nulls[Anum_pg_subscription_rel_srrelslotname - 1] = true; +} + +/* + * Internal function to modify columns for replication origin update + */ +static void +UpdateSubscriptionRelOrigin_internal(Datum *values, + bool *nulls, + bool *replaces, + char *reloriginname) +{ + replaces[Anum_pg_subscription_rel_srreloriginname - 1] = true; + if (reloriginname) + values[Anum_pg_subscription_rel_srreloriginname - 1] = CStringGetTextDatum(reloriginname); + else + nulls[Anum_pg_subscription_rel_srreloriginname - 1] = true; +} + /* * Update the state of a subscription table. */ @@ -305,14 +411,48 @@ UpdateSubscriptionRelState(Oid subid, Oid relid, char state, memset(nulls, false, sizeof(nulls)); memset(replaces, false, sizeof(replaces)); - replaces[Anum_pg_subscription_rel_srsubstate - 1] = true; - values[Anum_pg_subscription_rel_srsubstate - 1] = CharGetDatum(state); + UpdateSubscriptionRelState_internal(values, nulls, replaces, state, sublsn); - replaces[Anum_pg_subscription_rel_srsublsn - 1] = true; - if (sublsn != InvalidXLogRecPtr) - values[Anum_pg_subscription_rel_srsublsn - 1] = LSNGetDatum(sublsn); - else - nulls[Anum_pg_subscription_rel_srsublsn - 1] = true; + tup = heap_modify_tuple(tup, RelationGetDescr(rel), values, nulls, + replaces); + + /* Update the catalog. */ + CatalogTupleUpdate(rel, &tup->t_self, tup); + + /* Cleanup. */ + table_close(rel, NoLock); +} + +/* + * Update the replication slot name of a subscription table. + */ +void +UpdateSubscriptionRelReplicationSlot(Oid subid, Oid relid, char *relslotname) +{ + Relation rel; + HeapTuple tup; + bool nulls[Natts_pg_subscription_rel]; + Datum values[Natts_pg_subscription_rel]; + bool replaces[Natts_pg_subscription_rel]; + + LockSharedObject(SubscriptionRelationId, subid, 0, AccessShareLock); + + rel = table_open(SubscriptionRelRelationId, RowExclusiveLock); + + /* Try finding existing mapping. */ + tup = SearchSysCacheCopy2(SUBSCRIPTIONRELMAP, + ObjectIdGetDatum(relid), + ObjectIdGetDatum(subid)); + if (!HeapTupleIsValid(tup)) + elog(ERROR, "subscription table %u in subscription %u does not exist", + relid, subid); + + /* Update the tuple. */ + memset(values, 0, sizeof(values)); + memset(nulls, false, sizeof(nulls)); + memset(replaces, false, sizeof(replaces)); + + UpdateSubscriptionRelReplicationSlot_internal(values, nulls, replaces, relslotname); tup = heap_modify_tuple(tup, RelationGetDescr(rel), values, nulls, replaces); @@ -324,6 +464,134 @@ UpdateSubscriptionRelState(Oid subid, Oid relid, char state, table_close(rel, NoLock); } +/* + * Update replication slot name, origin name and state of + * a subscription table in one transaction. + */ +void +UpdateSubscriptionRel(Oid subid, + Oid relid, + char state, + XLogRecPtr sublsn, + char *relslotname, + char *reloriginname) +{ + Relation rel; + HeapTuple tup; + bool nulls[Natts_pg_subscription_rel]; + Datum values[Natts_pg_subscription_rel]; + bool replaces[Natts_pg_subscription_rel]; + + LockSharedObject(SubscriptionRelationId, subid, 0, AccessShareLock); + + rel = table_open(SubscriptionRelRelationId, RowExclusiveLock); + + /* Try finding existing mapping. */ + tup = SearchSysCacheCopy2(SUBSCRIPTIONRELMAP, + ObjectIdGetDatum(relid), + ObjectIdGetDatum(subid)); + if (!HeapTupleIsValid(tup)) + elog(ERROR, "subscription table %u in subscription %u does not exist", + relid, subid); + + /* Update the tuple. */ + memset(values, 0, sizeof(values)); + memset(nulls, false, sizeof(nulls)); + memset(replaces, false, sizeof(replaces)); + + UpdateSubscriptionRelState_internal(values, nulls, replaces, state, sublsn); + UpdateSubscriptionRelReplicationSlot_internal(values, nulls, replaces, relslotname); + UpdateSubscriptionRelOrigin_internal(values, nulls, replaces, reloriginname); + + tup = heap_modify_tuple(tup, RelationGetDescr(rel), values, nulls, + replaces); + + /* Update the catalog. */ + CatalogTupleUpdate(rel, &tup->t_self, tup); + + /* Cleanup. */ + table_close(rel, NoLock); +} + +/* + * Get origin name of subscription table. + * + * Returns null if the subscription table does not have a origin. + */ +void +GetSubscriptionRelOrigin(Oid subid, Oid relid, char *reloriginname, bool *isnull) +{ + HeapTuple tup; + Relation rel; + Datum d; + char *originname; + + rel = table_open(SubscriptionRelRelationId, AccessShareLock); + + /* Try finding the mapping. */ + tup = SearchSysCache2(SUBSCRIPTIONRELMAP, + ObjectIdGetDatum(relid), + ObjectIdGetDatum(subid)); + + if (!HeapTupleIsValid(tup)) + { + table_close(rel, AccessShareLock); + } + + d = SysCacheGetAttr(SUBSCRIPTIONRELMAP, tup, + Anum_pg_subscription_rel_srreloriginname, isnull); + if (!*isnull) + { + originname = TextDatumGetCString(d); + memcpy(reloriginname, originname, NAMEDATALEN); + } + + /* Cleanup */ + ReleaseSysCache(tup); + + table_close(rel, AccessShareLock); +} + +/* + * Get replication slot name of subscription table. + * + * Returns null if the subscription table does not have a replication slot. + */ +void +GetSubscriptionRelReplicationSlot(Oid subid, Oid relid, char *slotname) +{ + HeapTuple tup; + Relation rel; + Datum d; + char *relrepslot; + bool isnull; + + rel = table_open(SubscriptionRelRelationId, AccessShareLock); + + /* Try finding the mapping. */ + tup = SearchSysCache2(SUBSCRIPTIONRELMAP, + ObjectIdGetDatum(relid), + ObjectIdGetDatum(subid)); + + if (!HeapTupleIsValid(tup)) + { + table_close(rel, AccessShareLock); + } + + d = SysCacheGetAttr(SUBSCRIPTIONRELMAP, tup, + Anum_pg_subscription_rel_srrelslotname, &isnull); + if (!isnull) + { + relrepslot = TextDatumGetCString(d); + memcpy(slotname, relrepslot, NAMEDATALEN); + } + + /* Cleanup */ + ReleaseSysCache(tup); + + table_close(rel, AccessShareLock); +} + /* * Get state of subscription table. * diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c index baff00dd74..2a68648a2b 100644 --- a/src/backend/commands/subscriptioncmds.c +++ b/src/backend/commands/subscriptioncmds.c @@ -649,6 +649,7 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt, publicationListToArray(publications); values[Anum_pg_subscription_suborigin - 1] = CStringGetTextDatum(opts.origin); + values[Anum_pg_subscription_sublastusedid - 1] = Int64GetDatum(1); tup = heap_form_tuple(RelationGetDescr(rel), values, nulls); @@ -658,7 +659,7 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt, recordDependencyOnOwner(SubscriptionRelationId, subid, owner); - ReplicationOriginNameForLogicalRep(subid, InvalidOid, originname, sizeof(originname)); + ReplicationOriginNameForLogicalRep(subid, originname, sizeof(originname), false); replorigin_create(originname); /* @@ -709,7 +710,7 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt, rv->schemaname, rv->relname); AddSubscriptionRelState(subid, relid, table_state, - InvalidXLogRecPtr); + InvalidXLogRecPtr, NULL, NULL); } /* @@ -799,6 +800,8 @@ AlterSubscription_refresh(Subscription *sub, bool copy_data, } SubRemoveRels; SubRemoveRels *sub_remove_rels; WalReceiverConn *wrconn; + List *sub_remove_slots = NIL; + LogicalRepWorker *worker; /* Load the library providing us libpq calls. */ load_file("libpqwalreceiver", false); @@ -876,7 +879,7 @@ AlterSubscription_refresh(Subscription *sub, bool copy_data, { AddSubscriptionRelState(sub->oid, relid, copy_data ? SUBREL_STATE_INIT : SUBREL_STATE_READY, - InvalidXLogRecPtr); + InvalidXLogRecPtr, NULL, NULL); ereport(DEBUG1, (errmsg_internal("table \"%s.%s\" added to subscription \"%s\"", rv->schemaname, rv->relname, sub->name))); @@ -900,6 +903,7 @@ AlterSubscription_refresh(Subscription *sub, bool copy_data, { char state; XLogRecPtr statelsn; + char slotname[NAMEDATALEN] = {0}; /* * Lock pg_subscription_rel with AccessExclusiveLock to @@ -926,7 +930,29 @@ AlterSubscription_refresh(Subscription *sub, bool copy_data, RemoveSubscriptionRel(sub->oid, relid); - logicalrep_worker_stop(sub->oid, relid); + /* + * Find the logical replication sync worker if exists store + * the slot number for dropping associated replication slots + * later. + */ + LWLockAcquire(LogicalRepWorkerLock, LW_SHARED); + worker = logicalrep_worker_find(sub->oid, relid, false); + if (worker) + { + logicalrep_worker_stop(sub->oid, relid); + sub_remove_slots = lappend(sub_remove_slots, &worker->slot_name); + } + else + { + /* + * Sync of this relation might be failed in an earlier + * attempt, but the replication slot might still exist. + */ + GetSubscriptionRelReplicationSlot(sub->oid, relid, slotname); + if (strlen(slotname) > 0) + sub_remove_slots = lappend(sub_remove_slots, slotname); + } + LWLockRelease(LogicalRepWorkerLock); /* * For READY state, we would have already dropped the @@ -946,8 +972,8 @@ AlterSubscription_refresh(Subscription *sub, bool copy_data, * origin and by this time the origin might be already * removed. For these reasons, passing missing_ok = true. */ - ReplicationOriginNameForLogicalRep(sub->oid, relid, originname, - sizeof(originname)); + ReplicationOriginNameForLogicalRep(sub->oid, originname, + sizeof(originname), true); replorigin_drop_by_name(originname, true, false); } @@ -960,31 +986,24 @@ AlterSubscription_refresh(Subscription *sub, bool copy_data, } /* - * Drop the tablesync slots associated with removed tables. This has - * to be at the end because otherwise if there is an error while doing - * the database operations we won't be able to rollback dropped slots. + * Drop the replication slots associated with tablesync workers for + * removed tables. This has to be at the end because otherwise if + * there is an error while doing the database operations we won't be + * able to rollback dropped slots. */ - for (off = 0; off < remove_rel_len; off++) + foreach(lc, sub_remove_slots) { - if (sub_remove_rels[off].state != SUBREL_STATE_READY && - sub_remove_rels[off].state != SUBREL_STATE_SYNCDONE) - { - char syncslotname[NAMEDATALEN] = {0}; + char syncslotname[NAMEDATALEN] = {0}; - /* - * For READY/SYNCDONE states we know the tablesync slot has - * already been dropped by the tablesync worker. - * - * For other states, there is no certainty, maybe the slot - * does not exist yet. Also, if we fail after removing some of - * the slots, next time, it will again try to drop already - * dropped slots and fail. For these reasons, we allow - * missing_ok = true for the drop. - */ - ReplicationSlotNameForTablesync(sub->oid, sub_remove_rels[off].relid, - syncslotname, sizeof(syncslotname)); - ReplicationSlotDropAtPubNode(wrconn, syncslotname, true); - } + memcpy(syncslotname, lfirst(lc), sizeof(NAMEDATALEN)); + + /* + * There is no certainty, maybe the slot does not exist yet. Also, + * if we fail after removing some of the slots, next time, it will + * again try to drop already dropped slots and fail. For these + * reasons, we allow missing_ok = true for the drop. + */ + ReplicationSlotDropAtPubNode(wrconn, syncslotname, true); } } PG_FINALLY(); @@ -1320,8 +1339,8 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt, char originname[NAMEDATALEN]; XLogRecPtr remote_lsn; - ReplicationOriginNameForLogicalRep(subid, InvalidOid, - originname, sizeof(originname)); + ReplicationOriginNameForLogicalRep(subid, originname, + sizeof(originname), false); originid = replorigin_by_name(originname, false); remote_lsn = replorigin_get_progress(originid, false); @@ -1384,6 +1403,7 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel) char *subname; char *conninfo; char *slotname; + int64 lastusedid; List *subworkers; ListCell *lc; char originname[NAMEDATALEN]; @@ -1455,6 +1475,14 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel) else slotname = NULL; + /* Get the last used identifier by the subscription */ + datum = SysCacheGetAttr(SUBSCRIPTIONOID, tup, + Anum_pg_subscription_sublastusedid, &isnull); + if (!isnull) + lastusedid = DatumGetInt64(datum); + else + lastusedid = 0; + /* * Since dropping a replication slot is not transactional, the replication * slot stays dropped even if the transaction rolls back. So we cannot @@ -1504,37 +1532,29 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel) } list_free(subworkers); + rstates = GetSubscriptionRelations(subid, true); + /* - * Cleanup of tablesync replication origins. - * - * Any READY-state relations would already have dealt with clean-ups. + * Cleanup of tablesync replication origins associated with the + * subscription, if exists. Try to drop origins by creating all origin + * names created for this subscription. * * Note that the state can't change because we have already stopped both * the apply and tablesync workers and they can't restart because of * exclusive lock on the subscription. + * + * XXX: This can be handled better instead of looping through all possible */ - rstates = GetSubscriptionRelations(subid, true); - foreach(lc, rstates) + for (int64 i = 1; i <= lastusedid; i++) { - SubscriptionRelState *rstate = (SubscriptionRelState *) lfirst(lc); - Oid relid = rstate->relid; - - /* Only cleanup resources of tablesync workers */ - if (!OidIsValid(relid)) - continue; + char originname_to_drop[NAMEDATALEN] = {0}; - /* - * Drop the tablesync's origin tracking if exists. - * - * It is possible that the origin is not yet created for tablesync - * worker so passing missing_ok = true. This can happen for the states - * before SUBREL_STATE_FINISHEDCOPY. - */ - ReplicationOriginNameForLogicalRep(subid, relid, originname, - sizeof(originname)); - replorigin_drop_by_name(originname, true, false); + snprintf(originname_to_drop, sizeof(originname_to_drop), "pg_%u_%lld", subid, (long long) i); + /* missin_ok = true, since the origin might be already dropped. */ + replorigin_drop_by_name(originname_to_drop, true, false); } + /* Clean up dependencies */ deleteSharedDependencyRecordsFor(SubscriptionRelationId, subid, 0); @@ -1542,7 +1562,7 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel) RemoveSubscriptionRel(subid, InvalidOid); /* Remove the origin tracking if exists. */ - ReplicationOriginNameForLogicalRep(subid, InvalidOid, originname, sizeof(originname)); + ReplicationOriginNameForLogicalRep(subid, originname, sizeof(originname), false); replorigin_drop_by_name(originname, true, false); /* @@ -1586,39 +1606,17 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel) PG_TRY(); { - foreach(lc, rstates) - { - SubscriptionRelState *rstate = (SubscriptionRelState *) lfirst(lc); - Oid relid = rstate->relid; + List *slots = NULL; - /* Only cleanup resources of tablesync workers */ - if (!OidIsValid(relid)) - continue; - /* - * Drop the tablesync slots associated with removed tables. - * - * For SYNCDONE/READY states, the tablesync slot is known to have - * already been dropped by the tablesync worker. - * - * For other states, there is no certainty, maybe the slot does - * not exist yet. Also, if we fail after removing some of the - * slots, next time, it will again try to drop already dropped - * slots and fail. For these reasons, we allow missing_ok = true - * for the drop. - */ - if (rstate->state != SUBREL_STATE_SYNCDONE) - { - char syncslotname[NAMEDATALEN] = {0}; + slots = GetReplicationSlotNamesBySubId(wrconn, subid, true); + foreach(lc, slots) + { + char *syncslotname = (char *) lfirst(lc); - ReplicationSlotNameForTablesync(subid, relid, syncslotname, - sizeof(syncslotname)); - ReplicationSlotDropAtPubNode(wrconn, syncslotname, true); - } + ReplicationSlotDropAtPubNode(wrconn, syncslotname, true); } - list_free(rstates); - /* * If there is a slot associated with the subscription, then drop the * replication slot at the publisher. @@ -1641,6 +1639,71 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel) table_close(rel, NoLock); } +/* + * GetReplicationSlotNamesBySubId + * + * Get the replication slot names associated with the subscription. + */ +List * +GetReplicationSlotNamesBySubId(WalReceiverConn *wrconn, Oid subid, bool missing_ok) +{ + StringInfoData cmd; + TupleTableSlot *slot; + Oid tableRow[1] = {NAMEOID}; + List *tablelist = NIL; + + Assert(wrconn); + + load_file("libpqwalreceiver", false); + + initStringInfo(&cmd); + appendStringInfo(&cmd, "SELECT slot_name" + " FROM pg_replication_slots" + " WHERE slot_name LIKE 'pg_%i_sync_%%';", + subid); + PG_TRY(); + { + WalRcvExecResult *res; + + res = walrcv_exec(wrconn, cmd.data, 1, tableRow); + + if (res->status != WALRCV_OK_TUPLES) + { + ereport(ERROR, + errmsg("not tuple returned.")); + } + + /* Process tables. */ + slot = MakeSingleTupleTableSlot(res->tupledesc, &TTSOpsMinimalTuple); + while (tuplestore_gettupleslot(res->tuplestore, true, false, slot)) + { + char *repslotname; + char *slotattr; + bool isnull; + + slotattr = NameStr(*DatumGetName(slot_getattr(slot, 1, &isnull))); + Assert(!isnull); + + repslotname = palloc(sizeof(char) * strlen(slotattr) + 1); + memcpy(repslotname, slotattr, sizeof(char) * strlen(slotattr)); + repslotname[strlen(slotattr)] = '\0'; + tablelist = lappend(tablelist, repslotname); + + ExecClearTuple(slot); + } + ExecDropSingleTupleTableSlot(slot); + + walrcv_clear_result(res); + } + PG_FINALLY(); + { + pfree(cmd.data); + } + PG_END_TRY(); + \ + return tablelist; +} + /* * Drop the replication slot at the publisher node using the replication * connection. @@ -1995,6 +2058,7 @@ static void ReportSlotConnectionError(List *rstates, Oid subid, char *slotname, char *err) { ListCell *lc; + LogicalRepWorker *worker; foreach(lc, rstates) { @@ -2005,18 +2069,20 @@ ReportSlotConnectionError(List *rstates, Oid subid, char *slotname, char *err) if (!OidIsValid(relid)) continue; + /* Check if there is a sync worker for the relation */ + LWLockAcquire(LogicalRepWorkerLock, LW_SHARED); + worker = logicalrep_worker_find(subid, relid, false); + LWLockRelease(LogicalRepWorkerLock); + /* * Caller needs to ensure that relstate doesn't change underneath us. * See DropSubscription where we get the relstates. */ - if (rstate->state != SUBREL_STATE_SYNCDONE) + if (worker && + rstate->state != SUBREL_STATE_SYNCDONE) { - char syncslotname[NAMEDATALEN] = {0}; - - ReplicationSlotNameForTablesync(subid, relid, syncslotname, - sizeof(syncslotname)); elog(WARNING, "could not drop tablesync replication slot \"%s\"", - syncslotname); + worker->slot_name); } } diff --git a/src/backend/replication/logical/applyparallelworker.c b/src/backend/replication/logical/applyparallelworker.c index 2e5914d5d9..8df3a12c19 100644 --- a/src/backend/replication/logical/applyparallelworker.c +++ b/src/backend/replication/logical/applyparallelworker.c @@ -440,7 +440,8 @@ pa_launch_parallel_worker(void) MySubscription->name, MyLogicalRepWorker->userid, InvalidOid, - dsm_segment_handle(winfo->dsm_seg)); + dsm_segment_handle(winfo->dsm_seg), + 0); if (launched) { @@ -942,8 +943,8 @@ ParallelApplyWorkerMain(Datum main_arg) /* Setup replication origin tracking. */ StartTransactionCommand(); - ReplicationOriginNameForLogicalRep(MySubscription->oid, InvalidOid, - originname, sizeof(originname)); + ReplicationOriginNameForLogicalRep(MySubscription->oid, originname, + sizeof(originname), false); originid = replorigin_by_name(originname, false); /* diff --git a/src/backend/replication/logical/launcher.c b/src/backend/replication/logical/launcher.c index afb7acddaa..f817037479 100644 --- a/src/backend/replication/logical/launcher.c +++ b/src/backend/replication/logical/launcher.c @@ -276,7 +276,7 @@ logicalrep_workers_find(Oid subid, bool only_running) */ bool logicalrep_worker_launch(Oid dbid, Oid subid, const char *subname, Oid userid, - Oid relid, dsm_handle subworker_dsm) + Oid relid, dsm_handle subworker_dsm, int64 slotid) { BackgroundWorker bgw; BackgroundWorkerHandle *bgw_handle; @@ -401,7 +401,11 @@ retry: /* Prepare the worker slot. */ worker->launch_time = now; worker->in_use = true; + worker->is_first_run = true; worker->generation++; + worker->created_slot = false; + worker->rep_slot_id = slotid; + worker->slot_name = (char *) palloc(NAMEDATALEN); worker->proc = NULL; worker->dbid = dbid; worker->userid = userid; @@ -409,6 +413,7 @@ retry: worker->relid = relid; worker->relstate = SUBREL_STATE_UNKNOWN; worker->relstate_lsn = InvalidXLogRecPtr; + worker->move_to_next_rel = false; worker->stream_fileset = NULL; worker->apply_leader_pid = is_parallel_apply_worker ? MyProcPid : InvalidPid; worker->parallel_apply = is_parallel_apply_worker; @@ -1015,7 +1020,7 @@ ApplyLauncherMain(Datum main_arg) wait_time = wal_retrieve_retry_interval; logicalrep_worker_launch(sub->dbid, sub->oid, sub->name, - sub->owner, InvalidOid, DSM_HANDLE_INVALID); + sub->owner, InvalidOid, DSM_HANDLE_INVALID, 0); } } diff --git a/src/backend/replication/logical/tablesync.c b/src/backend/replication/logical/tablesync.c index 38dfce7129..01925d4e99 100644 --- a/src/backend/replication/logical/tablesync.c +++ b/src/backend/replication/logical/tablesync.c @@ -126,12 +126,8 @@ static bool FetchTableStates(bool *started_tx); static StringInfo copybuf = NULL; -/* - * Exit routine for synchronization worker. - */ static void -pg_attribute_noreturn() -finish_sync_worker(void) +clean_sync_worker(void) { /* * Commit any outstanding transaction. This is the usual case, unless @@ -143,18 +139,28 @@ finish_sync_worker(void) pgstat_report_stat(true); } - /* And flush all writes. */ - XLogFlush(GetXLogWriteRecPtr()); - - StartTransactionCommand(); - ereport(LOG, - (errmsg("logical replication table synchronization worker for subscription \"%s\", table \"%s\" has finished", - MySubscription->name, - get_rel_name(MyLogicalRepWorker->relid)))); - CommitTransactionCommand(); + /* + * Disconnect from publisher. Otherwise reused sync workers causes + * exceeding max_wal_senders + */ + walrcv_disconnect(LogRepWorkerWalRcvConn); + LogRepWorkerWalRcvConn = NULL; /* Find the leader apply worker and signal it. */ logicalrep_worker_wakeup(MyLogicalRepWorker->subid, InvalidOid); +} + +/* + * Exit routine for synchronization worker. + */ +static void +pg_attribute_noreturn() +finish_sync_worker(void) +{ + clean_sync_worker(); + + /* And flush all writes. */ + XLogFlush(GetXLogWriteRecPtr()); /* Stop gracefully */ proc_exit(0); @@ -284,6 +290,10 @@ invalidate_syncing_table_states(Datum arg, int cacheid, uint32 hashvalue) static void process_syncing_tables_for_sync(XLogRecPtr current_lsn) { + List *rstates; + SubscriptionRelState *rstate; + ListCell *lc; + SpinLockAcquire(&MyLogicalRepWorker->relmutex); if (MyLogicalRepWorker->relstate == SUBREL_STATE_CATCHUP && @@ -292,6 +302,7 @@ process_syncing_tables_for_sync(XLogRecPtr current_lsn) TimeLineID tli; char syncslotname[NAMEDATALEN] = {0}; char originname[NAMEDATALEN] = {0}; + bool is_streaming_ended = false; MyLogicalRepWorker->relstate = SUBREL_STATE_SYNCDONE; MyLogicalRepWorker->relstate_lsn = current_lsn; @@ -308,40 +319,29 @@ process_syncing_tables_for_sync(XLogRecPtr current_lsn) MyLogicalRepWorker->relid, MyLogicalRepWorker->relstate, MyLogicalRepWorker->relstate_lsn); + CommitTransactionCommand(); /* - * End streaming so that LogRepWorkerWalRcvConn can be used to drop - * the slot. - */ - walrcv_endstreaming(LogRepWorkerWalRcvConn, &tli); - - /* - * Cleanup the tablesync slot. + * Cleanup the tablesync slot. If the slot name used by this worker is + * different from the default slot name for the worker, this means the + * current table had started to being synchronized by another worker + * and replication slot. And this worker is reusing a replication slot + * from a previous attempt. We do not need that replication slot + * anymore. * * This has to be done after updating the state because otherwise if * there is an error while doing the database operations we won't be * able to rollback dropped slot. */ ReplicationSlotNameForTablesync(MyLogicalRepWorker->subid, - MyLogicalRepWorker->relid, + MyLogicalRepWorker->rep_slot_id, syncslotname, sizeof(syncslotname)); /* - * It is important to give an error if we are unable to drop the slot, - * otherwise, it won't be dropped till the corresponding subscription - * is dropped. So passing missing_ok = false. - */ - ReplicationSlotDropAtPubNode(LogRepWorkerWalRcvConn, syncslotname, false); - - CommitTransactionCommand(); - pgstat_report_stat(false); - - /* - * Start a new transaction to clean up the tablesync origin tracking. - * This transaction will be ended within the finish_sync_worker(). - * Now, even, if we fail to remove this here, the apply worker will - * ensure to clean it up afterward. + * We are safe to drop the replication trackin origin after this + * point. Now, even, if we fail to remove this here, the apply worker + * will ensure to clean it up afterward. * * We need to do this after the table state is set to SYNCDONE. * Otherwise, if an error occurs while performing the database @@ -350,34 +350,125 @@ process_syncing_tables_for_sync(XLogRecPtr current_lsn) * have been cleared before restart. So, the restarted worker will use * invalid replication progress state resulting in replay of * transactions that have already been applied. + * + * Firstly reset the origin session to remove the ownership of the + * slot. This is needed to allow the origin to be dropped or reused + * later. */ + replorigin_session_reset(); + replorigin_session_origin = InvalidRepOriginId; + replorigin_session_origin_lsn = InvalidXLogRecPtr; + replorigin_session_origin_timestamp = 0; + StartTransactionCommand(); + if (MyLogicalRepWorker->slot_name && strcmp(syncslotname, MyLogicalRepWorker->slot_name) != 0) + { + /* + * End streaming so that LogRepWorkerWalRcvConn can be used to + * drop the slot. + */ + walrcv_endstreaming(LogRepWorkerWalRcvConn, &tli); + is_streaming_ended = true; + ReplicationSlotDropAtPubNode(LogRepWorkerWalRcvConn, MyLogicalRepWorker->slot_name, false); - ReplicationOriginNameForLogicalRep(MyLogicalRepWorker->subid, - MyLogicalRepWorker->relid, - originname, - sizeof(originname)); + ReplicationOriginNameForLogicalRep(MyLogicalRepWorker->subid, + originname, + sizeof(originname), + true); + + /* Drop replication origin */ + replorigin_drop_by_name(originname, true, false); + } /* - * Resetting the origin session removes the ownership of the slot. - * This is needed to allow the origin to be dropped. + * We are safe to remove persisted replication slot and origin data, + * since it's already in SYNCDONE state. They will not be needed + * anymore. */ - replorigin_session_reset(); - replorigin_session_origin = InvalidRepOriginId; - replorigin_session_origin_lsn = InvalidXLogRecPtr; - replorigin_session_origin_timestamp = 0; + UpdateSubscriptionRel(MyLogicalRepWorker->subid, + MyLogicalRepWorker->relid, + MyLogicalRepWorker->relstate, + MyLogicalRepWorker->relstate_lsn, + NULL, + NULL); + + ereport(LOG, + (errmsg("logical replication table synchronization worker for subscription \"%s\", table \"%s\" has finished", + MySubscription->name, + get_rel_name(MyLogicalRepWorker->relid)))); + + CommitTransactionCommand(); + pgstat_report_stat(false); + + StartTransactionCommand(); /* - * Drop the tablesync's origin tracking if exists. - * - * There is a chance that the user is concurrently performing refresh - * for the subscription where we remove the table state and its origin - * or the apply worker would have removed this origin. So passing - * missing_ok = true. + * This should return the default origin name for the worker. Even if + * the worker used a different origin for this table, it should be + * dropped and removed from the catalog so far. + */ + ReplicationOriginNameForLogicalRep(MyLogicalRepWorker->subid, + originname, + sizeof(originname), + true); + + /* + * Check if any table whose relation state is still INIT. If a table + * in INIT state is found, the worker will not be finished, it will be + * reused instead. */ - replorigin_drop_by_name(originname, true, false); + rstates = GetSubscriptionRelations(MySubscription->oid, true); + + foreach(lc, rstates) + { + rstate = (SubscriptionRelState *) palloc(sizeof(SubscriptionRelState)); + memcpy(rstate, lfirst(lc), sizeof(SubscriptionRelState)); + + /* + * Pick the table for the next run if there is not another worker + * already picked that table. + */ + LWLockAcquire(LogicalRepWorkerLock, LW_SHARED); + if (rstate->state != SUBREL_STATE_SYNCDONE && + !logicalrep_worker_find(MySubscription->oid, rstate->relid, false)) + { + /* Update worker state for the next table */ + MyLogicalRepWorker->is_first_run = false; + MyLogicalRepWorker->relid = rstate->relid; + MyLogicalRepWorker->relstate = rstate->state; + MyLogicalRepWorker->relstate_lsn = rstate->lsn; + MyLogicalRepWorker->move_to_next_rel = true; + LWLockRelease(LogicalRepWorkerLock); + break; + } + LWLockRelease(LogicalRepWorkerLock); + } + + /* Cleanup before next run or ending the worker. */ + if (!MyLogicalRepWorker->move_to_next_rel) + { + /* + * It is important to give an error if we are unable to drop the + * slot, otherwise, it won't be dropped till the corresponding + * subscription is dropped. So passing missing_ok = false. + */ + if (MyLogicalRepWorker->created_slot) + { + /* End streaming if it's not already ended. */ + if (!is_streaming_ended) + walrcv_endstreaming(LogRepWorkerWalRcvConn, &tli); + ReplicationSlotDropAtPubNode(LogRepWorkerWalRcvConn, syncslotname, false); + } + + /* Drop replication origin before exiting. */ + replorigin_drop_by_name(originname, true, false); - finish_sync_worker(); + finish_sync_worker(); + } + else + { + clean_sync_worker(); + } } else SpinLockRelease(&MyLogicalRepWorker->relmutex); @@ -464,6 +555,7 @@ process_syncing_tables_for_apply(XLogRecPtr current_lsn) if (current_lsn >= rstate->lsn) { char originname[NAMEDATALEN]; + bool is_origin_null = true; rstate->state = SUBREL_STATE_READY; rstate->lsn = current_lsn; @@ -484,18 +576,27 @@ process_syncing_tables_for_apply(XLogRecPtr current_lsn) * error while dropping we won't restart it to drop the * origin. So passing missing_ok = true. */ - ReplicationOriginNameForLogicalRep(MyLogicalRepWorker->subid, - rstate->relid, - originname, - sizeof(originname)); - replorigin_drop_by_name(originname, true, false); + GetSubscriptionRelOrigin(MyLogicalRepWorker->subid, + rstate->relid, originname, + &is_origin_null); + + if (!is_origin_null) + { + replorigin_drop_by_name(originname, true, false); + } /* * Update the state to READY only after the origin cleanup. */ - UpdateSubscriptionRelState(MyLogicalRepWorker->subid, - rstate->relid, rstate->state, - rstate->lsn); + UpdateSubscriptionRel(MyLogicalRepWorker->subid, + rstate->relid, + rstate->state, + rstate->lsn, + NULL, + NULL); + + CommitTransactionCommand(); + started_tx = false; } } else @@ -584,12 +685,22 @@ process_syncing_tables_for_apply(XLogRecPtr current_lsn) TimestampDifferenceExceeds(hentry->last_start_time, now, wal_retrieve_retry_interval)) { + if (IsTransactionState()) + CommitTransactionCommand(); + StartTransactionCommand(); + started_tx = true; + + MySubscription->lastusedid++; + UpdateSubscriptionLastSlotId(MyLogicalRepWorker->subid, + MySubscription->lastusedid); + logicalrep_worker_launch(MyLogicalRepWorker->dbid, MySubscription->oid, MySubscription->name, MyLogicalRepWorker->userid, rstate->relid, - DSM_HANDLE_INVALID); + DSM_HANDLE_INVALID, + MySubscription->lastusedid); hentry->last_start_time = now; } } @@ -1190,8 +1301,8 @@ copy_table(Relation rel) * The name must not exceed NAMEDATALEN - 1 because of remote node constraints * on slot name length. We append system_identifier to avoid slot_name * collision with subscriptions in other clusters. With the current scheme - * pg_%u_sync_%u_UINT64_FORMAT (3 + 10 + 6 + 10 + 20 + '\0'), the maximum - * length of slot_name will be 50. + * pg_%u_sync_%lu_UINT64_FORMAT (3 + 10 + 6 + 20 + 20 + '\0'), the maximum + * length of slot_name will be 45. * * The returned slot name is stored in the supplied buffer (syncslotname) with * the given size. @@ -1202,11 +1313,11 @@ copy_table(Relation rel) * had changed. */ void -ReplicationSlotNameForTablesync(Oid suboid, Oid relid, +ReplicationSlotNameForTablesync(Oid suboid, int64 slotid, char *syncslotname, Size szslot) { - snprintf(syncslotname, szslot, "pg_%u_sync_%u_" UINT64_FORMAT, suboid, - relid, GetSystemIdentifier()); + snprintf(syncslotname, szslot, "pg_%u_sync_%lld_" UINT64_FORMAT, suboid, + (long long) slotid, GetSystemIdentifier()); } /* @@ -1229,6 +1340,7 @@ LogicalRepSyncTableStart(XLogRecPtr *origin_startpos) WalRcvExecResult *res; char originname[NAMEDATALEN]; RepOriginId originid; + char *prev_slotname; /* Check the state of the table synchronization. */ StartTransactionCommand(); @@ -1257,7 +1369,7 @@ LogicalRepSyncTableStart(XLogRecPtr *origin_startpos) /* Calculate the name of the tablesync slot. */ slotname = (char *) palloc(NAMEDATALEN); ReplicationSlotNameForTablesync(MySubscription->oid, - MyLogicalRepWorker->relid, + MyLogicalRepWorker->rep_slot_id, slotname, NAMEDATALEN); @@ -1277,11 +1389,25 @@ LogicalRepSyncTableStart(XLogRecPtr *origin_startpos) MyLogicalRepWorker->relstate == SUBREL_STATE_DATASYNC || MyLogicalRepWorker->relstate == SUBREL_STATE_FINISHEDCOPY); + /* + * See if tablesync of the current relation has been started with another + * replication slot. + * + * Read previous slot name from the catalog, if exists. + */ + prev_slotname = (char *) palloc0(NAMEDATALEN); + StartTransactionCommand(); + GetSubscriptionRelReplicationSlot(MyLogicalRepWorker->subid, + MyLogicalRepWorker->relid, + prev_slotname); + /* Assign the origin tracking record name. */ ReplicationOriginNameForLogicalRep(MySubscription->oid, - MyLogicalRepWorker->relid, originname, - sizeof(originname)); + sizeof(originname), + true); + + CommitTransactionCommand(); if (MyLogicalRepWorker->relstate == SUBREL_STATE_DATASYNC) { @@ -1296,10 +1422,48 @@ LogicalRepSyncTableStart(XLogRecPtr *origin_startpos) * breakdown then it wouldn't have succeeded so trying it next time * seems like a better bet. */ - ReplicationSlotDropAtPubNode(LogRepWorkerWalRcvConn, slotname, true); + if (strlen(prev_slotname) > 0) + { + ReplicationSlotDropAtPubNode(LogRepWorkerWalRcvConn, prev_slotname, true); + + StartTransactionCommand(); + /* Replication drop might still exist. Try to drop */ + replorigin_drop_by_name(originname, true, false); + + /* + * Remove replication slot and origin name from the relation's + * catalog record + */ + UpdateSubscriptionRel(MyLogicalRepWorker->subid, + MyLogicalRepWorker->relid, + MyLogicalRepWorker->relstate, + MyLogicalRepWorker->relstate_lsn, + NULL, + NULL); + CommitTransactionCommand(); + } } else if (MyLogicalRepWorker->relstate == SUBREL_STATE_FINISHEDCOPY) { + /* + * At this point, the table that is currently being synchronized + * should have its replication slot name filled in the catalog. The + * tablesync process was started with another sync worker and + * replication slot. We need to continue using the same replication + * slot in this worker too. + */ + if (strlen(prev_slotname) == 0) + { + elog(ERROR, "Replication slot could not be found for relation %u", + MyLogicalRepWorker->relid); + } + + /* + * Proceed with the correct replication slot. Use previously created + * replication slot to sync this table. + */ + slotname = prev_slotname; + /* * The COPY phase was previously done, but tablesync then crashed * before it was able to finish normally. @@ -1320,6 +1484,7 @@ LogicalRepSyncTableStart(XLogRecPtr *origin_startpos) goto copy_table_done; } + /* Preparing for table copy operation */ SpinLockAcquire(&MyLogicalRepWorker->relmutex); MyLogicalRepWorker->relstate = SUBREL_STATE_DATASYNC; MyLogicalRepWorker->relstate_lsn = InvalidXLogRecPtr; @@ -1327,10 +1492,12 @@ LogicalRepSyncTableStart(XLogRecPtr *origin_startpos) /* Update the state and make it visible to others. */ StartTransactionCommand(); - UpdateSubscriptionRelState(MyLogicalRepWorker->subid, - MyLogicalRepWorker->relid, - MyLogicalRepWorker->relstate, - MyLogicalRepWorker->relstate_lsn); + UpdateSubscriptionRel(MyLogicalRepWorker->subid, + MyLogicalRepWorker->relid, + MyLogicalRepWorker->relstate, + MyLogicalRepWorker->relstate_lsn, + slotname, + originname); CommitTransactionCommand(); pgstat_report_stat(true); @@ -1369,6 +1536,7 @@ LogicalRepSyncTableStart(XLogRecPtr *origin_startpos) GetUserNameFromId(GetUserId(), true), RelationGetRelationName(rel)))); + /* * Start a transaction in the remote node in REPEATABLE READ mode. This * ensures that both the replication slot we create (see below) and the @@ -1384,55 +1552,99 @@ LogicalRepSyncTableStart(XLogRecPtr *origin_startpos) res->err))); walrcv_clear_result(res); + originid = replorigin_by_name(originname, true); + /* * Create a new permanent logical decoding slot. This slot will be used * for the catchup phase after COPY is done, so tell it to use the * snapshot to make the final data consistent. * + * Replication slot will only be created if either this is the first run + * of the worker or we're not using a previous replication slot. + * * Prevent cancel/die interrupts while creating slot here because it is * possible that before the server finishes this command, a concurrent * drop subscription happens which would complete without removing this * slot leading to a dangling slot on the server. + * */ - HOLD_INTERRUPTS(); - walrcv_create_slot(LogRepWorkerWalRcvConn, - slotname, false /* permanent */ , false /* two_phase */ , - CRS_USE_SNAPSHOT, origin_startpos); - RESUME_INTERRUPTS(); - - /* - * Setup replication origin tracking. The purpose of doing this before the - * copy is to avoid doing the copy again due to any error in setting up - * origin tracking. - */ - originid = replorigin_by_name(originname, true); - if (!OidIsValid(originid)) + if (!MyLogicalRepWorker->created_slot) { + HOLD_INTERRUPTS(); + walrcv_create_slot(LogRepWorkerWalRcvConn, + slotname, false /* permanent */ , false /* two_phase */ , + CRS_USE_SNAPSHOT, origin_startpos); + RESUME_INTERRUPTS(); + /* - * Origin tracking does not exist, so create it now. - * - * Then advance to the LSN got from walrcv_create_slot. This is WAL - * logged for the purpose of recovery. Locks are to prevent the - * replication origin from vanishing while advancing. + * Remember that we created the slot so that we will not try to create + * it again. */ - originid = replorigin_create(originname); - - LockRelationOid(ReplicationOriginRelationId, RowExclusiveLock); - replorigin_advance(originid, *origin_startpos, InvalidXLogRecPtr, - true /* go backward */ , true /* WAL log */ ); - UnlockRelationOid(ReplicationOriginRelationId, RowExclusiveLock); + SpinLockAcquire(&MyLogicalRepWorker->relmutex); + MyLogicalRepWorker->created_slot = true; + SpinLockRelease(&MyLogicalRepWorker->relmutex); - replorigin_session_setup(originid, 0); - replorigin_session_origin = originid; + /* + * Setup replication origin tracking. The purpose of doing this before + * the copy is to avoid doing the copy again due to any error in + * setting up origin tracking. + */ + if (!OidIsValid(originid)) + { + /* + * Origin tracking does not exist, so create it now. + */ + originid = replorigin_create(originname); + } + else + { + /* + * At this point, there shouldn't be any existing replication + * origin wit the same name. + */ + ereport(ERROR, + (errcode(ERRCODE_DUPLICATE_OBJECT), + errmsg("replication origin \"%s\" already exists", + originname))); + } } else { - ereport(ERROR, - (errcode(ERRCODE_DUPLICATE_OBJECT), - errmsg("replication origin \"%s\" already exists", - originname))); + /* + * Do not create a new replication slot, reuse the existing one + * instead. Use a new snapshot for the replication slot to ensure that + * tablesync and apply proceses are consistent with each other. + */ + WalRcvStreamOptions options; + int server_version; + + server_version = walrcv_server_version(LogRepWorkerWalRcvConn); + options.proto.logical.proto_version = + server_version >= 150000 ? LOGICALREP_PROTO_TWOPHASE_VERSION_NUM : + server_version >= 140000 ? LOGICALREP_PROTO_STREAM_VERSION_NUM : + LOGICALREP_PROTO_VERSION_NUM; + options.proto.logical.publication_names = MySubscription->publications; + + HOLD_INTERRUPTS(); + walrcv_slot_snapshot(LogRepWorkerWalRcvConn, slotname, &options, origin_startpos); + RESUME_INTERRUPTS(); } + /* + * Advance to the LSN got from walrcv_create_slot. This is WAL + * logged for the purpose of recovery. Locks are to prevent the + * replication origin from vanishing while advancing. + * + * Then setup replication origin tracking. + */ + LockRelationOid(ReplicationOriginRelationId, RowExclusiveLock); + replorigin_advance(originid, *origin_startpos, InvalidXLogRecPtr, + true /* go backward */ , true /* WAL log */ ); + UnlockRelationOid(ReplicationOriginRelationId, RowExclusiveLock); + + replorigin_session_setup(originid, 0); + replorigin_session_origin = originid; + /* Now do the initial data copy */ PushActiveSnapshot(GetTransactionSnapshot()); copy_table(rel); @@ -1455,10 +1667,12 @@ LogicalRepSyncTableStart(XLogRecPtr *origin_startpos) * Update the persisted state to indicate the COPY phase is done; make it * visible to others. */ - UpdateSubscriptionRelState(MyLogicalRepWorker->subid, - MyLogicalRepWorker->relid, - SUBREL_STATE_FINISHEDCOPY, - MyLogicalRepWorker->relstate_lsn); + UpdateSubscriptionRel(MyLogicalRepWorker->subid, + MyLogicalRepWorker->relid, + SUBREL_STATE_FINISHEDCOPY, + MyLogicalRepWorker->relstate_lsn, + slotname, + originname); CommitTransactionCommand(); diff --git a/src/backend/replication/logical/worker.c b/src/backend/replication/logical/worker.c index 79cda39445..032f402aed 100644 --- a/src/backend/replication/logical/worker.c +++ b/src/backend/replication/logical/worker.c @@ -385,6 +385,7 @@ static void stream_open_file(Oid subid, TransactionId xid, static void stream_write_change(char action, StringInfo s); static void stream_open_and_write_change(TransactionId xid, char action, StringInfo s); static void stream_close_file(void); +static void stream_build_options(WalRcvStreamOptions *options, char *slotname, XLogRecPtr *origin_startpos); static void send_feedback(XLogRecPtr recvpos, bool force, bool requestReply); @@ -443,18 +444,26 @@ get_worker_name(void) * Form the origin name for the subscription. * * This is a common function for tablesync and other workers. Tablesync workers - * must pass a valid relid. Other callers must pass relid = InvalidOid. + * must pass is_tablesync true so that origin name includes slot id. * * Return the name in the supplied buffer. */ void -ReplicationOriginNameForLogicalRep(Oid suboid, Oid relid, - char *originname, Size szoriginname) +ReplicationOriginNameForLogicalRep(Oid suboid, char *originname, + Size szoriginname, bool is_tablesync) { - if (OidIsValid(relid)) + if (is_tablesync) { - /* Replication origin name for tablesync workers. */ - snprintf(originname, szoriginname, "pg_%u_%u", suboid, relid); + bool is_null = true; + + /* + * Replication origin name for tablesync workers. First, look into the + * catalog. If originname does not exist, then use the default name. + */ + GetSubscriptionRelOrigin(suboid, MyLogicalRepWorker->relid, + originname, &is_null); + if (is_null) + snprintf(originname, szoriginname, "pg_%u_%lld", suboid, (long long) MyLogicalRepWorker->rep_slot_id); } else { @@ -3573,6 +3582,23 @@ LogicalRepApplyLoop(XLogRecPtr last_received) MemoryContextReset(ApplyMessageContext); } + /* + * apply_dispatch() may have gone into apply_handle_commit() + * which can move to next table while running + * process_syncing_tables_for_sync. Before we were able to + * reuse tablesync workers, that + * process_syncing_tables_for_sync call would exit the worker + * instead of moving to next table. Now that tablesync workers + * can be reused, we need to take care of memory contexts here + * before moving to sync a table. + */ + if (MyLogicalRepWorker->move_to_next_rel) + { + MemoryContextResetAndDeleteChildren(ApplyMessageContext); + MemoryContextSwitchTo(TopMemoryContext); + return; + } + len = walrcv_receive(LogRepWorkerWalRcvConn, &buf, &fd); } } @@ -3592,6 +3618,10 @@ LogicalRepApplyLoop(XLogRecPtr last_received) /* Process any table synchronization changes. */ process_syncing_tables(last_received); + if (MyLogicalRepWorker->move_to_next_rel) + { + endofstream = true; + } } /* Cleanup the memory. */ @@ -3694,8 +3724,16 @@ LogicalRepApplyLoop(XLogRecPtr last_received) error_context_stack = errcallback.previous; apply_error_context_stack = error_context_stack; - /* All done */ - walrcv_endstreaming(LogRepWorkerWalRcvConn, &tli); + /* + * If it's moving to next relation, this is a sync worker. Sync workers + * end the streaming during process_syncing_tables_for_sync. Calling + * endstreaming twice causes "no COPY in progress" errors. + */ + if (!MyLogicalRepWorker->move_to_next_rel) + { + /* All done */ + walrcv_endstreaming(LogRepWorkerWalRcvConn, &tli); + } } /* @@ -4277,6 +4315,56 @@ stream_open_and_write_change(TransactionId xid, char action, StringInfo s) stream_stop_internal(xid); } + /* stream_build_options + * Build logical replication streaming options. + * + * This function sets streaming options including replication slot name + * and origin start position. Workers need these options for logical replication. + */ +static void +stream_build_options(WalRcvStreamOptions *options, char *slotname, XLogRecPtr *origin_startpos) +{ + int server_version; + + options->logical = true; + options->startpoint = *origin_startpos; + options->slotname = slotname; + + server_version = walrcv_server_version(LogRepWorkerWalRcvConn); + options->proto.logical.proto_version = + server_version >= 160000 ? LOGICALREP_PROTO_STREAM_PARALLEL_VERSION_NUM : + server_version >= 150000 ? LOGICALREP_PROTO_TWOPHASE_VERSION_NUM : + server_version >= 140000 ? LOGICALREP_PROTO_STREAM_VERSION_NUM : + LOGICALREP_PROTO_VERSION_NUM; + + options->proto.logical.publication_names = MySubscription->publications; + options->proto.logical.binary = MySubscription->binary; + options->proto.logical.twophase = false; + options->proto.logical.origin = pstrdup(MySubscription->origin); + + /* + * Assign the appropriate option value for streaming option according to + * the 'streaming' mode and the publisher's ability to support that mode. + */ + if (server_version >= 160000 && + MySubscription->stream == LOGICALREP_STREAM_PARALLEL) + { + options->proto.logical.streaming_str = "parallel"; + MyLogicalRepWorker->parallel_apply = true; + } + else if (server_version >= 140000 && + MySubscription->stream != LOGICALREP_STREAM_OFF) + { + options->proto.logical.streaming_str = "on"; + MyLogicalRepWorker->parallel_apply = false; + } + else + { + options->proto.logical.streaming_str = NULL; + MyLogicalRepWorker->parallel_apply = false; + } +} + /* * Cleanup the memory for subxacts and reset the related variables. */ @@ -4351,6 +4439,9 @@ start_table_sync(XLogRecPtr *origin_startpos, char **myslotname) /* allocate slot name in long-lived context */ *myslotname = MemoryContextStrdup(ApplyContext, syncslotname); + + /* Keep the replication slot name used for this sync. */ + MyLogicalRepWorker->slot_name = *myslotname; pfree(syncslotname); } @@ -4388,6 +4479,135 @@ start_apply(XLogRecPtr origin_startpos) PG_END_TRY(); } +/* + * Runs the tablesync worker. + * It starts table sync. After successful sync, + * builds streaming options and starts streaming. + */ +static void +run_tablesync_worker(WalRcvStreamOptions *options, + char *slotname, + char *originname, + int originname_size, + XLogRecPtr *origin_startpos) +{ + /* Set this to false for safety, in case we're already reusing the worker */ + MyLogicalRepWorker->move_to_next_rel = false; + + start_table_sync(origin_startpos, &slotname); + + /* + * Allocate the origin name in long-lived context for error context + * message. + */ + StartTransactionCommand(); + ReplicationOriginNameForLogicalRep(MySubscription->oid, + originname, + originname_size, + true); + CommitTransactionCommand(); + + set_apply_error_context_origin(originname); + + stream_build_options(options, slotname, origin_startpos); + + /* Start normal logical streaming replication. */ + walrcv_startstreaming(LogRepWorkerWalRcvConn, options); +} + +/* + * Runs the apply worker. + * It sets up replication origin, the streaming options + * and then starts streaming. + */ +static void +run_apply_worker(WalRcvStreamOptions *options, + char *slotname, + char *originname, + int originname_size, + XLogRecPtr *origin_startpos) +{ + /* This is the leader apply worker */ + RepOriginId originid; + TimeLineID startpointTLI; + char *err; + + slotname = MySubscription->slotname; + + /* + * This shouldn't happen if the subscription is enabled, but guard + * against DDL bugs or manual catalog changes. (libpqwalreceiver will + * crash if slot is NULL.) + */ + if (!slotname) + ereport(ERROR, + (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("subscription has no replication slot set"))); + + /* Setup replication origin tracking. */ + StartTransactionCommand(); + ReplicationOriginNameForLogicalRep(MySubscription->oid, originname, + originname_size, false); + originid = replorigin_by_name(originname, true); + if (!OidIsValid(originid)) + originid = replorigin_create(originname); + replorigin_session_setup(originid, 0); + replorigin_session_origin = originid; + *origin_startpos = replorigin_session_get_progress(false); + CommitTransactionCommand(); + + LogRepWorkerWalRcvConn = walrcv_connect(MySubscription->conninfo, true, + MySubscription->name, &err); + if (LogRepWorkerWalRcvConn == NULL) + ereport(ERROR, + (errcode(ERRCODE_CONNECTION_FAILURE), + errmsg("could not connect to the publisher: %s", err))); + + /* + * We don't really use the output identify_system for anything but it + * does some initializations on the upstream so let's still call it. + */ + (void) walrcv_identify_system(LogRepWorkerWalRcvConn, &startpointTLI); + + set_apply_error_context_origin(originname); + + stream_build_options(options, slotname, origin_startpos); + + /* + * Even when the two_phase mode is requested by the user, it remains as + * the tri-state PENDING until all tablesyncs have reached READY state. + * Only then, can it become ENABLED. + * + * Note: If the subscription has no tables then leave the state as + * PENDING, which allows ALTER SUBSCRIPTION ... REFRESH PUBLICATION to + * work. + */ + if (MySubscription->twophasestate == LOGICALREP_TWOPHASE_STATE_PENDING && + AllTablesyncsReady()) + { + /* Start streaming with two_phase enabled */ + options->proto.logical.twophase = true; + walrcv_startstreaming(LogRepWorkerWalRcvConn, options); + + StartTransactionCommand(); + UpdateTwoPhaseState(MySubscription->oid, LOGICALREP_TWOPHASE_STATE_ENABLED); + MySubscription->twophasestate = LOGICALREP_TWOPHASE_STATE_ENABLED; + CommitTransactionCommand(); + } + else + { + walrcv_startstreaming(LogRepWorkerWalRcvConn, options); + } + + ereport(DEBUG1, + (errmsg_internal("logical replication apply worker for subscription \"%s\" two_phase is %s", + MySubscription->name, + MySubscription->twophasestate == LOGICALREP_TWOPHASE_STATE_DISABLED ? "DISABLED" : + MySubscription->twophasestate == LOGICALREP_TWOPHASE_STATE_PENDING ? "PENDING" : + MySubscription->twophasestate == LOGICALREP_TWOPHASE_STATE_ENABLED ? "ENABLED" : + "?"))); +} + /* * Common initialization for leader apply worker and parallel apply worker. * @@ -4477,7 +4697,6 @@ ApplyWorkerMain(Datum main_arg) XLogRecPtr origin_startpos = InvalidXLogRecPtr; char *myslotname = NULL; WalRcvStreamOptions options; - int server_version; /* Attach to slot */ logicalrep_worker_attach(worker_slot); @@ -4505,156 +4724,48 @@ ApplyWorkerMain(Datum main_arg) elog(DEBUG1, "connecting to publisher using connection string \"%s\"", MySubscription->conninfo); - if (am_tablesync_worker()) - { - start_table_sync(&origin_startpos, &myslotname); - - ReplicationOriginNameForLogicalRep(MySubscription->oid, - MyLogicalRepWorker->relid, - originname, - sizeof(originname)); - set_apply_error_context_origin(originname); - } - else - { - /* This is the leader apply worker */ - RepOriginId originid; - TimeLineID startpointTLI; - char *err; - - myslotname = MySubscription->slotname; - - /* - * This shouldn't happen if the subscription is enabled, but guard - * against DDL bugs or manual catalog changes. (libpqwalreceiver will - * crash if slot is NULL.) - */ - if (!myslotname) - ereport(ERROR, - (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), - errmsg("subscription has no replication slot set"))); - - /* Setup replication origin tracking. */ - StartTransactionCommand(); - ReplicationOriginNameForLogicalRep(MySubscription->oid, InvalidOid, - originname, sizeof(originname)); - originid = replorigin_by_name(originname, true); - if (!OidIsValid(originid)) - originid = replorigin_create(originname); - replorigin_session_setup(originid, 0); - replorigin_session_origin = originid; - origin_startpos = replorigin_session_get_progress(false); - CommitTransactionCommand(); - - LogRepWorkerWalRcvConn = walrcv_connect(MySubscription->conninfo, true, - MySubscription->name, &err); - if (LogRepWorkerWalRcvConn == NULL) - ereport(ERROR, - (errcode(ERRCODE_CONNECTION_FAILURE), - errmsg("could not connect to the publisher: %s", err))); - - /* - * We don't really use the output identify_system for anything but it - * does some initializations on the upstream so let's still call it. - */ - (void) walrcv_identify_system(LogRepWorkerWalRcvConn, &startpointTLI); - - set_apply_error_context_origin(originname); - } - /* * Setup callback for syscache so that we know when something changes in - * the subscription relation state. + * the subscription relation state. Do this outside the loop to avoid + * exceeding MAX_SYSCACHE_CALLBACKS */ CacheRegisterSyscacheCallback(SUBSCRIPTIONRELMAP, invalidate_syncing_table_states, (Datum) 0); - /* Build logical replication streaming options. */ - options.logical = true; - options.startpoint = origin_startpos; - options.slotname = myslotname; - - server_version = walrcv_server_version(LogRepWorkerWalRcvConn); - options.proto.logical.proto_version = - server_version >= 160000 ? LOGICALREP_PROTO_STREAM_PARALLEL_VERSION_NUM : - server_version >= 150000 ? LOGICALREP_PROTO_TWOPHASE_VERSION_NUM : - server_version >= 140000 ? LOGICALREP_PROTO_STREAM_VERSION_NUM : - LOGICALREP_PROTO_VERSION_NUM; - - options.proto.logical.publication_names = MySubscription->publications; - options.proto.logical.binary = MySubscription->binary; - /* - * Assign the appropriate option value for streaming option according to - * the 'streaming' mode and the publisher's ability to support that mode. + * The loop where worker does its job. It loops until the worker is not + * reused. */ - if (server_version >= 160000 && - MySubscription->stream == LOGICALREP_STREAM_PARALLEL) - { - options.proto.logical.streaming_str = "parallel"; - MyLogicalRepWorker->parallel_apply = true; - } - else if (server_version >= 140000 && - MySubscription->stream != LOGICALREP_STREAM_OFF) - { - options.proto.logical.streaming_str = "on"; - MyLogicalRepWorker->parallel_apply = false; - } - else - { - options.proto.logical.streaming_str = NULL; - MyLogicalRepWorker->parallel_apply = false; - } - - options.proto.logical.twophase = false; - options.proto.logical.origin = pstrdup(MySubscription->origin); - - if (!am_tablesync_worker()) + while (MyLogicalRepWorker->is_first_run || + MyLogicalRepWorker->move_to_next_rel) { - /* - * Even when the two_phase mode is requested by the user, it remains - * as the tri-state PENDING until all tablesyncs have reached READY - * state. Only then, can it become ENABLED. - * - * Note: If the subscription has no tables then leave the state as - * PENDING, which allows ALTER SUBSCRIPTION ... REFRESH PUBLICATION to - * work. - */ - if (MySubscription->twophasestate == LOGICALREP_TWOPHASE_STATE_PENDING && - AllTablesyncsReady()) + if (am_tablesync_worker()) { - /* Start streaming with two_phase enabled */ - options.proto.logical.twophase = true; - walrcv_startstreaming(LogRepWorkerWalRcvConn, &options); - - StartTransactionCommand(); - UpdateTwoPhaseState(MySubscription->oid, LOGICALREP_TWOPHASE_STATE_ENABLED); - MySubscription->twophasestate = LOGICALREP_TWOPHASE_STATE_ENABLED; - CommitTransactionCommand(); + /* + * This is a tablesync worker. Start syncing tables before + * starting the apply loop. + */ + run_tablesync_worker(&options, myslotname, originname, sizeof(originname), &origin_startpos); } else { - walrcv_startstreaming(LogRepWorkerWalRcvConn, &options); + /* This is main apply worker */ + run_apply_worker(&options, myslotname, originname, sizeof(originname), &origin_startpos); } - ereport(DEBUG1, - (errmsg_internal("logical replication apply worker for subscription \"%s\" two_phase is %s", - MySubscription->name, - MySubscription->twophasestate == LOGICALREP_TWOPHASE_STATE_DISABLED ? "DISABLED" : - MySubscription->twophasestate == LOGICALREP_TWOPHASE_STATE_PENDING ? "PENDING" : - MySubscription->twophasestate == LOGICALREP_TWOPHASE_STATE_ENABLED ? "ENABLED" : - "?"))); - } - else - { - /* Start normal logical streaming replication. */ - walrcv_startstreaming(LogRepWorkerWalRcvConn, &options); - } - - /* Run the main loop. */ - start_apply(origin_startpos); + /* Run the main loop. */ + start_apply(origin_startpos); + if (MyLogicalRepWorker->move_to_next_rel) + { + StartTransactionCommand(); + ereport(LOG, + (errmsg("logical replication table synchronization worker for subscription \"%s\" has moved to sync table \"%s\".", + MySubscription->name, get_rel_name(MyLogicalRepWorker->relid)))); + CommitTransactionCommand(); + } + } proc_exit(0); } diff --git a/src/include/catalog/pg_subscription.h b/src/include/catalog/pg_subscription.h index b0f2a1705d..a0ee12e259 100644 --- a/src/include/catalog/pg_subscription.h +++ b/src/include/catalog/pg_subscription.h @@ -103,6 +103,9 @@ CATALOG(pg_subscription,6100,SubscriptionRelationId) BKI_SHARED_RELATION BKI_ROW /* Only publish data originating from the specified origin */ text suborigin BKI_DEFAULT(LOGICALREP_ORIGIN_ANY); + + /* The last used ID to create a replication slot for tablesync */ + int64 sublastusedid BKI_DEFAULT(0); #endif } FormData_pg_subscription; @@ -137,6 +140,8 @@ typedef struct Subscription List *publications; /* List of publication names to subscribe to */ char *origin; /* Only publish data originating from the * specified origin */ + int64 lastusedid; /* Last used unique ID to create replication + * slots in tablesync */ } Subscription; /* Disallow streaming in-progress transactions. */ @@ -157,6 +162,7 @@ typedef struct Subscription extern Subscription *GetSubscription(Oid subid, bool missing_ok); extern void FreeSubscription(Subscription *sub); extern void DisableSubscription(Oid subid); +extern void UpdateSubscriptionLastSlotId(Oid subid, int64 lastusedid); extern int CountDBSubscriptions(Oid dbid); diff --git a/src/include/catalog/pg_subscription_rel.h b/src/include/catalog/pg_subscription_rel.h index 60a2bcca23..a35d04cccd 100644 --- a/src/include/catalog/pg_subscription_rel.h +++ b/src/include/catalog/pg_subscription_rel.h @@ -44,6 +44,12 @@ CATALOG(pg_subscription_rel,6102,SubscriptionRelRelationId) * used for synchronization * coordination, or NULL if not * valid */ + text srrelslotname BKI_FORCE_NULL; /* name of the replication + * slot for relation in + * subscription */ + text srreloriginname BKI_FORCE_NULL; /* origin name for relation in + * subscription */ + #endif } FormData_pg_subscription_rel; @@ -81,10 +87,17 @@ typedef struct SubscriptionRelState } SubscriptionRelState; extern void AddSubscriptionRelState(Oid subid, Oid relid, char state, - XLogRecPtr sublsn); + XLogRecPtr sublsn, char *relslotname, char *reloriginname); extern void UpdateSubscriptionRelState(Oid subid, Oid relid, char state, XLogRecPtr sublsn); +extern void UpdateSubscriptionRel(Oid subid, Oid relid, char state, + XLogRecPtr sublsn, char *relslotname, char *reloriginname); +extern void UpdateSubscriptionRelReplicationSlot(Oid subid, Oid relid, char *relslotname); + extern char GetSubscriptionRelState(Oid subid, Oid relid, XLogRecPtr *sublsn); +extern void GetSubscriptionRelReplicationSlot(Oid subid, Oid relid, char *slotname); +extern void GetSubscriptionRelOrigin(Oid subid, Oid relid, char *reloriginname, bool *isnull); + extern void RemoveSubscriptionRel(Oid subid, Oid relid); extern bool HasSubscriptionRelations(Oid subid); diff --git a/src/include/replication/slot.h b/src/include/replication/slot.h index 8872c80cdf..3547daaaec 100644 --- a/src/include/replication/slot.h +++ b/src/include/replication/slot.h @@ -219,8 +219,9 @@ extern bool InvalidateObsoleteReplicationSlots(XLogSegNo oldestSegno); extern ReplicationSlot *SearchNamedReplicationSlot(const char *name, bool need_lock); extern int ReplicationSlotIndex(ReplicationSlot *slot); extern bool ReplicationSlotName(int index, Name name); -extern void ReplicationSlotNameForTablesync(Oid suboid, Oid relid, char *syncslotname, Size szslot); +extern void ReplicationSlotNameForTablesync(Oid suboid, int64 slotid, char *syncslotname, Size szslot); extern void ReplicationSlotDropAtPubNode(WalReceiverConn *wrconn, char *slotname, bool missing_ok); +extern List *GetReplicationSlotNamesBySubId(WalReceiverConn *wrconn, Oid subid, bool missing_ok); extern void StartupReplicationSlots(void); extern void CheckPointReplicationSlots(void); diff --git a/src/include/replication/worker_internal.h b/src/include/replication/worker_internal.h index db891eea8a..aee0d85f1d 100644 --- a/src/include/replication/worker_internal.h +++ b/src/include/replication/worker_internal.h @@ -35,6 +35,26 @@ typedef struct LogicalRepWorker /* Indicates if this slot is used or free. */ bool in_use; + /* + * Indicates if worker is running for the first time or in reuse + */ + bool is_first_run; + + /* + * Indicates if the sync worker created a replication slot or it reuses an + * existing one created by another worker. + */ + bool created_slot; + + /* + * Unique identifier for replication slot to be created by tablesnync + * workers, if needed. + */ + int64 rep_slot_id; + + /* Replication slot name used by the worker. */ + char *slot_name; + /* Increased every time the slot is taken by new worker. */ uint16 generation; @@ -56,6 +76,12 @@ typedef struct LogicalRepWorker XLogRecPtr relstate_lsn; slock_t relmutex; + /* + * Used to indicate whether sync worker will be reused for another + * relation + */ + bool move_to_next_rel; + /* * Used to create the changes and subxact files for the streaming * transactions. Upon the arrival of the first streaming transaction or @@ -231,7 +257,8 @@ extern LogicalRepWorker *logicalrep_worker_find(Oid subid, Oid relid, extern List *logicalrep_workers_find(Oid subid, bool only_running); extern bool logicalrep_worker_launch(Oid dbid, Oid subid, const char *subname, Oid userid, Oid relid, - dsm_handle subworker_dsm); + dsm_handle subworker_dsm, + int64 slotid); extern void logicalrep_worker_stop(Oid subid, Oid relid); extern void logicalrep_pa_worker_stop(int slot_no, uint16 generation); extern void logicalrep_worker_wakeup(Oid subid, Oid relid); @@ -239,8 +266,8 @@ extern void logicalrep_worker_wakeup_ptr(LogicalRepWorker *worker); extern int logicalrep_sync_worker_count(Oid subid); -extern void ReplicationOriginNameForLogicalRep(Oid suboid, Oid relid, - char *originname, Size szoriginname); +extern void ReplicationOriginNameForLogicalRep(Oid suboid, char *originname, + Size szoriginname, bool is_tablesync); extern char *LogicalRepSyncTableStart(XLogRecPtr *origin_startpos); extern bool AllTablesyncsReady(void); diff --git a/src/test/regress/expected/misc_sanity.out b/src/test/regress/expected/misc_sanity.out index a57fd142a9..3d34a21421 100644 --- a/src/test/regress/expected/misc_sanity.out +++ b/src/test/regress/expected/misc_sanity.out @@ -47,20 +47,22 @@ WHERE c.oid < 16384 AND relkind = 'r' AND attstorage != 'p' ORDER BY 1, 2; - relname | attname | atttypid --------------------------+---------------+-------------- - pg_attribute | attacl | aclitem[] - pg_attribute | attfdwoptions | text[] - pg_attribute | attmissingval | anyarray - pg_attribute | attoptions | text[] - pg_class | relacl | aclitem[] - pg_class | reloptions | text[] - pg_class | relpartbound | pg_node_tree - pg_index | indexprs | pg_node_tree - pg_index | indpred | pg_node_tree - pg_largeobject | data | bytea - pg_largeobject_metadata | lomacl | aclitem[] -(11 rows) + relname | attname | atttypid +-------------------------+-----------------+-------------- + pg_attribute | attacl | aclitem[] + pg_attribute | attfdwoptions | text[] + pg_attribute | attmissingval | anyarray + pg_attribute | attoptions | text[] + pg_class | relacl | aclitem[] + pg_class | reloptions | text[] + pg_class | relpartbound | pg_node_tree + pg_index | indexprs | pg_node_tree + pg_index | indpred | pg_node_tree + pg_largeobject | data | bytea + pg_largeobject_metadata | lomacl | aclitem[] + pg_subscription_rel | srreloriginname | text + pg_subscription_rel | srrelslotname | text +(13 rows) -- system catalogs without primary keys -- -- 2.25.1 ^ permalink raw reply [nested|flat] 3+ messages in thread
* RE: [PATCH] Reuse Workers and Replication Slots during Logical Replication 2023-01-11 08:31 Re: [PATCH] Reuse Workers and Replication Slots during Logical Replication Melih Mutlu <[email protected]> @ 2023-01-17 11:15 ` [email protected] <[email protected]> 0 siblings, 0 replies; 3+ messages in thread From: [email protected] @ 2023-01-17 11:15 UTC (permalink / raw) To: Melih Mutlu <[email protected]>; Amit Kapila <[email protected]>; pgsql-hackers On Wed, Jan 11, 2023 4:31 PM Melih Mutlu <[email protected]> wrote: > Rebased the patch to resolve conflicts. Thanks for your patch set. Here are some comments: v3-0001* patch =============== 1. typedefs.list I think we also need to add "walrcv_slot_snapshot_fn" to this file. v7-0002* patch =============== 1. About function ReplicationOriginNameForLogicalRep() Do we need to modify the API of this function? I think the original API could also meet the current needs. Since this is not a static function, I think it seems better to keep the original API if there is no reason. Please let me know if I'm missing something. ----- 2. Comment atop the function GetSubscriptionRelReplicationSlot +/* + * Get replication slot name of subscription table. + * + * Returns null if the subscription table does not have a replication slot. + */ Since this function always returns NULL, I think it would be better to say the value in "slotname" here instead of the function's return value. If you agree with this, please also kindly modify the comment atop the function GetSubscriptionRelOrigin. ----- 3. typo + * At this point, there shouldn't be any existing replication + * origin wit the same name. wit -> with ----- 4. In function CreateSubscription + values[Anum_pg_subscription_sublastusedid - 1] = Int64GetDatum(1); I think it might be better to initialize this field to NULL or 0 here. Because in the patch, we always ignore the initialized value when launching the sync worker in the function process_syncing_tables_for_apply. And I think we could document in pg-doc that this value means that no tables have been synced yet. ----- 5. New member "created_slot" in structure LogicalRepWorker + /* + * Indicates if the sync worker created a replication slot or it reuses an + * existing one created by another worker. + */ + bool created_slot; I think the second half of the sentence looks inaccurate. Because I think this flag could be false even when we reuse an existing slot created by another worker. Assuming the first run for the worker tries to sync a table which is synced by another sync worker before, and the relstate is set to SUBREL_STATE_FINISHEDCOPY by another sync worker, I think this flag will not be set to true. (see function LogicalRepSyncTableStart) So, what if we simplify the description here and just say that this worker already has it's default slot? If I'm not missing something and you agree with this, please also kindly modify the relevant comment atop the if-statement (!MyLogicalRepWorker->created_slot) in the function LogicalRepSyncTableStart. Regards, Wang Wei ^ permalink raw reply [nested|flat] 3+ messages in thread
end of thread, other threads:[~2023-01-17 11:15 UTC | newest] Thread overview: 3+ messages (download: mbox mbox.gz follow: Atom feed) -- links below jump to the message on this page -- 2021-02-10 06:19 [PATCH v24 7/7] Add support for PRESERVE Dilip Kumar <[email protected]> 2023-01-11 08:31 Re: [PATCH] Reuse Workers and Replication Slots during Logical Replication Melih Mutlu <[email protected]> 2023-01-17 11:15 ` RE: [PATCH] Reuse Workers and Replication Slots during Logical Replication [email protected] <[email protected]>
This inbox is served by agora; see mirroring instructions for how to clone and mirror all data and code used for this inbox