Browse Source

Bootstrapper: don't restart healthy tablets (#9659)

Aleksei Borzenkov 5 months ago
parent
commit
6bec40a8b1

+ 8 - 4
ydb/core/base/statestorage.cpp

@@ -164,13 +164,14 @@ void TStateStorageInfo::TSelection::MergeReply(EStatus status, EStatus *owner, u
     ui32 unknown = 0;
     ui32 ok = 0;
     ui32 outdated = 0;
+    ui32 unavailable = 0;
 
     const ui32 majority = Sz / 2 + 1;
 
     ui32 cookie = 0;
     for (ui32 i = 0; i < Sz; ++i) {
         EStatus &st = Status[i];
-        if (resetOld && st != StatusUnknown)
+        if (resetOld && st != StatusUnknown && st != StatusUnavailable)
             st = StatusOutdated;
 
         if (cookie == targetCookie)
@@ -190,16 +191,19 @@ void TStateStorageInfo::TSelection::MergeReply(EStatus status, EStatus *owner, u
         case StatusOutdated:
             ++outdated;
             break;
+        case StatusUnavailable:
+            ++unavailable;
+            break;
         }
     }
 
     if (owner) {
         if (ok >= majority) {
             *owner = StatusOk;
-        } else if (outdated >= majority) {
-            *owner = StatusOutdated;
         } else if (ok + unknown < majority) {
-            if (outdated)
+            if (unavailable > (Sz - majority))
+                *owner = StatusUnavailable;
+            else if (outdated)
                 *owner = StatusOutdated;
             else
                 *owner = StatusNoInfo;

+ 1 - 0
ydb/core/base/statestorage.h

@@ -470,6 +470,7 @@ struct TStateStorageInfo : public TThrRefBase {
             StatusOk,
             StatusNoInfo,
             StatusOutdated,
+            StatusUnavailable,
         };
 
         ui32 Sz;

+ 22 - 7
ydb/core/base/statestorage_proxy.cpp

@@ -177,7 +177,7 @@ class TStateStorageProxyRequest : public TActor<TStateStorageProxyRequest> {
             Signature[cookie] = Max<ui64>();
             ++RepliesMerged;
 
-            ReplicaSelection->MergeReply(TStateStorageInfo::TSelection::StatusNoInfo, &ReplyStatus, cookie, false);
+            ReplicaSelection->MergeReply(TStateStorageInfo::TSelection::StatusUnavailable, &ReplyStatus, cookie, false);
         }
     }
 
@@ -192,7 +192,8 @@ class TStateStorageProxyRequest : public TActor<TStateStorageProxyRequest> {
         ++RepliesMerged;
         ++SignaturesMerged;
 
-        if (status == NKikimrProto::OK) {
+        switch (status) {
+        case NKikimrProto::OK: {
             const ui32 gen = record.GetCurrentGeneration();
             const ui32 step = record.GetCurrentStep();
             const TActorId leader = ActorIdFromProto(record.GetCurrentLeader());
@@ -221,9 +222,14 @@ class TStateStorageProxyRequest : public TActor<TStateStorageProxyRequest> {
 
                 ReplicaSelection->MergeReply(TStateStorageInfo::TSelection::StatusOk, &ReplyStatus, cookie, reset);
             }
-        } else if (status == NKikimrProto::ERROR) {
+            break;
+        }
+        // NOTE: replicas currently reply with ERROR when there is no data for the tablet
+        case NKikimrProto::ERROR:
+        case NKikimrProto::NODATA:
             ReplicaSelection->MergeReply(TStateStorageInfo::TSelection::StatusNoInfo, &ReplyStatus, cookie, false);
-        } else {
+            break;
+        default:
             Y_ABORT();
         }
 
@@ -307,11 +313,14 @@ class TStateStorageProxyRequest : public TActor<TStateStorageProxyRequest> {
             ReplyAndDie(NKikimrProto::OK);
             return;
         case TStateStorageInfo::TSelection::StatusNoInfo:
-            ReplyAndDie(NKikimrProto::ERROR);
+            ReplyAndDie(NKikimrProto::NODATA);
             return;
         case TStateStorageInfo::TSelection::StatusOutdated:
             ReplyAndDie(NKikimrProto::RACE);
             return;
+        case TStateStorageInfo::TSelection::StatusUnavailable:
+            ReplyAndDie(NKikimrProto::ERROR);
+            return;
         }
         Y_DEBUG_ABORT_UNLESS(false);
         PassAway();
@@ -332,12 +341,15 @@ class TStateStorageProxyRequest : public TActor<TStateStorageProxyRequest> {
                 return;
             case TStateStorageInfo::TSelection::StatusNoInfo:
                 if (RepliesMerged == Replicas) { // for negative response always waits for full reply set to avoid herding of good replicas by fast retry cycle
-                    ReplyAndSig(NKikimrProto::ERROR);
+                    ReplyAndSig(NKikimrProto::NODATA);
                 }
                 return;
             case TStateStorageInfo::TSelection::StatusOutdated:
                 ReplyAndSig(NKikimrProto::RACE);
                 return;
+            case TStateStorageInfo::TSelection::StatusUnavailable:
+                ReplyAndSig(NKikimrProto::ERROR);
+                return;
             }
         }
     }
@@ -379,6 +391,8 @@ class TStateStorageProxyRequest : public TActor<TStateStorageProxyRequest> {
             }
             return;
         case TStateStorageInfo::TSelection::StatusNoInfo:
+        case TStateStorageInfo::TSelection::StatusUnavailable:
+            // Note: StatusNoInfo shouldn't really happen for update queries
             ReplyAndDie(NKikimrProto::ERROR);
             return;
         case TStateStorageInfo::TSelection::StatusOutdated:
@@ -404,7 +418,8 @@ class TStateStorageProxyRequest : public TActor<TStateStorageProxyRequest> {
             }
             return;
             case TStateStorageInfo::TSelection::StatusNoInfo:
-                // should not happens for update queries
+            case TStateStorageInfo::TSelection::StatusUnavailable:
+                // Note: StatusNoInfo shouldn't really happen for update queries
                 ReplyAndSig(NKikimrProto::ERROR);
                 return;
             case TStateStorageInfo::TSelection::StatusOutdated:

+ 1 - 0
ydb/core/base/statestorage_replica.cpp

@@ -104,6 +104,7 @@ class TStateStorageReplica : public TActorBootstrapped<TStateStorageReplica> {
                 }
             }
         } else {
+            // FIXME: change to NODATA in a future version
             msg.Reset(new TEvStateStorage::TEvReplicaInfo(tabletId, NKikimrProto::ERROR));
         }
         msg->Record.SetCookie(cookie);

File diff suppressed because it is too large
+ 595 - 360
ydb/core/tablet/bootstrapper.cpp


+ 2 - 2
ydb/core/tablet/bootstrapper.h

@@ -38,7 +38,7 @@ struct TBootstrapperInfo : public TThrRefBase {
     TDuration OfflineDelay;
     bool StartFollowers;
 
-    TBootstrapperInfo(TTabletSetupInfo *setupInfo)
+    TBootstrapperInfo(TTabletSetupInfo* setupInfo)
         : SetupInfo(setupInfo)
         , WatchThreshold(TDuration::MilliSeconds(200))
         , OfflineDelay(TDuration::Seconds(3))
@@ -46,7 +46,7 @@ struct TBootstrapperInfo : public TThrRefBase {
     {}
 };
 
-IActor* CreateBootstrapper(TTabletStorageInfo *tabletInfo, TBootstrapperInfo *bootstrapperInfo, bool standby = false);
+IActor* CreateBootstrapper(TTabletStorageInfo* tabletInfo, TBootstrapperInfo* bootstrapperInfo, bool standby = false);
 TActorId MakeBootstrapperID(ui64 tablet, ui32 node);
 
 }

+ 44 - 0
ydb/core/tablet/bootstrapper_impl.h

@@ -0,0 +1,44 @@
+#pragma once
+#include "bootstrapper.h"
+#include <ydb/core/protos/bootstrapper.pb.h>
+
+namespace NKikimr {
+
+struct TEvBootstrapper::TEvWatch : public TEventPB<TEvWatch, NKikimrBootstrapper::TEvWatch, EvWatch> {
+    TEvWatch()
+    {}
+
+    TEvWatch(ui64 tabletId, ui64 selfSeed, ui64 round)
+    {
+        Record.SetTabletID(tabletId);
+        Record.SetSelfSeed(selfSeed);
+        Record.SetRound(round);
+    }
+};
+
+struct TEvBootstrapper::TEvWatchResult : public TEventPB<TEvWatchResult, NKikimrBootstrapper::TEvWatchResult, EvWatchResult> {
+    TEvWatchResult()
+    {}
+
+    TEvWatchResult(ui64 tabletId, NKikimrBootstrapper::TEvWatchResult::EState state, ui64 seed, ui64 round)
+    {
+        Record.SetTabletID(tabletId);
+        Record.SetState(state);
+        Record.SetSeed(seed);
+        Record.SetRound(round);
+    }
+};
+
+struct TEvBootstrapper::TEvNotify : public TEventPB<TEvNotify, NKikimrBootstrapper::TEvNotify, EvNotify> {
+    TEvNotify()
+    {}
+
+    TEvNotify(ui64 tabletId, NKikimrBootstrapper::TEvNotify::EOp op, ui64 round)
+    {
+        Record.SetTabletID(tabletId);
+        Record.SetOp(op);
+        Record.SetRound(round);
+    }
+};
+
+} // namespace NKikimr

+ 428 - 0
ydb/core/tablet/bootstrapper_ut.cpp

@@ -0,0 +1,428 @@
+#include "bootstrapper.h"
+#include "bootstrapper_impl.h"
+#include <ydb/core/testlib/tablet_helpers.h>
+#include <ydb/core/testlib/actors/block_events.h>
+#include <ydb/core/base/tablet_pipe.h>
+#include <ydb/core/base/statestorage_impl.h>
+#include <ydb/core/tablet_flat/tablet_flat_executed.h>
+#include <ydb/library/actors/core/hfunc.h>
+#include <library/cpp/testing/unittest/registar.h>
+
+namespace NKikimr {
+
+Y_UNIT_TEST_SUITE(BootstrapperTest) {
+
+    class TSimpleTablet
+        : public TActor<TSimpleTablet>
+        , public NTabletFlatExecutor::TTabletExecutedFlat
+    {
+    public:
+        TSimpleTablet(const TActorId &tablet, TTabletStorageInfo *info)
+            : TActor(&TThis::StateInit)
+            , TTabletExecutedFlat(info, tablet, nullptr)
+        {}
+
+    private:
+        void DefaultSignalTabletActive(const TActorContext&) override {
+            // must be empty
+        }
+
+        void OnActivateExecutor(const TActorContext& ctx) override {
+            Become(&TThis::StateWork);
+            SignalTabletActive(ctx);
+        }
+
+        void OnDetach(const TActorContext &ctx) override {
+            return Die(ctx);
+        }
+
+        void OnTabletDead(TEvTablet::TEvTabletDead::TPtr &, const TActorContext &ctx) override {
+            return Die(ctx);
+        }
+
+        void Handle(TEvents::TEvPing::TPtr& ev) {
+            Send(ev->Sender, new TEvents::TEvPong);
+        }
+
+        STFUNC(StateInit) {
+            StateInitImpl(ev, SelfId());
+        }
+
+        STFUNC(StateWork) {
+            switch (ev->GetTypeRewrite()) {
+                hFunc(TEvents::TEvPing, Handle);
+            default:
+                HandleDefaultEvents(ev, SelfId());
+            }
+        }
+    };
+
+    TIntrusivePtr<TTabletStorageInfo> CreateSimpleTabletStorageInfo(ui64 tabletId = TTestTxConfig::TxTablet0) {
+        return CreateTestTabletInfo(tabletId, TTabletTypes::Dummy);
+    }
+
+    TIntrusivePtr<TTabletSetupInfo> CreateSimpleTabletSetupInfo() {
+        return MakeIntrusive<TTabletSetupInfo>(
+            [](const TActorId& tablet, TTabletStorageInfo* info) {
+                return new TSimpleTablet(tablet, info);
+            },
+            TMailboxType::Simple,
+            ui32(0),
+            TMailboxType::Simple,
+            ui32(0));
+    }
+
+    TActorId StartSimpleTablet(
+            TTestActorRuntime& runtime,
+            const TActorId& launcher = {},
+            ui32 nodeIdx = 0,
+            ui64 tabletId = TTestTxConfig::TxTablet0)
+    {
+        auto tabletInfo = CreateSimpleTabletStorageInfo(tabletId);
+        auto setupInfo = CreateSimpleTabletSetupInfo();
+        auto actor = runtime.Register(
+            CreateTablet(launcher, tabletInfo.Get(), setupInfo.Get(), /* generation */ 0),
+            nodeIdx);
+        runtime.EnableScheduleForActor(actor);
+        return actor;
+    }
+
+    std::vector<TActorId> StartSimpleTabletBootstrappers(
+            TTestActorRuntime& runtime,
+            const std::vector<ui32>& nodeIdxs,
+            ui64 tabletId = TTestTxConfig::TxTablet0)
+    {
+        std::vector<TActorId> boots;
+        auto tabletInfo = CreateSimpleTabletStorageInfo(tabletId);
+        auto setupInfo = CreateSimpleTabletSetupInfo();
+        for (ui32 nodeIdx : nodeIdxs) {
+            auto bootInfo = MakeIntrusive<TBootstrapperInfo>(setupInfo.Get());
+            for (ui32 otherNodeIdx : nodeIdxs) {
+                if (otherNodeIdx != nodeIdx) {
+                    bootInfo->OtherNodes.push_back(runtime.GetNodeId(otherNodeIdx));
+                }
+            }
+            boots.push_back(runtime.Register(CreateBootstrapper(tabletInfo.Get(), bootInfo.Get()), nodeIdx));
+            runtime.EnableScheduleForActor(boots.back());
+            // Make this bootstrapper discoverable by tablet id / node id
+            runtime.RegisterService(
+                MakeBootstrapperID(tabletId, runtime.GetNodeId(nodeIdx)),
+                boots.back(),
+                nodeIdx);
+        }
+        return boots;
+    }
+
+    TActorId StartSimpleTabletBootstrapper(
+            TTestActorRuntime& runtime,
+            ui32 nodeIdx = 0,
+            ui64 tabletId = TTestTxConfig::TxTablet0)
+    {
+        return StartSimpleTabletBootstrappers(runtime, {nodeIdx}, tabletId).at(0);
+    }
+
+    Y_UNIT_TEST(LoneBootstrapper) {
+        TTestBasicRuntime runtime;
+        SetupTabletServices(runtime);
+        auto sender = runtime.AllocateEdgeActor();
+
+        StartSimpleTabletBootstrapper(runtime);
+
+        ui32 gen1;
+        auto client = runtime.ConnectToPipe(TTestTxConfig::TxTablet0, sender, 0, NTabletPipe::TClientRetryPolicy::WithRetries());
+        {
+            Cerr << "... waiting for pipe to connect" << Endl;
+            auto ev = runtime.GrabEdgeEventRethrow<TEvTabletPipe::TEvClientConnected>(sender);
+            UNIT_ASSERT_VALUES_EQUAL(ev->Get()->Status, NKikimrProto::OK);
+            gen1 = ev->Get()->Generation;
+        }
+        Cerr << "... stopping current instance" << Endl;
+        runtime.SendToPipe(client, sender, new TEvents::TEvPoison);
+        {
+            Cerr << "... waiting for pipe to disconnect" << Endl;
+            auto ev = runtime.GrabEdgeEventRethrow<TEvTabletPipe::TEvClientDestroyed>(sender);
+        }
+
+        ui32 gen2;
+        client = runtime.ConnectToPipe(TTestTxConfig::TxTablet0, sender, 0, NTabletPipe::TClientRetryPolicy::WithRetries());
+        {
+            Cerr << "... waiting for pipe to connect" << Endl;
+            auto ev = runtime.GrabEdgeEventRethrow<TEvTabletPipe::TEvClientConnected>(sender);
+            UNIT_ASSERT_VALUES_EQUAL(ev->Get()->Status, NKikimrProto::OK);
+            gen2 = ev->Get()->Generation;
+        }
+
+        UNIT_ASSERT_C(gen1 < gen2, "Unexpected gen1# " << gen1 << " not before gen2# " << gen2);
+    }
+
+    Y_UNIT_TEST(KeepExistingTablet) {
+        TTestBasicRuntime runtime(2);
+        SetupTabletServices(runtime);
+
+        auto launcher = runtime.AllocateEdgeActor(1);
+        auto instance = StartSimpleTablet(runtime, launcher, 1);
+
+        bool dead = false;
+        auto deadObserver = runtime.AddObserver<TEvTablet::TEvTabletDead>(
+            [&](TEvTablet::TEvTabletDead::TPtr& ev) {
+                if (ev->Sender == instance) {
+                    dead = true;
+                }
+            });
+
+        auto sender = runtime.AllocateEdgeActor();
+
+        ui32 gen1;
+        auto client = runtime.ConnectToPipe(TTestTxConfig::TxTablet0, sender, 0, NTabletPipe::TClientRetryPolicy::WithRetries());
+        {
+            Cerr << "... waiting for pipe to connect" << Endl;
+            auto ev = runtime.GrabEdgeEventRethrow<TEvTabletPipe::TEvClientConnected>(sender);
+            UNIT_ASSERT_VALUES_EQUAL(ev->Get()->Status, NKikimrProto::OK);
+            gen1 = ev->Get()->Generation;
+        }
+
+        StartSimpleTabletBootstrapper(runtime);
+
+        Cerr << "... sleeping (original instance should be preserved)" << Endl;
+        runtime.SimulateSleep(TDuration::Seconds(1));
+        UNIT_ASSERT_C(!dead, "The original instance should be preserved");
+
+        runtime.Send(new IEventHandle(instance, launcher, new TEvents::TEvPoison), 1);
+        runtime.WaitFor("original instance to stop", [&]{ return dead; });
+
+        ui32 gen2;
+        client = runtime.ConnectToPipe(TTestTxConfig::TxTablet0, sender, 0, NTabletPipe::TClientRetryPolicy::WithRetries());
+        {
+            Cerr << "... waiting for pipe to connect" << Endl;
+            auto ev = runtime.GrabEdgeEventRethrow<TEvTabletPipe::TEvClientConnected>(sender);
+            UNIT_ASSERT_VALUES_EQUAL(ev->Get()->Status, NKikimrProto::OK);
+            gen2 = ev->Get()->Generation;
+        }
+
+        UNIT_ASSERT_C(gen1 < gen2, "Unexpected gen1# " << gen1 << " not before gen2# " << gen2);
+    }
+
+    Y_UNIT_TEST(RestartUnavailableTablet) {
+        TTestBasicRuntime runtime(3);
+        SetupTabletServices(runtime);
+
+        auto launcher = runtime.AllocateEdgeActor(1);
+        auto instance = StartSimpleTablet(runtime, launcher, 1);
+        Y_UNUSED(instance);
+
+        auto sender = runtime.AllocateEdgeActor();
+
+        ui32 gen1;
+        auto client = runtime.ConnectToPipe(TTestTxConfig::TxTablet0, sender, 0, NTabletPipe::TClientRetryPolicy::WithRetries());
+        {
+            Cerr << "... waiting for pipe to connect" << Endl;
+            auto ev = runtime.GrabEdgeEventRethrow<TEvTabletPipe::TEvClientConnected>(sender);
+            UNIT_ASSERT_VALUES_EQUAL(ev->Get()->Status, NKikimrProto::OK);
+            UNIT_ASSERT_VALUES_EQUAL(ev->Get()->ServerId.NodeId(), runtime.GetNodeId(1));
+            gen1 = ev->Get()->Generation;
+        }
+
+        TBlockEvents<TEvTabletPipe::TEvConnect> blockedConnect(runtime,
+            [&](const auto& ev) {
+                return ev->Get()->Record.GetTabletId() == TTestTxConfig::TxTablet0;
+            });
+
+        StartSimpleTabletBootstrapper(runtime, 2);
+
+        runtime.WaitFor("blocked connect attempt", [&]{ return blockedConnect.size() >= 1; });
+        blockedConnect.Stop();
+
+        Cerr << "... disconnecting nodes 2 <-> 1" << Endl;
+        runtime.DisconnectNodes(2, 1);
+
+        {
+            Cerr << "... waiting for pipe to disconnect" << Endl;
+            auto ev = runtime.GrabEdgeEventRethrow<TEvTabletPipe::TEvClientDestroyed>(sender);
+        }
+
+        ui32 gen2;
+        client = runtime.ConnectToPipe(TTestTxConfig::TxTablet0, sender, 0, NTabletPipe::TClientRetryPolicy::WithRetries());
+        {
+            Cerr << "... waiting for pipe to connect" << Endl;
+            auto ev = runtime.GrabEdgeEventRethrow<TEvTabletPipe::TEvClientConnected>(sender);
+            UNIT_ASSERT_VALUES_EQUAL(ev->Get()->Status, NKikimrProto::OK);
+            UNIT_ASSERT_VALUES_EQUAL(ev->Get()->ServerId.NodeId(), runtime.GetNodeId(2));
+            gen2 = ev->Get()->Generation;
+        }
+
+        UNIT_ASSERT_C(gen1 < gen2, "Unexpected gen1# " << gen1 << " not before gen2# " << gen2);
+    }
+
+    Y_UNIT_TEST(UnavailableStateStorage) {
+        TTestBasicRuntime runtime(3);
+        SetupTabletServices(runtime);
+        runtime.SetLogPriority(NKikimrServices::BOOTSTRAPPER, NActors::NLog::PRI_DEBUG);
+
+        auto launcher = runtime.AllocateEdgeActor(1);
+        auto instance = StartSimpleTablet(runtime, launcher, 1);
+
+        bool dead = false;
+        auto deadObserver = runtime.AddObserver<TEvTablet::TEvTabletDead>(
+            [&](TEvTablet::TEvTabletDead::TPtr& ev) {
+                if (ev->Sender == instance) {
+                    dead = true;
+                }
+            });
+
+        auto sender = runtime.AllocateEdgeActor();
+
+        ui32 gen1;
+        auto client = runtime.ConnectToPipe(TTestTxConfig::TxTablet0, sender, 0, NTabletPipe::TClientRetryPolicy::WithRetries());
+        {
+            Cerr << "... waiting for pipe to connect" << Endl;
+            auto ev = runtime.GrabEdgeEventRethrow<TEvTabletPipe::TEvClientConnected>(sender);
+            UNIT_ASSERT_VALUES_EQUAL(ev->Get()->Status, NKikimrProto::OK);
+            UNIT_ASSERT_VALUES_EQUAL(ev->Get()->ServerId.NodeId(), runtime.GetNodeId(1));
+            gen1 = ev->Get()->Generation;
+        }
+
+        ui32 node2 = runtime.GetNodeId(2);
+        TBlockEvents<TEvStateStorage::TEvReplicaLookup> blockedReplicaLookup(runtime,
+            [&](const auto& ev) {
+                // Block EvReplicaLookup requests from node 2 and disconnect every time
+                if (ev->Sender.NodeId() == node2 && ev->Get()->Record.GetTabletID() == TTestTxConfig::TxTablet0) {
+                    Cerr << "... disconnecting nodes 2 <-> 0 (" << ev->Get()->ToString() << " for " << ev->GetRecipientRewrite() << ")" << Endl;
+                    runtime.DisconnectNodes(2, 0);
+                    return true;
+                }
+                return false;
+            });
+
+        StartSimpleTabletBootstrapper(runtime, 2);
+        runtime.WaitFor("multiple state storage lookup attempts", [&]{ return blockedReplicaLookup.size() >= 6; });
+
+        UNIT_ASSERT_C(!dead, "The original instance should be preserved");
+
+        Y_UNUSED(client);
+        Y_UNUSED(gen1);
+    }
+
+    Y_UNIT_TEST(MultipleBootstrappers) {
+        TTestBasicRuntime runtime(4);
+        SetupTabletServices(runtime);
+        runtime.SetLogPriority(NKikimrServices::BOOTSTRAPPER, NActors::NLog::PRI_DEBUG);
+
+        StartSimpleTabletBootstrappers(runtime, {1, 2, 3});
+
+        Cerr << "... sleeping for 2 seconds" << Endl;
+        runtime.SimulateSleep(TDuration::Seconds(2));
+
+        auto sender = runtime.AllocateEdgeActor();
+
+        ui32 gen1;
+        ui32 initialNode;
+        ui32 initialNodeIdx;
+        auto client = runtime.ConnectToPipe(TTestTxConfig::TxTablet0, sender, 0, NTabletPipe::TClientRetryPolicy::WithRetries());
+        {
+            Cerr << "... waiting for pipe to connect" << Endl;
+            auto ev = runtime.GrabEdgeEventRethrow<TEvTabletPipe::TEvClientConnected>(sender);
+            UNIT_ASSERT_VALUES_EQUAL(ev->Get()->Status, NKikimrProto::OK);
+            gen1 = ev->Get()->Generation;
+            initialNode = ev->Get()->ServerId.NodeId();
+            initialNodeIdx = initialNode - runtime.GetNodeId(0);
+        }
+
+        Cerr << "... tablet initially started on node " << initialNode
+            << " (idx " << initialNodeIdx << ")" << " in gen " << gen1 << Endl;
+        UNIT_ASSERT_VALUES_EQUAL_C(gen1, 2u, "Expected tablet to start from gen 2");
+
+        std::vector<ui32> otherNodeIdxs;
+        for (ui32 nodeIdx : {1, 2, 3}) {
+            if (nodeIdx != initialNodeIdx) {
+                otherNodeIdxs.push_back(nodeIdx);
+            }
+        }
+
+        Cerr << "... disconnecting other nodes" << Endl;
+        for (ui32 nodeIdx : otherNodeIdxs) {
+            runtime.DisconnectNodes(initialNodeIdx, nodeIdx);
+        }
+
+        Cerr << "... sleeping for 2 seconds (tablet expected to survive)" << Endl;
+        runtime.SimulateSleep(TDuration::Seconds(2));
+
+        {
+            runtime.SendToPipe(client, sender, new TEvents::TEvPing);
+            runtime.GrabEdgeEventRethrow<TEvents::TEvPong>(sender);
+        }
+
+        // Block new connect attempts to tablet at that node
+        TBlockEvents<TEvTabletPipe::TEvConnect> blockConnect(runtime,
+            [&](const auto& ev) {
+                if (ev->GetRecipientRewrite().NodeId() == initialNode &&
+                    ev->Get()->Record.GetTabletId() == TTestTxConfig::TxTablet0)
+                {
+                    ui32 otherNodeIdx = ev->Sender.NodeId() - runtime.GetNodeId(0);
+                    Cerr << "... disconnecting nodes " << initialNodeIdx << " <-> " << otherNodeIdx
+                        << " (tablet connect attempt)" << Endl;
+                    runtime.DisconnectNodes(initialNodeIdx, otherNodeIdx);
+                    return true;
+                }
+                return false;
+            });
+
+        Cerr << "... disconnecting other nodes (new tablet connections fail)" << Endl;
+        for (ui32 nodeIdx : otherNodeIdxs) {
+            runtime.DisconnectNodes(initialNodeIdx, nodeIdx);
+        }
+
+        Cerr << "... sleeping for 2 seconds (tablet expected to survive)" << Endl;
+        runtime.SimulateSleep(TDuration::Seconds(2));
+
+        {
+            runtime.SendToPipe(client, sender, new TEvents::TEvPing);
+            runtime.GrabEdgeEventRethrow<TEvents::TEvPong>(sender);
+        }
+
+        // Block watch messages towards tablet node, a new owner must be selected
+        TBlockEvents<TEvBootstrapper::TEvWatch> blockWatch(runtime,
+            [&](const auto& ev) {
+                if (ev->GetRecipientRewrite().NodeId() == initialNode &&
+                    ev->Get()->Record.GetTabletID() == TTestTxConfig::TxTablet0)
+                {
+                    ui32 otherNodeIdx = ev->Sender.NodeId() - runtime.GetNodeId(0);
+                    Cerr << "... disconnecting nodes " << initialNodeIdx << " <-> " << otherNodeIdx
+                        << " (bootstrap watch attempt)" << Endl;
+                    runtime.DisconnectNodes(initialNodeIdx, otherNodeIdx);
+                    return true;
+                }
+                return false;
+            });
+
+        Cerr << "... disconnect other nodes (new owner expected)" << Endl;
+        for (ui32 nodeIdx : otherNodeIdxs) {
+            runtime.DisconnectNodes(initialNodeIdx, nodeIdx);
+        }
+
+        Cerr << "... sleeping for 2 seconds (new tablet expected to start once)" << Endl;
+        runtime.SimulateSleep(TDuration::Seconds(2));
+
+        ui32 gen2;
+        ui32 secondNode;
+        ui32 secondNodeIdx;
+        auto sender2 = runtime.AllocateEdgeActor();
+        auto client2 = runtime.ConnectToPipe(TTestTxConfig::TxTablet0, sender2, 0, NTabletPipe::TClientRetryPolicy::WithRetries());
+        {
+            Cerr << "... waiting for pipe to connect" << Endl;
+            auto ev = runtime.GrabEdgeEventRethrow<TEvTabletPipe::TEvClientConnected>(sender2);
+            UNIT_ASSERT_VALUES_EQUAL(ev->Get()->Status, NKikimrProto::OK);
+            gen2 = ev->Get()->Generation;
+            secondNode = ev->Get()->ServerId.NodeId();
+            secondNodeIdx = secondNode - runtime.GetNodeId(0);
+        }
+
+        UNIT_ASSERT_C(secondNodeIdx != initialNodeIdx, "Tablet expected to move to a different node");
+        UNIT_ASSERT_C(gen2 == gen1 + 1, "Tablet restarted with gen2# " << gen2 << " after gen1# " << gen1);
+
+        runtime.GrabEdgeEventRethrow<TEvTabletPipe::TEvClientDestroyed>(sender);
+        Y_UNUSED(client2);
+    }
+
+} // Y_UNIT_TEST_SUITE(BootstrapperTest)
+
+} // namespace NKikimr

+ 3 - 0
ydb/core/tablet/tablet_sys.cpp

@@ -817,6 +817,7 @@ void TTablet::HandleStateStorageInfoResolve(TEvStateStorage::TEvInfo::TPtr &ev)
             return PromoteToCandidate(0);
         }
     case NKikimrProto::ERROR:
+    case NKikimrProto::NODATA:
     case NKikimrProto::RACE:
     case NKikimrProto::TIMEOUT:
         return LockedInitializationPath();
@@ -848,6 +849,7 @@ void TTablet::HandleStateStorageInfoLock(TEvStateStorage::TEvInfo::TPtr &ev) {
         }
         return;
     case NKikimrProto::ERROR:
+    case NKikimrProto::NODATA:
         return CancelTablet(TEvTablet::TEvTabletDead::ReasonBootSSError);
     case NKikimrProto::TIMEOUT:
         return CancelTablet(TEvTablet::TEvTabletDead::ReasonBootSSTimeout);
@@ -884,6 +886,7 @@ void TTablet::HandleStateStorageInfoUpgrade(TEvStateStorage::TEvInfo::TPtr &ev)
             return TabletBlockBlobStorage();
         }
     case NKikimrProto::ERROR:
+    case NKikimrProto::NODATA:
         return CancelTablet(TEvTablet::TEvTabletDead::ReasonBootSSError);
     case NKikimrProto::TIMEOUT:
         return CancelTablet(TEvTablet::TEvTabletDead::ReasonBootSSTimeout);

+ 1 - 0
ydb/core/tablet/ut/ya.make

@@ -18,6 +18,7 @@ PEERDIR(
 YQL_LAST_ABI_VERSION()
 
 SRCS(
+    bootstrapper_ut.cpp
     pipe_tracker_ut.cpp
     resource_broker_ut.cpp
     tablet_counters_ut.cpp

Some files were not shown because too many files changed in this diff