From 713ea636f37b14e08f9a80eb9ac418270f547347 Mon Sep 17 00:00:00 2001 From: StekPerepolnen Date: Fri, 31 May 2024 16:14:16 +0000 Subject: [PATCH] vdisk evict --- ydb/core/viewer/json_handlers_vdisk.cpp | 3 +- ydb/core/viewer/json_pipe_req.h | 15 ++ ydb/core/viewer/json_vdisk_evict.h | 290 ++++++++++++++++++++++++ ydb/core/viewer/ya.make | 1 + 4 files changed, 308 insertions(+), 1 deletion(-) create mode 100644 ydb/core/viewer/json_vdisk_evict.h diff --git a/ydb/core/viewer/json_handlers_vdisk.cpp b/ydb/core/viewer/json_handlers_vdisk.cpp index 1e243d0331a1..49c869692f08 100644 --- a/ydb/core/viewer/json_handlers_vdisk.cpp +++ b/ydb/core/viewer/json_handlers_vdisk.cpp @@ -6,7 +6,7 @@ #include "json_vdiskstat.h" #include "json_getblob.h" #include "json_blobindexstat.h" - +#include "json_vdisk_evict.h" namespace NKikimr::NViewer { @@ -14,6 +14,7 @@ void InitVDiskJsonHandlers(TJsonHandlers& jsonHandlers) { jsonHandlers.AddHandler("/vdisk/vdiskstat", new TJsonHandler); jsonHandlers.AddHandler("/vdisk/getblob", new TJsonHandler); jsonHandlers.AddHandler("/vdisk/blobindexstat", new TJsonHandler); + jsonHandlers.AddHandler("/vdisk/evict", new TJsonHandler); } } diff --git a/ydb/core/viewer/json_pipe_req.h b/ydb/core/viewer/json_pipe_req.h index 3c2bebc21159..9fb8a949446c 100644 --- a/ydb/core/viewer/json_pipe_req.h +++ b/ydb/core/viewer/json_pipe_req.h @@ -174,6 +174,21 @@ class TViewerPipeClient : public TActorBootstrapped { SendRequestToPipe(pipeClient, request.Release()); } + void RequestBSControllerVDiskEvict(ui32 groupId, ui32 groupGeneration, ui32 failRealmIdx, ui32 failDomainIdx, ui32 vdiskIdx, bool force = false) { + TActorId pipeClient = ConnectTabletPipe(GetBSControllerId()); + THolder request = MakeHolder(); + auto* evictVDisk = request->Record.MutableRequest()->AddCommand()->MutableReassignGroupDisk(); + evictVDisk->SetGroupId(groupId); + evictVDisk->SetGroupGeneration(groupGeneration); + evictVDisk->SetFailRealmIdx(failRealmIdx); + evictVDisk->SetFailDomainIdx(failDomainIdx); + evictVDisk->SetVDiskIdx(vdiskIdx); + if (force) { + request->Record.MutableRequest()->SetIgnoreDegradedGroupsChecks(true); + } + SendRequestToPipe(pipeClient, request.Release()); + } + void RequestSchemeCacheNavigate(const TString& path) { THolder request = MakeHolder(); NSchemeCache::TSchemeCacheNavigate::TEntry entry; diff --git a/ydb/core/viewer/json_vdisk_evict.h b/ydb/core/viewer/json_vdisk_evict.h new file mode 100644 index 000000000000..812d57d13c01 --- /dev/null +++ b/ydb/core/viewer/json_vdisk_evict.h @@ -0,0 +1,290 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include "viewer.h" +#include "json_pipe_req.h" + +namespace NKikimr { +namespace NViewer { + +using namespace NActors; + +class TJsonVDiskEvict : public TViewerPipeClient { + enum EEv { + EvRetryNodeRequest = EventSpaceBegin(NActors::TEvents::ES_PRIVATE), + EvEnd + }; + + static_assert(EvEnd < EventSpaceEnd(NActors::TEvents::ES_PRIVATE), "expect EvEnd < EventSpaceEnd(TEvents::ES_PRIVATE)"); + + struct TEvRetryNodeRequest : NActors::TEventLocal { + TEvRetryNodeRequest() + {} + }; + +protected: + using TThis = TJsonVDiskEvict; + using TBase = TViewerPipeClient; + IViewer* Viewer; + NMon::TEvHttpInfo::TPtr Event; + TJsonSettings JsonSettings; + ui32 Timeout = 0; + ui32 ActualRetries = 0; + ui32 Retries = 0; + TDuration RetryPeriod = TDuration::MilliSeconds(500); + + std::unique_ptr Response; + + ui32 GroupId = 0; + ui32 GroupGeneration = 0; + ui32 FailRealmIdx = 0; + ui32 FailDomainIdx = 0; + ui32 VdiskIdx = 0; + bool Force = false; + +public: + static constexpr NKikimrServices::TActivity::EType ActorActivityType() { + return NKikimrServices::TActivity::VIEWER_HANDLER; + } + + TJsonVDiskEvict(IViewer* viewer, NMon::TEvHttpInfo::TPtr& ev) + : Viewer(viewer) + , Event(ev) + {} + + inline ui32 GetRequiredParam(const TCgiParameters& params, const std::string& name, ui32& obj) { + if (!TryFromString(params.Get(name), obj)) { + TBase::Send(Event->Sender, new NMon::TEvHttpInfoRes( + Viewer->GetHTTPBADREQUEST(Event->Get(), "text/plain", TStringBuilder() << "field '" << name << "' or 'vdisk_id' are required"), + 0, NMon::IEvHttpInfoRes::EContentType::Custom)); + return false; + } + return true; + } + + void Bootstrap() { + const auto& params(Event->Get()->Request.GetParams()); + TString vdisk_id = params.Get("vdisk_id"); + if (vdisk_id) { + TVector parts = StringSplitter(vdisk_id).Split('-').SkipEmpty(); + if (parts.size() == 5) { + GroupId = FromStringWithDefault(parts[0], Max()); + GroupGeneration = FromStringWithDefault(parts[1], Max()); + FailRealmIdx = FromStringWithDefault(parts[2], Max()); + FailDomainIdx = FromStringWithDefault(parts[3], Max()); + VdiskIdx = FromStringWithDefault(parts[4], Max()); + } + if (parts.size() != 5 || GroupId == Max() + || GroupGeneration == Max() || FailRealmIdx == Max() + || FailDomainIdx == Max() || VdiskIdx == Max()) { + TBase::Send(Event->Sender, new NMon::TEvHttpInfoRes( + Viewer->GetHTTPBADREQUEST(Event->Get(), "text/plain", TStringBuilder() << "Unable to parse the 'vdisk_id' parameter"), + 0, NMon::IEvHttpInfoRes::EContentType::Custom)); + return PassAway(); + } + } else if (!GetRequiredParam(params, "group_id", GroupId) + || !GetRequiredParam(params, "group_generation_id", GroupGeneration) + || !GetRequiredParam(params, "fail_realm_idx", FailRealmIdx) + || !GetRequiredParam(params, "fail_domain_idx", FailDomainIdx) + || !GetRequiredParam(params, "vdisk_idx", VdiskIdx)) { + return PassAway(); + } + + if (Event->Get()->Request.GetMethod() != HTTP_METHOD_POST) { + TBase::Send(Event->Sender, new NMon::TEvHttpInfoRes( + Viewer->GetHTTPBADREQUEST(Event->Get(), "text/plain", "Only POST method is allowed"), + 0, NMon::IEvHttpInfoRes::EContentType::Custom)); + return PassAway(); + } + TBase::InitConfig(params); + + Force = FromStringWithDefault(params.Get("force"), false); + Timeout = FromStringWithDefault(params.Get("timeout"), 10000); + Retries = FromStringWithDefault(params.Get("retries"), 0); + RetryPeriod = TDuration::MilliSeconds(FromStringWithDefault(params.Get("retry_period"), RetryPeriod.MilliSeconds())); + + SendRequest(); + + TBase::Become(&TThis::StateWork, TDuration::MilliSeconds(Timeout), new TEvents::TEvWakeup()); + } + + STATEFN(StateWork) { + switch (ev->GetTypeRewrite()) { + hFunc(TEvBlobStorage::TEvControllerConfigResponse, Handle); + cFunc(TEvRetryNodeRequest::EventType, HandleRetry); + cFunc(TEvents::TEvUndelivered::EventType, Undelivered); + cFunc(TEvents::TSystem::Wakeup, HandleTimeout); + } + } + + void SendRequest() { + RequestBSControllerVDiskEvict(GroupId, GroupGeneration, FailRealmIdx, FailDomainIdx, VdiskIdx, Force); + } + + bool RetryRequest() { + if (Retries) { + if (++ActualRetries <= Retries) { + TBase::Schedule(RetryPeriod, new TEvRetryNodeRequest()); + return true; + } + } + return false; + } + + void Undelivered() { + if (!RetryRequest()) { + TBase::RequestDone(); + } + } + + void Handle(TEvBlobStorage::TEvControllerConfigResponse::TPtr& ev) { + Response.reset(ev->Release().Release()); + ReplyAndPassAway(); + } + + void HandleRetry() { + SendRequest(); + } + + void HandleTimeout() { + Send(Event->Sender, new NMon::TEvHttpInfoRes( + Viewer->GetHTTPGATEWAYTIMEOUT(Event->Get(), "text/plain", "Timeout receiving response from NodeWarden"), + 0, NMon::IEvHttpInfoRes::EContentType::Custom)); + } + + void PassAway() override { + TBase::PassAway(); + } + + void TryToTranslateFromBSC2Human(const NKikimrBlobStorage::TConfigResponse& response, TString& bscError, bool& forceRetryPossible) { + if (response.GroupsGetDisintegratedByExpectedStatusSize()) { + bscError = TStringBuilder() << "Calling this operation will cause at least groups [" << JoinStrings(response.GetGroupsGetDisintegratedByExpectedStatus().begin(), response.GetGroupsGetDisintegratedByExpectedStatus().end(), ", ") << "] to go into a dead state"; + } else if (response.GroupsGetDisintegratedSize()) { + bscError = TStringBuilder() << "Calling this operation will cause at least groups [" << JoinStrings(response.GetGroupsGetDisintegrated().begin(), response.GetGroupsGetDisintegrated().end(), ", ") << "] to go into a dead state"; + } else if (response.GroupsGetDegradedSize()) { + bscError = TStringBuilder() << "Calling this operation will cause at least groups [" << JoinStrings(response.GetGroupsGetDegraded().begin(), response.GetGroupsGetDegraded().end(), ", ") << "] to go into a degraded state"; + forceRetryPossible = true; + } else if (response.StatusSize()) { + const auto& lastStatus = response.GetStatus(response.StatusSize() - 1); + TVector groups; + for (auto& failParam: lastStatus.GetFailParam()) { + if (failParam.HasGroupId()) { + groups.emplace_back(failParam.GetGroupId()); + } + } + if (lastStatus.GetFailReason() == NKikimrBlobStorage::TConfigResponse::TStatus::kMayGetDegraded) { + bscError = TStringBuilder() << "Calling this operation will cause at least groups [" << JoinVectorIntoString(groups, ", ") << "] to go into a degraded state"; + forceRetryPossible = true; + } else if (lastStatus.GetFailReason() == NKikimrBlobStorage::TConfigResponse::TStatus::kMayLoseData) { + bscError = TStringBuilder() << "Calling this operation may result in data loss for at least groups [" << JoinVectorIntoString(groups, ", ") << "]"; + } + } + } + + void ReplyAndPassAway() { + NJson::TJsonValue json; + if (Response != nullptr) { + if (Response->Record.GetResponse().GetSuccess()) { + json["result"] = true; + } else { + json["result"] = false; + TString error = Response->Record.GetResponse().GetErrorDescription(); + bool forceRetryPossible = false; + TryToTranslateFromBSC2Human(Response->Record.GetResponse(), error, forceRetryPossible); + json["error"] = error; + if (forceRetryPossible) { + json["forceRetryPossible"] = true; + } + } + json["debugMessage"] = Response->Record.ShortDebugString(); + } else { + json["result"] = false; + json["error"] = "No response was received from BSC"; + } + TBase::Send(Event->Sender, new NMon::TEvHttpInfoRes(Viewer->GetHTTPOKJSON(Event->Get(), NJson::WriteJson(json)), 0, NMon::IEvHttpInfoRes::EContentType::Custom)); + PassAway(); + } +}; + +template <> +YAML::Node TJsonRequestSwagger::GetSwagger() { + return YAML::Load(R"___( + post: + tags: + - vdisk + summary: VDisk evict + description: VDisk evict + parameters: + - name: vdisk_id + in: query + description: vdisk identifier + required: false + type: string + - name: group_id + in: query + description: group identifier + required: false + type: integer + - name: group_generation_id + in: query + description: group generation identifier + required: false + type: integer + - name: fail_realm_idx + in: query + description: fail realm identifier + required: false + type: integer + - name: fail_domain_ids + in: query + description: fail domain identifier + required: false + type: integer + - name: vdisk_idx + in: query + description: vdisk idx identifier + required: false + type: integer + - name: timeout + in: query + description: timeout in ms + required: false + type: integer + - name: force + in: query + description: attempt forced operation, ignore warnings + required: false + type: boolean + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + properties: + result: + type: boolean + description: was operation successful or not + error: + type: string + description: details about failed operation + forceRetryPossible: + type: boolean + description: if true, operation can be retried with force flag + 400: + description: Bad Request + 403: + description: Forbidden + 504: + description: Gateway Timeout + )___"); +} + +} +} diff --git a/ydb/core/viewer/ya.make b/ydb/core/viewer/ya.make index 80986b38b2ad..8bb4246391fc 100644 --- a/ydb/core/viewer/ya.make +++ b/ydb/core/viewer/ya.make @@ -48,6 +48,7 @@ SRCS( json_topicinfo.h json_pqconsumerinfo.h json_vdisk_req.h + json_vdisk_evict.h json_vdiskinfo.h json_vdiskstat.h json_wb_req.h