diff --git a/api/api-doc/raft.json b/api/api-doc/raft.json new file mode 100644 index 0000000000..9673682069 --- /dev/null +++ b/api/api-doc/raft.json @@ -0,0 +1,43 @@ +{ + "apiVersion":"0.0.1", + "swaggerVersion":"1.2", + "basePath":"{{Protocol}}://{{Host}}", + "resourcePath":"/raft", + "produces":[ + "application/json" + ], + "apis":[ + { + "path":"/raft/trigger_snapshot/{group_id}", + "operations":[ + { + "method":"POST", + "summary":"Triggers snapshot creation and log truncation for the given Raft group", + "type":"string", + "nickname":"trigger_snapshot", + "produces":[ + "application/json" + ], + "parameters":[ + { + "name":"group_id", + "description":"The ID of the group which should get snapshotted", + "required":true, + "allowMultiple":false, + "type":"string", + "paramType":"path" + }, + { + "name":"timeout", + "description":"Timeout in seconds after which the endpoint returns a failure. If not provided, 60s is used.", + "required":false, + "allowMultiple":false, + "type":"long", + "paramType":"query" + } + ] + } + ] + } + ] +} diff --git a/api/api.cc b/api/api.cc index f323da431c..92b10a69fe 100644 --- a/api/api.cc +++ b/api/api.cc @@ -33,6 +33,7 @@ #include "task_manager.hh" #include "task_manager_test.hh" #include "tasks.hh" +#include "raft.hh" logging::logger apilog("api"); @@ -326,6 +327,18 @@ future<> unset_server_tasks_compaction_module(http_context& ctx) { return ctx.http_server.set_routes([&ctx] (routes& r) { unset_tasks_compaction_module(ctx, r); }); } +future<> set_server_raft(http_context& ctx, sharded& raft_gr) { + auto rb = std::make_shared(ctx.api_doc); + return ctx.http_server.set_routes([rb, &ctx, &raft_gr] (routes& r) { + rb->register_function(r, "raft", "The Raft API"); + set_raft(ctx, r, raft_gr); + }); +} + +future<> unset_server_raft(http_context& ctx) { + return ctx.http_server.set_routes([&ctx] (routes& r) { unset_raft(ctx, r); }); +} + void req_params::process(const request& req) { // Process mandatory parameters for (auto& [name, ent] : params) { diff --git a/api/api_init.hh b/api/api_init.hh index 54b15b3ed0..90f40b497a 100644 --- a/api/api_init.hh +++ b/api/api_init.hh @@ -24,6 +24,7 @@ class load_meter; class storage_proxy; class storage_service; class raft_group0_client; +class raft_group_registry; } // namespace service @@ -129,5 +130,7 @@ future<> set_server_task_manager_test(http_context& ctx, sharded unset_server_task_manager_test(http_context& ctx); future<> set_server_tasks_compaction_module(http_context& ctx, sharded& ss, sharded& snap_ctl); future<> unset_server_tasks_compaction_module(http_context& ctx); +future<> set_server_raft(http_context&, sharded&); +future<> unset_server_raft(http_context&); } diff --git a/api/raft.cc b/api/raft.cc new file mode 100644 index 0000000000..eab13889dd --- /dev/null +++ b/api/raft.cc @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2024-present ScyllaDB + */ + +/* + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +#include + +#include "api/api.hh" +#include "api/api-doc/raft.json.hh" + +#include "service/raft/raft_group_registry.hh" + +using namespace seastar::httpd; + +extern logging::logger apilog; + +namespace api { + +namespace r = httpd::raft_json; +using namespace json; + +void set_raft(http_context&, httpd::routes& r, sharded& raft_gr) { + r::trigger_snapshot.set(r, [&raft_gr] (std::unique_ptr req) -> future { + raft::group_id gid{utils::UUID{req->param["group_id"]}}; + auto timeout_dur = std::invoke([timeout_str = req->get_query_param("timeout")] { + if (timeout_str.empty()) { + return std::chrono::seconds{60}; + } + auto dur = std::stoll(timeout_str); + if (dur <= 0) { + throw std::runtime_error{"Timeout must be a positive number."}; + } + return std::chrono::seconds{dur}; + }); + + std::atomic found_srv{false}; + co_await raft_gr.invoke_on_all([gid, timeout_dur, &found_srv] (service::raft_group_registry& raft_gr) -> future<> { + auto* srv = raft_gr.find_server(gid); + if (!srv) { + co_return; + } + + found_srv = true; + abort_on_expiry aoe(lowres_clock::now() + timeout_dur); + apilog.info("Triggering Raft group {} snapshot", gid); + auto result = co_await srv->trigger_snapshot(&aoe.abort_source()); + if (result) { + apilog.info("New snapshot for Raft group {} created", gid); + } else { + apilog.info("Could not create new snapshot for Raft group {}, no new entries applied", gid); + } + }); + + if (!found_srv) { + throw std::runtime_error{fmt::format("Server for group ID {} not found", gid)}; + } + + co_return json_void{}; + }); +} + +void unset_raft(http_context&, httpd::routes& r) { + r::trigger_snapshot.unset(r); +} + +} + diff --git a/api/raft.hh b/api/raft.hh new file mode 100644 index 0000000000..e514a1bc22 --- /dev/null +++ b/api/raft.hh @@ -0,0 +1,18 @@ +/* + * Copyright (C) 2023-present ScyllaDB + */ + +/* + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +#pragma once + +#include "api_init.hh" + +namespace api { + +void set_raft(http_context& ctx, httpd::routes& r, sharded& raft_gr); +void unset_raft(http_context& ctx, httpd::routes& r); + +} diff --git a/configure.py b/configure.py index 3162b61854..33a4872d89 100755 --- a/configure.py +++ b/configure.py @@ -1240,6 +1240,8 @@ api = ['api/api.cc', Json2Code('api/api-doc/error_injection.json'), 'api/authorization_cache.cc', Json2Code('api/api-doc/authorization_cache.json'), + 'api/raft.cc', + Json2Code('api/api-doc/raft.json'), ] alternator = [ diff --git a/main.cc b/main.cc index 9bc1afe6c3..df101a129e 100644 --- a/main.cc +++ b/main.cc @@ -1442,6 +1442,11 @@ To start the scylla server proper, simply invoke as: scylla server (or just scyl supervisor::notify("starting Raft Group Registry service"); raft_gr.invoke_on_all(&service::raft_group_registry::start).get(); + api::set_server_raft(ctx, raft_gr).get(); + auto stop_raft_api = defer_verbose_shutdown("Raft API", [&ctx] { + api::unset_server_raft(ctx).get(); + }); + group0_client.init().get(); // schema migration, if needed, is also done on shard 0