diff --git a/sql/ddl_api.sql b/sql/ddl_api.sql index c1b469bde86..e48baaa1e18 100644 --- a/sql/ddl_api.sql +++ b/sql/ddl_api.sql @@ -216,3 +216,12 @@ CREATE OR REPLACE PROCEDURE @extschema@.refresh_continuous_aggregate( window_start "any", window_end "any" ) LANGUAGE C AS '@MODULE_PATHNAME@', 'ts_continuous_agg_refresh'; + +CREATE OR REPLACE FUNCTION @extschema@.alter_data_node( + node_name NAME, + host TEXT = NULL, + database NAME = NULL, + port INTEGER = NULL, + available BOOLEAN = NULL +) RETURNS TABLE(node_name NAME, host TEXT, database NAME, port INTEGER, available BOOLEAN) +AS '@MODULE_PATHNAME@', 'ts_data_node_alter' LANGUAGE C VOLATILE; diff --git a/sql/pre_install/tables.sql b/sql/pre_install/tables.sql index aec4e73c7d4..fa6e74df393 100644 --- a/sql/pre_install/tables.sql +++ b/sql/pre_install/tables.sql @@ -251,6 +251,8 @@ CREATE TABLE _timescaledb_catalog.chunk_data_node ( CONSTRAINT chunk_data_node_chunk_id_fkey FOREIGN KEY (chunk_id) REFERENCES _timescaledb_catalog.chunk (id) ); +CREATE INDEX chunk_data_node_node_name_idx ON _timescaledb_catalog.chunk_data_node (node_name); + SELECT pg_catalog.pg_extension_config_dump('_timescaledb_catalog.chunk_data_node', ''); -- Default jobs are given the id space [1,1000). User-installed jobs and any jobs created inside tests diff --git a/sql/updates/latest-dev.sql b/sql/updates/latest-dev.sql index 99ea13790f0..ba59f8d4ca2 100644 --- a/sql/updates/latest-dev.sql +++ b/sql/updates/latest-dev.sql @@ -175,3 +175,6 @@ $BODY$ UNION ALL SELECT * FROM _chunk_sizes) AS sizes; $BODY$ SET search_path TO pg_catalog, pg_temp; + + +CREATE INDEX chunk_data_node_node_name_idx ON _timescaledb_catalog.chunk_data_node (node_name); diff --git a/sql/updates/reverse-dev.sql b/sql/updates/reverse-dev.sql index f571e427980..cffdfbf9227 100644 --- a/sql/updates/reverse-dev.sql +++ b/sql/updates/reverse-dev.sql @@ -115,3 +115,5 @@ $BODY$ SELECT * FROM _chunk_sizes) AS sizes; $BODY$ SET search_path TO pg_catalog, pg_temp; + +DROP INDEX IF EXISTS chunk_data_node_node_name_idx; diff --git a/src/cross_module_fn.c b/src/cross_module_fn.c index eec82d93d2a..1c2c8902267 100644 --- a/src/cross_module_fn.c +++ b/src/cross_module_fn.c @@ -102,6 +102,7 @@ CROSSMODULE_WRAPPER(data_node_add); CROSSMODULE_WRAPPER(data_node_delete); CROSSMODULE_WRAPPER(data_node_attach); CROSSMODULE_WRAPPER(data_node_detach); +CROSSMODULE_WRAPPER(data_node_alter); CROSSMODULE_WRAPPER(chunk_drop_replica); CROSSMODULE_WRAPPER(chunk_set_default_data_node); @@ -504,6 +505,7 @@ TSDLLEXPORT CrossModuleFunctions ts_cm_functions_default = { .data_node_attach = error_no_default_fn_pg_community, .data_node_ping = error_no_default_fn_pg_community, .data_node_detach = error_no_default_fn_pg_community, + .data_node_alter = error_no_default_fn_pg_community, .data_node_allow_new_chunks = error_no_default_fn_pg_community, .data_node_block_new_chunks = error_no_default_fn_pg_community, .distributed_exec = error_no_default_fn_pg_community, diff --git a/src/cross_module_fn.h b/src/cross_module_fn.h index 1207a60ab64..4b361ffbcee 100644 --- a/src/cross_module_fn.h +++ b/src/cross_module_fn.h @@ -158,6 +158,7 @@ typedef struct CrossModuleFunctions PGFunction data_node_attach; PGFunction data_node_ping; PGFunction data_node_detach; + PGFunction data_node_alter; PGFunction data_node_allow_new_chunks; PGFunction data_node_block_new_chunks; diff --git a/src/ts_catalog/catalog.c b/src/ts_catalog/catalog.c index a2b6e574afe..89cdf3bcfe1 100644 --- a/src/ts_catalog/catalog.c +++ b/src/ts_catalog/catalog.c @@ -190,6 +190,7 @@ static const TableIndexDef catalog_table_index_definitions[_MAX_CATALOG_TABLES] .names = (char *[]) { [CHUNK_DATA_NODE_CHUNK_ID_NODE_NAME_IDX] = "chunk_data_node_chunk_id_node_name_key", [CHUNK_DATA_NODE_NODE_CHUNK_ID_NODE_NAME_IDX] = "chunk_data_node_node_chunk_id_node_name_key", + [CHUNK_DATA_NODE_NODE_NAME_IDX] = "chunk_data_node_node_name_idx", } }, [TABLESPACE] = { diff --git a/src/ts_catalog/catalog.h b/src/ts_catalog/catalog.h index 56da2d53b9e..083e3457757 100644 --- a/src/ts_catalog/catalog.h +++ b/src/ts_catalog/catalog.h @@ -596,6 +596,7 @@ enum { CHUNK_DATA_NODE_CHUNK_ID_NODE_NAME_IDX, CHUNK_DATA_NODE_NODE_CHUNK_ID_NODE_NAME_IDX, + CHUNK_DATA_NODE_NODE_NAME_IDX, _MAX_CHUNK_DATA_NODE_INDEX, }; @@ -625,6 +626,17 @@ struct FormData_chunk_data_node_node_chunk_id_node_name_idx NameData node_name; }; +enum Anum_chunk_data_node_node_name_idx +{ + Anum_chunk_data_node_name_idx_node_name = 1, + _Anum_chunk_data_node_node_name_idx_max, +}; + +struct FormData_chunk_data_node_node_name_idx +{ + NameData node_name; +}; + /************************************ * * Tablespace table definitions diff --git a/src/ts_catalog/chunk_data_node.c b/src/ts_catalog/chunk_data_node.c index b28c9637bd0..c0c4a7e3dd1 100644 --- a/src/ts_catalog/chunk_data_node.c +++ b/src/ts_catalog/chunk_data_node.c @@ -339,3 +339,16 @@ ts_chunk_data_nodes_scan_iterator_set_chunk_id(ScanIterator *it, int32 chunk_id) F_INT4EQ, Int32GetDatum(chunk_id)); } + +void +ts_chunk_data_nodes_scan_iterator_set_node_name(ScanIterator *it, const char *node_name) +{ + it->ctx.index = + catalog_get_index(ts_catalog_get(), CHUNK_DATA_NODE, CHUNK_DATA_NODE_NODE_NAME_IDX); + ts_scan_iterator_scan_key_reset(it); + ts_scan_iterator_scan_key_init(it, + Anum_chunk_data_node_name_idx_node_name, + BTEqualStrategyNumber, + F_NAMEEQ, + CStringGetDatum(node_name)); +} diff --git a/src/ts_catalog/chunk_data_node.h b/src/ts_catalog/chunk_data_node.h index 1aef275b99a..83a35c78583 100644 --- a/src/ts_catalog/chunk_data_node.h +++ b/src/ts_catalog/chunk_data_node.h @@ -32,7 +32,10 @@ extern int ts_chunk_data_node_delete_by_node_name(const char *node_name); extern TSDLLEXPORT List * ts_chunk_data_node_scan_by_node_name_and_hypertable_id(const char *node_name, int32 hypertable_id, MemoryContext mctx); -extern ScanIterator ts_chunk_data_nodes_scan_iterator_create(MemoryContext result_mcxt); -extern void ts_chunk_data_nodes_scan_iterator_set_chunk_id(ScanIterator *it, int32 chunk_id); +extern TSDLLEXPORT ScanIterator ts_chunk_data_nodes_scan_iterator_create(MemoryContext result_mcxt); +extern TSDLLEXPORT void ts_chunk_data_nodes_scan_iterator_set_chunk_id(ScanIterator *it, + int32 chunk_id); +extern TSDLLEXPORT void ts_chunk_data_nodes_scan_iterator_set_node_name(ScanIterator *it, + const char *node_name); #endif /* TIMESCALEDB_CHUNK_DATA_NODE_H */ diff --git a/tsl/src/chunk.c b/tsl/src/chunk.c index 783149c78eb..aa97c321d7d 100644 --- a/tsl/src/chunk.c +++ b/tsl/src/chunk.c @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -66,7 +67,7 @@ chunk_match_data_node_by_server(const Chunk *chunk, const ForeignServer *server) } static bool -chunk_set_foreign_server(Chunk *chunk, ForeignServer *new_server) +chunk_set_foreign_server(const Chunk *chunk, const ForeignServer *new_server) { Relation ftrel; HeapTuple tuple; @@ -134,32 +135,72 @@ chunk_set_foreign_server(Chunk *chunk, ForeignServer *new_server) return true; } -void -chunk_update_foreign_server_if_needed(int32 chunk_id, Oid existing_server_id) +static bool +data_node_is_available(const ForeignServer *server) +{ + ListCell *lc; + + foreach (lc, server->options) + { + DefElem *elem = lfirst(lc); + + if (strcmp(elem->defname, "available") == 0) + { + bool available = defGetBoolean(elem); + + return available; + } + } + + /* If no option is set, default to available=true */ + return true; +} + +bool +chunk_update_foreign_server_if_needed(const Chunk *chunk, Oid existing_server_id) { ListCell *lc; ChunkDataNode *new_server = NULL; - Chunk *chunk = ts_chunk_get_by_id(chunk_id, true); ForeignTable *foreign_table = NULL; + ForeignServer *server = NULL; + bool new_data_node_found = false; Assert(chunk->relkind == RELKIND_FOREIGN_TABLE); foreign_table = GetForeignTable(chunk->table_id); + /* Cannot switch to other data node if only one or none assigned */ + if (list_length(chunk->data_nodes) < 2) + return false; + /* no need to update since foreign table doesn't reference server we try to remove */ if (existing_server_id != foreign_table->serverid) - return; - - Assert(list_length(chunk->data_nodes) > 1); + return false; + /* Pick the first "available" data node referenced by the chunk, which is + * not the existing data node. */ foreach (lc, chunk->data_nodes) { new_server = lfirst(lc); + if (new_server->foreign_server_oid != existing_server_id) - break; + { + server = GetForeignServer(new_server->foreign_server_oid); + + if (data_node_is_available(server)) + { + new_data_node_found = true; + break; + } + } + } + + if (new_data_node_found) + { + Assert(server != NULL); + chunk_set_foreign_server(chunk, server); } - Assert(new_server != NULL); - chunk_set_foreign_server(chunk, GetForeignServer(new_server->foreign_server_oid)); + return new_data_node_found; } Datum diff --git a/tsl/src/chunk.h b/tsl/src/chunk.h index 0ede76be13e..cc41a46fedf 100644 --- a/tsl/src/chunk.h +++ b/tsl/src/chunk.h @@ -10,7 +10,7 @@ #include #include -extern void chunk_update_foreign_server_if_needed(int32 chunk_id, Oid existing_server_id); +extern bool chunk_update_foreign_server_if_needed(const Chunk *chunk, Oid existing_server_id); extern Datum chunk_set_default_data_node(PG_FUNCTION_ARGS); extern Datum chunk_drop_replica(PG_FUNCTION_ARGS); extern int chunk_invoke_drop_chunks(Oid relid, Datum older_than, Datum older_than_type); diff --git a/tsl/src/chunk_api.c b/tsl/src/chunk_api.c index 05bfc7138ef..d535475d498 100644 --- a/tsl/src/chunk_api.c +++ b/tsl/src/chunk_api.c @@ -1767,6 +1767,6 @@ chunk_api_call_chunk_drop_replica(const Chunk *chunk, const char *node_name, Oid * This chunk might have this data node as primary, change that association * if so. Then delete the chunk_id and node_name association. */ - chunk_update_foreign_server_if_needed(chunk->fd.id, serverid); + chunk_update_foreign_server_if_needed(chunk, serverid); ts_chunk_data_node_delete_by_chunk_id_and_node_name(chunk->fd.id, node_name); } diff --git a/tsl/src/data_node.c b/tsl/src/data_node.c index 790f61f54eb..2283313bcb8 100644 --- a/tsl/src/data_node.c +++ b/tsl/src/data_node.c @@ -4,6 +4,11 @@ * LICENSE-TIMESCALE for a copy of the license. */ #include "cache.h" +#include "scan_iterator.h" +#include "ts_catalog/catalog.h" +#include +#include +#include #include #include @@ -28,6 +33,7 @@ #include #include #include +#include #include #include "compat/compat.h" @@ -679,6 +685,16 @@ get_server_port() return pg_strtoint32(portstr); } +static void +validate_data_node_port(int port) +{ + if (port < 1 || port > PG_UINT16_MAX) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + (errmsg("invalid port number %d", port), + errhint("The port number must be between 1 and %u.", PG_UINT16_MAX)))); +} + /* set_distid may need to be false for some otherwise invalid configurations * that are useful for testing */ static Datum @@ -719,11 +735,7 @@ data_node_add_internal(PG_FUNCTION_ARGS, bool set_distid) (errcode(ERRCODE_INVALID_PARAMETER_VALUE), (errmsg("data node name cannot be NULL")))); - if (port < 1 || port > PG_UINT16_MAX) - ereport(ERROR, - (errcode(ERRCODE_INVALID_PARAMETER_VALUE), - (errmsg("invalid port number %d", port), - errhint("The port number must be between 1 and %u.", PG_UINT16_MAX)))); + validate_data_node_port(port); result = get_database_info(MyDatabaseId, &database); Assert(result); @@ -1135,8 +1147,8 @@ data_node_modify_hypertable_data_nodes(const char *node_name, List *hypertable_d foreach (cs_lc, chunk_data_nodes) { ChunkDataNode *cdn = lfirst(cs_lc); - - chunk_update_foreign_server_if_needed(cdn->fd.chunk_id, cdn->foreign_server_oid); + const Chunk *chunk = ts_chunk_get_by_id(cdn->fd.chunk_id, true); + chunk_update_foreign_server_if_needed(chunk, cdn->foreign_server_oid); ts_chunk_data_node_delete_by_chunk_id_and_node_name(cdn->fd.chunk_id, NameStr(cdn->fd.node_name)); } @@ -1416,6 +1428,205 @@ data_node_detach(PG_FUNCTION_ARGS) PG_RETURN_INT32(removed); } +enum Anum_show_conn +{ + Anum_alter_data_node_node_name = 1, + Anum_alter_data_node_host, + Anum_alter_data_node_database, + Anum_alter_data_node_port, + Anum_alter_data_node_available, + _Anum_alter_data_node_max, +}; + +#define Natts_alter_data_node (_Anum_alter_data_node_max - 1) + +static HeapTuple +create_alter_data_node_tuple(TupleDesc tupdesc, const char *node_name, List *options) +{ + Datum values[Natts_alter_data_node]; + bool nulls[Natts_alter_data_node] = { false }; + ListCell *lc; + + MemSet(nulls, false, sizeof(nulls)); + + values[AttrNumberGetAttrOffset(Anum_alter_data_node_node_name)] = CStringGetDatum(node_name); + /* Default to true for "available" if not set in options */ + values[AttrNumberGetAttrOffset(Anum_alter_data_node_available)] = BoolGetDatum(true); + + foreach (lc, options) + { + DefElem *elem = lfirst(lc); + + if (strcmp("host", elem->defname) == 0) + { + values[AttrNumberGetAttrOffset(Anum_alter_data_node_host)] = + CStringGetTextDatum(defGetString(elem)); + } + else if (strcmp("dbname", elem->defname) == 0) + { + values[AttrNumberGetAttrOffset(Anum_alter_data_node_database)] = + CStringGetDatum(defGetString(elem)); + } + else if (strcmp("port", elem->defname) == 0) + { + int port = atoi(defGetString(elem)); + values[AttrNumberGetAttrOffset(Anum_alter_data_node_port)] = Int32GetDatum(port); + } + else if (strcmp("available", elem->defname) == 0) + { + values[AttrNumberGetAttrOffset(Anum_alter_data_node_available)] = + BoolGetDatum(defGetBoolean(elem)); + } + } + + return heap_form_tuple(tupdesc, values, nulls); +} + +/* + * Switch default data node on chunks to avoid using the given data node. + */ +static void +switch_default_data_node_on_chunks(const ForeignServer *datanode) +{ + unsigned int failed_update_count = 0; + ScanIterator it = ts_chunk_data_nodes_scan_iterator_create(CurrentMemoryContext); + ts_chunk_data_nodes_scan_iterator_set_node_name(&it, datanode->servername); + + /* Scan for chunks that reference the given data node */ + ts_scanner_foreach(&it) + { + TupleTableSlot *slot = ts_scan_iterator_slot(&it); + bool PG_USED_FOR_ASSERTS_ONLY isnull = false; + Datum chunk_id = slot_getattr(slot, Anum_chunk_data_node_chunk_id, &isnull); + + Assert(!isnull); + + const Chunk *chunk = ts_chunk_get_by_id(DatumGetInt32(chunk_id), true); + + if (!chunk_update_foreign_server_if_needed(chunk, datanode->serverid)) + failed_update_count++; + } + + ts_scan_iterator_close(&it); + + if (failed_update_count > 0) + elog(WARNING, "could not switch default data node on %u chunks", failed_update_count); +} + +/* + * Alter a data node. + * + * Change the configuration of a data node, including host, port, and + * database. + * + * Can also be used to mark a data node "unavailable", which ensures it is no + * longer used for reads as long as there are replica chunks on other data + * nodes to use for reads instead. If it is not possible to fail over all + * chunks, a warning will be raised. + */ +Datum +data_node_alter(PG_FUNCTION_ARGS) +{ + const char *node_name = PG_ARGISNULL(0) ? NULL : NameStr(*PG_GETARG_NAME(0)); + const char *host = PG_ARGISNULL(1) ? NULL : TextDatumGetCString(PG_GETARG_TEXT_P(1)); + const char *database = PG_ARGISNULL(2) ? NULL : NameStr(*PG_GETARG_NAME(2)); + int port = PG_ARGISNULL(3) ? -1 : PG_GETARG_INT32(3); + bool available = PG_ARGISNULL(4) ? true : PG_GETARG_BOOL(4); + bool available_is_null = PG_ARGISNULL(4); + ForeignServer *server = NULL; + TupleDesc tupdesc; + AlterForeignServerStmt alter_server_stmt = { + .type = T_AlterForeignServerStmt, + .servername = node_name ? pstrdup(node_name) : NULL, + .has_version = false, + .version = NULL, + .options = NIL, + }; + + TS_PREVENT_FUNC_IF_READ_ONLY(); + + if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("function returning record called in context " + "that cannot accept type record"))); + + tupdesc = BlessTupleDesc(tupdesc); + + /* Check if a data node with the given name actually exists, or raise an error. */ + server = data_node_get_foreign_server(node_name, ACL_NO_CHECK, false, false /* missing_ok */); + + if (host == NULL && database == NULL && port == -1 && available_is_null) + PG_RETURN_DATUM( + HeapTupleGetDatum(create_alter_data_node_tuple(tupdesc, node_name, server->options))); + + if (host != NULL) + { + DefElem *elem = + makeDefElemExtended(NULL, "host", (Node *) makeString((char *) host), DEFELEM_SET, -1); + alter_server_stmt.options = lappend(alter_server_stmt.options, elem); + } + + if (database != NULL) + { + DefElem *elem = makeDefElemExtended(NULL, + "dbname", + (Node *) makeString((char *) database), + DEFELEM_SET, + -1); + alter_server_stmt.options = lappend(alter_server_stmt.options, elem); + } + + if (port != -1) + { + DefElem *elem; + + validate_data_node_port(port); + elem = makeDefElemExtended(NULL, "port", (Node *) makeInteger(port), DEFELEM_SET, -1); + alter_server_stmt.options = lappend(alter_server_stmt.options, elem); + } + + if (!available_is_null) + { + /* "available" is not a mandatory option so it might not exist + * previously. Therefore, need to figure out if the action is SET or + * ADD. */ + DefElem *elem; + ListCell *lc; + bool available_found = false; + + foreach (lc, server->options) + { + elem = lfirst(lc); + + if (strcmp(elem->defname, "available") == 0) + { + available_found = true; + break; + } + } + + elem = makeDefElemExtended(NULL, + "available", + (Node *) (available ? makeString("true") : makeString("false")), + available_found ? DEFELEM_SET : DEFELEM_ADD, + -1); + alter_server_stmt.options = lappend(alter_server_stmt.options, elem); + } + + if (!available_is_null && !available) + switch_default_data_node_on_chunks(server); + + AlterForeignServer(&alter_server_stmt); + + /* Add updated options last as they will take precedence over old options + * when creating the result tuple. */ + List *merged_options = list_concat(server->options, alter_server_stmt.options); + + PG_RETURN_DATUM( + HeapTupleGetDatum(create_alter_data_node_tuple(tupdesc, node_name, merged_options))); +} + /* * Drop a data node's database. * diff --git a/tsl/src/data_node.h b/tsl/src/data_node.h index e46ecebd6e2..4cfdc905462 100644 --- a/tsl/src/data_node.h +++ b/tsl/src/data_node.h @@ -28,6 +28,7 @@ extern Datum data_node_add(PG_FUNCTION_ARGS); extern Datum data_node_delete(PG_FUNCTION_ARGS); extern Datum data_node_attach(PG_FUNCTION_ARGS); extern Datum data_node_detach(PG_FUNCTION_ARGS); +extern Datum data_node_alter(PG_FUNCTION_ARGS); extern Datum data_node_block_new_chunks(PG_FUNCTION_ARGS); extern Datum data_node_allow_new_chunks(PG_FUNCTION_ARGS); extern List *data_node_get_node_name_list_with_aclcheck(AclMode mode, bool fail_on_aclcheck); diff --git a/tsl/src/fdw/option.c b/tsl/src/fdw/option.c index 303d8a3c1ed..690797baa23 100644 --- a/tsl/src/fdw/option.c +++ b/tsl/src/fdw/option.c @@ -154,6 +154,7 @@ init_ts_fdw_options(void) /* fetch_size is available on both foreign data wrapper and server */ { "fetch_size", ForeignDataWrapperRelationId }, { "fetch_size", ForeignServerRelationId }, + { "available", ForeignServerRelationId }, { NULL, InvalidOid } }; diff --git a/tsl/src/init.c b/tsl/src/init.c index 32314005d44..eb0536d70ff 100644 --- a/tsl/src/init.c +++ b/tsl/src/init.c @@ -189,6 +189,7 @@ CrossModuleFunctions tsl_cm_functions = { .data_node_attach = data_node_attach, .data_node_ping = data_node_ping, .data_node_detach = data_node_detach, + .data_node_alter = data_node_alter, .data_node_allow_new_chunks = data_node_allow_new_chunks, .data_node_block_new_chunks = data_node_block_new_chunks, .chunk_set_default_data_node = chunk_set_default_data_node, diff --git a/tsl/test/expected/data_node.out b/tsl/test/expected/data_node.out index f941793b98b..010f1994f0b 100644 --- a/tsl/test/expected/data_node.out +++ b/tsl/test/expected/data_node.out @@ -1579,3 +1579,216 @@ DROP DATABASE :DN_DBNAME_3; DROP DATABASE :DN_DBNAME_4; DROP DATABASE :DN_DBNAME_5; DROP DATABASE :DN_DBNAME_6; +----------------------------------------------- +-- Test alter_data_node() +----------------------------------------------- +SELECT node_name, database, node_created, database_created, extension_created FROM add_data_node('data_node_1', host => 'localhost', database => :'DN_DBNAME_1'); + node_name | database | node_created | database_created | extension_created +-------------+----------------+--------------+------------------+------------------- + data_node_1 | db_data_node_1 | t | t | t +(1 row) + +SELECT node_name, database, node_created, database_created, extension_created FROM add_data_node('data_node_2', host => 'localhost', database => :'DN_DBNAME_2'); + node_name | database | node_created | database_created | extension_created +-------------+----------------+--------------+------------------+------------------- + data_node_2 | db_data_node_2 | t | t | t +(1 row) + +SELECT node_name, database, node_created, database_created, extension_created FROM add_data_node('data_node_3', host => 'localhost', database => :'DN_DBNAME_3'); + node_name | database | node_created | database_created | extension_created +-------------+----------------+--------------+------------------+------------------- + data_node_3 | db_data_node_3 | t | t | t +(1 row) + +GRANT USAGE ON FOREIGN SERVER data_node_1, data_node_2, data_node_3 TO :ROLE_1; +SET ROLE :ROLE_1; +CREATE TABLE hyper1 (time timestamptz, location int, temp float); +CREATE TABLE hyper2 (LIKE hyper1); +CREATE TABLE hyper3 (LIKE hyper1); +SELECT create_distributed_hypertable('hyper1', 'time', 'location', replication_factor=>1); +NOTICE: adding not-null constraint to column "time" + create_distributed_hypertable +------------------------------- + (10,public,hyper1,t) +(1 row) + +SELECT create_distributed_hypertable('hyper2', 'time', 'location', replication_factor=>2); +NOTICE: adding not-null constraint to column "time" + create_distributed_hypertable +------------------------------- + (11,public,hyper2,t) +(1 row) + +SELECT create_distributed_hypertable('hyper3', 'time', 'location', replication_factor=>3); +NOTICE: adding not-null constraint to column "time" + create_distributed_hypertable +------------------------------- + (12,public,hyper3,t) +(1 row) + +INSERT INTO hyper1 +SELECT t, (abs(timestamp_hash(t::timestamp)) % 3) + 1, random() * 30 +FROM generate_series('2022-01-01 00:00:00'::timestamptz, '2022-01-05 00:00:00', '1 h') t; +INSERT INTO hyper2 SELECT * FROM hyper1; +INSERT INTO hyper3 SELECT * FROM hyper1; +-- create view to see the data nodes and default data node of all +-- chunks +CREATE TEMPORARY VIEW chunk_default_data_node AS +SELECT hypertable_name, format('%I.%I', ch.chunk_schema, ch.chunk_name)::regclass AS chunk, ch.data_nodes, fs.srvname default_data_node + FROM timescaledb_information.chunks ch + INNER JOIN pg_foreign_table ft ON (format('%I.%I', ch.chunk_schema, ch.chunk_name)::regclass = ft.ftrelid) + INNER JOIN pg_foreign_server fs ON (ft.ftserver = fs.oid); +SELECT * FROM chunk_default_data_node; + hypertable_name | chunk | data_nodes | default_data_node +-----------------+-----------------------------------------------+---------------------------------------+------------------- + hyper1 | _timescaledb_internal._dist_hyper_10_12_chunk | {data_node_1} | data_node_1 + hyper1 | _timescaledb_internal._dist_hyper_10_13_chunk | {data_node_2} | data_node_2 + hyper1 | _timescaledb_internal._dist_hyper_10_14_chunk | {data_node_3} | data_node_3 + hyper2 | _timescaledb_internal._dist_hyper_11_15_chunk | {data_node_1,data_node_2} | data_node_1 + hyper2 | _timescaledb_internal._dist_hyper_11_16_chunk | {data_node_2,data_node_3} | data_node_2 + hyper2 | _timescaledb_internal._dist_hyper_11_17_chunk | {data_node_1,data_node_3} | data_node_3 + hyper3 | _timescaledb_internal._dist_hyper_12_18_chunk | {data_node_1,data_node_2,data_node_3} | data_node_1 + hyper3 | _timescaledb_internal._dist_hyper_12_19_chunk | {data_node_1,data_node_2,data_node_3} | data_node_2 + hyper3 | _timescaledb_internal._dist_hyper_12_20_chunk | {data_node_1,data_node_2,data_node_3} | data_node_3 +(9 rows) + +-- test "switching over" to other data node when +\set ON_ERROR_STOP 0 +-- must be owner to alter a data node +SELECT alter_data_node('data_node_1', available=>false); +WARNING: could not switch default data node on 4 chunks +ERROR: must be owner of foreign server data_node_1 +SELECT alter_data_node('data_node_1', port=>8989); +ERROR: must be owner of foreign server data_node_1 +\set ON_ERROR_STOP 1 +RESET ROLE; +--when altering a data node, related entries in the connection cache +--should be invalidated +SELECT node_name, host, port, invalidated +FROM _timescaledb_internal.show_connection_cache() +WHERE node_name = 'data_node_1'; + node_name | host | port | invalidated +-------------+-----------+-------+------------- + data_node_1 | localhost | 55432 | f +(1 row) + +SELECT * FROM alter_data_node('data_node_1', available=>false); +WARNING: could not switch default data node on 4 chunks + node_name | host | database | port | available +-------------+-----------+----------------+-------+----------- + data_node_1 | localhost | db_data_node_1 | 55432 | f +(1 row) + +SELECT node_name, host, port, invalidated +FROM _timescaledb_internal.show_connection_cache() +WHERE node_name = 'data_node_1'; + node_name | host | port | invalidated +-------------+-----------+-------+------------- + data_node_1 | localhost | 55432 | t +(1 row) + +-- the node that is not available for reads should no longer be +-- default data node for chunks, except for those that have no +-- alternative (i.e., the chunk only has one data node). +SELECT * FROM chunk_default_data_node; + hypertable_name | chunk | data_nodes | default_data_node +-----------------+-----------------------------------------------+---------------------------------------+------------------- + hyper1 | _timescaledb_internal._dist_hyper_10_12_chunk | {data_node_1} | data_node_1 + hyper1 | _timescaledb_internal._dist_hyper_10_13_chunk | {data_node_2} | data_node_2 + hyper1 | _timescaledb_internal._dist_hyper_10_14_chunk | {data_node_3} | data_node_3 + hyper2 | _timescaledb_internal._dist_hyper_11_15_chunk | {data_node_1,data_node_2} | data_node_2 + hyper2 | _timescaledb_internal._dist_hyper_11_16_chunk | {data_node_2,data_node_3} | data_node_2 + hyper2 | _timescaledb_internal._dist_hyper_11_17_chunk | {data_node_1,data_node_3} | data_node_3 + hyper3 | _timescaledb_internal._dist_hyper_12_18_chunk | {data_node_1,data_node_2,data_node_3} | data_node_2 + hyper3 | _timescaledb_internal._dist_hyper_12_19_chunk | {data_node_1,data_node_2,data_node_3} | data_node_2 + hyper3 | _timescaledb_internal._dist_hyper_12_20_chunk | {data_node_1,data_node_2,data_node_3} | data_node_3 +(9 rows) + +-- save old port so that we can restore connectivity after +-- we alter the data node +WITH options AS ( + SELECT unnest(options) option + FROM timescaledb_information.data_nodes + WHERE node_name = 'data_node_1' +) +SELECT split_part(option, '=', 2) AS old_port +FROM options WHERE option LIKE 'port%' \gset +-- also test altering host, port and database +SELECT node_name, options FROM timescaledb_information.data_nodes; + node_name | options +-------------+------------------------------------------------------------------- + data_node_2 | {host=localhost,port=55432,dbname=db_data_node_2} + data_node_3 | {host=localhost,port=55432,dbname=db_data_node_3} + data_node_1 | {host=localhost,port=55432,dbname=db_data_node_1,available=false} +(3 rows) + +SELECT * FROM alter_data_node('data_node_1', available=>true, host=>'foo.bar', port=>8989, database=>'new_db'); + node_name | host | database | port | available +-------------+---------+----------+------+----------- + data_node_1 | foo.bar | new_db | 8989 | t +(1 row) + +SELECT node_name, options FROM timescaledb_information.data_nodes; + node_name | options +-------------+------------------------------------------------------- + data_node_2 | {host=localhost,port=55432,dbname=db_data_node_2} + data_node_3 | {host=localhost,port=55432,dbname=db_data_node_3} + data_node_1 | {host=foo.bar,port=8989,dbname=new_db,available=true} +(3 rows) + +-- just show current options: +SELECT * FROM alter_data_node('data_node_1'); + node_name | host | database | port | available +-------------+---------+----------+------+----------- + data_node_1 | foo.bar | new_db | 8989 | t +(1 row) + +\set ON_ERROR_STOP 0 +SELECT * FROM alter_data_node(NULL); +ERROR: data node name cannot be NULL +SELECT * FROM alter_data_node('does_not_exist'); +ERROR: server "does_not_exist" does not exist +SELECT * FROM alter_data_node('data_node_1', port=>89000); +ERROR: invalid port number 89000 +-- cannot delete data node with "drop_database" since configuration is wrong +SELECT delete_data_node('data_node_1', drop_database=>true); +ERROR: could not connect to data node "data_node_1" +\set ON_ERROR_STOP 0 +-- restore configuration for data_node_1 +SELECT * FROM alter_data_node('data_node_1', host=>'localhost', port=>:old_port, database=>:'DN_DBNAME_1'); + node_name | host | database | port | available +-------------+-----------+----------------+-------+----------- + data_node_1 | localhost | db_data_node_1 | 55432 | t +(1 row) + +SELECT node_name, options FROM timescaledb_information.data_nodes; + node_name | options +-------------+------------------------------------------------------------------ + data_node_1 | {host=localhost,port=55432,dbname=db_data_node_1,available=true} + data_node_2 | {host=localhost,port=55432,dbname=db_data_node_2} + data_node_3 | {host=localhost,port=55432,dbname=db_data_node_3} +(3 rows) + +DROP TABLE hyper1; +DROP TABLE hyper2; +DROP TABLE hyper3; +-- create new session to clear out connection cache +\c :TEST_DBNAME :ROLE_CLUSTER_SUPERUSER; +SELECT delete_data_node('data_node_1', drop_database=>true); + delete_data_node +------------------ + t +(1 row) + +SELECT delete_data_node('data_node_2', drop_database=>true); + delete_data_node +------------------ + t +(1 row) + +SELECT delete_data_node('data_node_3', drop_database=>true); + delete_data_node +------------------ + t +(1 row) + diff --git a/tsl/test/shared/expected/extension.out b/tsl/test/shared/expected/extension.out index ec71bfe18ee..b793fac01c0 100644 --- a/tsl/test/shared/expected/extension.out +++ b/tsl/test/shared/expected/extension.out @@ -154,6 +154,7 @@ ORDER BY pronamespace::regnamespace::text COLLATE "C", p.oid::regprocedure::text add_job(regproc,interval,jsonb,timestamp with time zone,boolean,regproc) add_reorder_policy(regclass,name,boolean) add_retention_policy(regclass,"any",boolean,interval) + alter_data_node(name,text,name,integer,boolean) alter_job(integer,interval,interval,integer,interval,boolean,jsonb,timestamp with time zone,boolean,regproc) approximate_row_count(regclass) attach_data_node(name,regclass,boolean,boolean) diff --git a/tsl/test/sql/data_node.sql b/tsl/test/sql/data_node.sql index 97a9703c970..ba07f03cd2f 100644 --- a/tsl/test/sql/data_node.sql +++ b/tsl/test/sql/data_node.sql @@ -767,3 +767,101 @@ DROP DATABASE :DN_DBNAME_3; DROP DATABASE :DN_DBNAME_4; DROP DATABASE :DN_DBNAME_5; DROP DATABASE :DN_DBNAME_6; + + +----------------------------------------------- +-- Test alter_data_node() +----------------------------------------------- +SELECT node_name, database, node_created, database_created, extension_created FROM add_data_node('data_node_1', host => 'localhost', database => :'DN_DBNAME_1'); +SELECT node_name, database, node_created, database_created, extension_created FROM add_data_node('data_node_2', host => 'localhost', database => :'DN_DBNAME_2'); +SELECT node_name, database, node_created, database_created, extension_created FROM add_data_node('data_node_3', host => 'localhost', database => :'DN_DBNAME_3'); + +GRANT USAGE ON FOREIGN SERVER data_node_1, data_node_2, data_node_3 TO :ROLE_1; +SET ROLE :ROLE_1; + +CREATE TABLE hyper1 (time timestamptz, location int, temp float); +CREATE TABLE hyper2 (LIKE hyper1); +CREATE TABLE hyper3 (LIKE hyper1); + +SELECT create_distributed_hypertable('hyper1', 'time', 'location', replication_factor=>1); +SELECT create_distributed_hypertable('hyper2', 'time', 'location', replication_factor=>2); +SELECT create_distributed_hypertable('hyper3', 'time', 'location', replication_factor=>3); + +INSERT INTO hyper1 +SELECT t, (abs(timestamp_hash(t::timestamp)) % 3) + 1, random() * 30 +FROM generate_series('2022-01-01 00:00:00'::timestamptz, '2022-01-05 00:00:00', '1 h') t; + +INSERT INTO hyper2 SELECT * FROM hyper1; +INSERT INTO hyper3 SELECT * FROM hyper1; + +-- create view to see the data nodes and default data node of all +-- chunks +CREATE TEMPORARY VIEW chunk_default_data_node AS +SELECT hypertable_name, format('%I.%I', ch.chunk_schema, ch.chunk_name)::regclass AS chunk, ch.data_nodes, fs.srvname default_data_node + FROM timescaledb_information.chunks ch + INNER JOIN pg_foreign_table ft ON (format('%I.%I', ch.chunk_schema, ch.chunk_name)::regclass = ft.ftrelid) + INNER JOIN pg_foreign_server fs ON (ft.ftserver = fs.oid); + +SELECT * FROM chunk_default_data_node; + +-- test "switching over" to other data node when +\set ON_ERROR_STOP 0 +-- must be owner to alter a data node +SELECT alter_data_node('data_node_1', available=>false); +SELECT alter_data_node('data_node_1', port=>8989); +\set ON_ERROR_STOP 1 + +RESET ROLE; +--when altering a data node, related entries in the connection cache +--should be invalidated +SELECT node_name, host, port, invalidated +FROM _timescaledb_internal.show_connection_cache() +WHERE node_name = 'data_node_1'; +SELECT * FROM alter_data_node('data_node_1', available=>false); +SELECT node_name, host, port, invalidated +FROM _timescaledb_internal.show_connection_cache() +WHERE node_name = 'data_node_1'; + +-- the node that is not available for reads should no longer be +-- default data node for chunks, except for those that have no +-- alternative (i.e., the chunk only has one data node). +SELECT * FROM chunk_default_data_node; + +-- save old port so that we can restore connectivity after +-- we alter the data node +WITH options AS ( + SELECT unnest(options) option + FROM timescaledb_information.data_nodes + WHERE node_name = 'data_node_1' +) +SELECT split_part(option, '=', 2) AS old_port +FROM options WHERE option LIKE 'port%' \gset + +-- also test altering host, port and database +SELECT node_name, options FROM timescaledb_information.data_nodes; +SELECT * FROM alter_data_node('data_node_1', available=>true, host=>'foo.bar', port=>8989, database=>'new_db'); + +SELECT node_name, options FROM timescaledb_information.data_nodes; +-- just show current options: +SELECT * FROM alter_data_node('data_node_1'); + +\set ON_ERROR_STOP 0 +SELECT * FROM alter_data_node(NULL); +SELECT * FROM alter_data_node('does_not_exist'); +SELECT * FROM alter_data_node('data_node_1', port=>89000); +-- cannot delete data node with "drop_database" since configuration is wrong +SELECT delete_data_node('data_node_1', drop_database=>true); +\set ON_ERROR_STOP 0 + +-- restore configuration for data_node_1 +SELECT * FROM alter_data_node('data_node_1', host=>'localhost', port=>:old_port, database=>:'DN_DBNAME_1'); +SELECT node_name, options FROM timescaledb_information.data_nodes; + +DROP TABLE hyper1; +DROP TABLE hyper2; +DROP TABLE hyper3; +-- create new session to clear out connection cache +\c :TEST_DBNAME :ROLE_CLUSTER_SUPERUSER; +SELECT delete_data_node('data_node_1', drop_database=>true); +SELECT delete_data_node('data_node_2', drop_database=>true); +SELECT delete_data_node('data_node_3', drop_database=>true);