From d09196068cc2b8eeb0b36d32455925ef0fac4d1d Mon Sep 17 00:00:00 2001 From: Nikos Dragazis Date: Tue, 3 Mar 2026 18:37:30 +0200 Subject: [PATCH] api: Add REST endpoint for migration finalization The endpoint is the following: POST /storage_service/vnode_tablet_migrations/keyspaces/{keyspace}/finalization When called, it issues a `finalize_migration` topology request and waits for its completion. Signed-off-by: Nikos Dragazis --- api/api-doc/storage_service.json | 20 +++++++++ api/storage_service.cc | 16 +++++++ service/storage_service.cc | 72 ++++++++++++++++++++++++++++++++ service/storage_service.hh | 1 + test/pylib/rest_client.py | 4 ++ 5 files changed, 113 insertions(+) diff --git a/api/api-doc/storage_service.json b/api/api-doc/storage_service.json index 45cf2011f2..a14f28ad14 100644 --- a/api/api-doc/storage_service.json +++ b/api/api-doc/storage_service.json @@ -3206,6 +3206,26 @@ ] }] }, + { + "path":"/storage_service/vnode_tablet_migrations/keyspaces/{keyspace}/finalization", + "operations":[{ + "method":"POST", + "summary":"Finalize vnodes-to-tablets migration for all tables in a keyspace", + "type":"void", + "nickname":"finalize_vnode_tablet_migration", + "produces":["application/json"], + "parameters":[ + { + "name":"keyspace", + "description":"Keyspace name", + "required":true, + "allowMultiple":false, + "type":"string", + "paramType":"path" + } + ] + }] + }, { "path":"/storage_service/quiesce_topology", "operations":[ diff --git a/api/storage_service.cc b/api/storage_service.cc index 37f6d33436..f8d286e8b4 100644 --- a/api/storage_service.cc +++ b/api/storage_service.cc @@ -1753,6 +1753,20 @@ rest_set_vnode_tablet_migration_node_storage_mode(http_context& ctx, sharded +rest_finalize_vnode_tablet_migration(http_context& ctx, sharded& ss, std::unique_ptr req) { + if (!ss.local().get_feature_service().vnodes_to_tablets_migrations) { + apilog.warn("finalize_vnode_tablet_migration: called before the cluster feature was enabled"); + throw std::runtime_error("vnodes-to-tablets migration requires all nodes to support the VNODES_TO_TABLETS_MIGRATIONS cluster feature"); + } + auto keyspace = validate_keyspace(ctx, req); + validate_keyspace(ctx, keyspace); + + co_await ss.local().finalize_tablets_migration(keyspace); + co_return json_void(); +} + static future rest_quiesce_topology(sharded& ss, std::unique_ptr req) { @@ -1905,6 +1919,7 @@ void set_storage_service(http_context& ctx, routes& r, sharded storage_service::set_node_intended_storage_mode(intended_storage_mode m slogger.info("Successfully set intended storage mode for node {} to {}", raft_server.id(), mode); } +future<> storage_service::finalize_tablets_migration(const sstring& ks_name) { + if (this_shard_id() != 0) { + co_return co_await container().invoke_on(0, [&ks_name] (auto& ss) { + return ss.finalize_tablets_migration(ks_name); + }); + } + + slogger.info("Finalizing vnodes-to-tablets migration for keyspace '{}'", ks_name); + + utils::UUID request_id; + + while (true) { + auto guard = co_await _group0->client().start_operation(_group0_as, raft_timeout{}); + + auto& db = _db.local(); + auto& ks = db.find_keyspace(ks_name); + + if (ks.uses_tablets()) { + throw std::runtime_error(fmt::format("Keyspace '{}' already uses tablets", ks_name)); + } + + const auto& tm = get_token_metadata(); + const auto& tablet_metadata = tm.tablets(); + + auto tables = ks.metadata()->tables(); + if (tables.empty()) { + throw std::runtime_error(fmt::format("Keyspace '{}' has no tables", ks_name)); + } + + for (const auto& schema : tables) { + if (!tablet_metadata.has_tablet_map(schema->id())) { + throw std::runtime_error(fmt::format("Table {}.{} does not have a tablet map; " + "all tables in keyspace '{}' must be prepared for migration before finalizing", + ks_name, schema->cf_name(), ks_name)); + } + } + + slogger.info("All {} table(s) in keyspace '{}' have tablet maps, submitting finalization request", + tables.size(), ks_name); + + request_id = guard.new_group0_state_id(); + + topology_mutation_builder builder(guard.write_timestamp()); + builder.queue_global_topology_request_id(request_id); + + topology_request_tracking_mutation_builder rtbuilder(request_id, _feature_service.topology_requests_type_column); + rtbuilder.set("done", false) + .set("start_time", db_clock::now()) + .set("request_type", global_topology_request::finalize_migration) + .set_finalize_migration_data(ks_name); + + topology_change change{{builder.build(), rtbuilder.build()}}; + group0_command g0_cmd = _group0->client().prepare_command(std::move(change), guard, + fmt::format("finalize vnodes-to-tablets migration for keyspace '{}'", ks_name)); + + try { + co_await _group0->client().add_entry(std::move(g0_cmd), std::move(guard), _group0_as, raft_timeout{}); + } catch (group0_concurrent_modification&) { + slogger.info("finalize_tablets_migration: concurrent modification, retrying"); + continue; + } + break; + } + + auto error = co_await wait_for_topology_request_completion(request_id); + if (!error.empty()) { + throw std::runtime_error(fmt::format("Migration finalization failed for keyspace '{}': {}", ks_name, error)); + } + + slogger.info("Successfully finalized vnodes-to-tablets migration for keyspace '{}'", ks_name); +} + future<> storage_service::process_tablet_split_candidate(table_id table) noexcept { tasks::task_info tablet_split_task_info; diff --git a/service/storage_service.hh b/service/storage_service.hh index 789334cf6d..a63413c586 100644 --- a/service/storage_service.hh +++ b/service/storage_service.hh @@ -289,6 +289,7 @@ public: // persists them to group0. future<> prepare_for_tablets_migration(const sstring& ks_name); future<> set_node_intended_storage_mode(intended_storage_mode mode); + future<> finalize_tablets_migration(const sstring& ks_name); void start_tablet_split_monitor(); private: diff --git a/test/pylib/rest_client.py b/test/pylib/rest_client.py index 2e75d5d2b9..023d738f9e 100644 --- a/test/pylib/rest_client.py +++ b/test/pylib/rest_client.py @@ -336,6 +336,10 @@ class ScyllaRESTAPIClient: """Set the node's intended storage mode to vnodes""" await self.client.put_json(f"/storage_service/vnode_tablet_migrations/node/storage_mode?intended_mode=vnodes", host=node_ip) + async def finalize_vnode_tablet_migration(self, node_ip: str, ks: str) -> None: + """Finalize vnodes-to-tablets migration for all tables in a keyspace""" + await self.client.post(f"/storage_service/vnode_tablet_migrations/keyspaces/{ks}/finalization", host=node_ip) + async def keyspace_upgrade_sstables(self, node_ip: str, ks: str) -> None: await self.client.get(f"/storage_service/keyspace_upgrade_sstables/{ks}", host=node_ip)