From b2b1b29384238ab279972822befbf9ad3987cc2a Mon Sep 17 00:00:00 2001
From: Yaroslav Dynnikov <yaroslav.dynnikov@gmail.com>
Date: Sun, 22 May 2022 23:35:52 +0300
Subject: [PATCH] bug: uuid mismatch on bootstrap leader

When bootstrapping an instance, there're two possible execution paths -
`start_boot` and `start_join`. While `start_join` takes all uuids from
JoinResponse, `start_boot` already deals with a bootstrapped `box.cfg`
(it's done in `start_discover`, refer to [1]). In order to make uuids
consistent across `box.cfg` and topology module, `start_boot` stage is
preceded with rebootstrap.

This case is also covered with a pytest.

- [1] doc/clustering.md
---
 docs/clustering.md         |   5 ++---
 docs/clustering_curves.svg | Bin 113797 -> 124610 bytes
 src/main.rs                |  41 +++++++++++++++++++++++++++++--------
 test/int/test_joining.py   |  32 ++++++++++++++++++++---------
 4 files changed, 56 insertions(+), 22 deletions(-)

diff --git a/docs/clustering.md b/docs/clustering.md
index 04d6675951..cb313212e4 100644
--- a/docs/clustering.md
+++ b/docs/clustering.md
@@ -23,7 +23,7 @@ picodata run --instance-id iN --listen iN --peer i1,i2
 ![main.rs](clustering_curves.svg "main.rs control flow")
 
 Красным показан родительский процесс, который запущен на всём протяжении жизненного цикла инстанса. Вся логика поиска лидера Raft-группы и присоединения к ней происходит в дочернем процессе (голубой цвет). При сбросе состояния инстанса и rebootstrap происходит повторная инициализация форка (сиреневый цвет).
- 
+
 Данная схема наиболее полно отражает логику кода в файле `main.rs`. Ниже описаны детали выполнения каждого этапа и соответствующей программной функции.
 
 ### fn main()
@@ -32,8 +32,7 @@ picodata run --instance-id iN --listen iN --peer i1,i2
 
 ### fn start_discover()
 
-Дочерний процесс начинает своё существование с запуска модуля `box.cfg()` и вызова функции `start_discover`. Возможно, что при этом из постоянно хранимых данных будет ясно, что bootstrap данного инстанса уже был произведён ранее и что Raft уже знает о вхождении этого инстанса в кластер - в таком случае никакого discovery не будет, инстанс сразу перейдёт к этапу `postjoin()`. Однако, если это новый инстанс, то алгоритм discovery выдаст ему флаг `i_am_bootstrap_leader == false` и сообщит адрес лидера Raft-группы. Сам лидер (единственный с `i_am_bootstrap_leader == true`) выполняет функцию `start_boot` и затем переходит к функции `postjoin()`. Остальные инстансы (уже запущенные и все будущие) сбрасывают своё состояние (этап rebootstrap) и переходят к функции `start_join`.
-
+Дочерний процесс начинает своё существование с запуска модуля `box.cfg()` и вызова функции `start_discover`. Возможно, что при этом из постоянно хранимых данных будет ясно, что bootstrap данного инстанса уже был произведён ранее и что Raft уже знает о вхождении этого инстанса в кластер - в таком случае никакого discovery не будет, инстанс сразу перейдёт к этапу `postjoin()`. В противном случае, если место инстанса в кластере ещё не известно, алгоритм discovery опредяет значение флага `i_am_bootstrap_leader` и адрес лидера Raft-группы. Далее все инстансы сбрасывают своё состояние (этап rebootstrap) чтобы повторно провести инициализацию `box.cfg`, теперь уже с известными параметрами. Сам лидер (единственный с `i_am_bootstrap_leader == true`) выполняет функцию `start_boot`. Остальные инстансы переходят к функции `start_join`.
 
 ### fn start_boot()
 
diff --git a/docs/clustering_curves.svg b/docs/clustering_curves.svg
index 107a7add67fb907180a549fc03ca141531fa3842..69d7454948e1da6f52c89873740918ec5fb85a5b 100644
GIT binary patch
delta 1113
zcmZ`&O=ule6y}aE#@G1UHl(8B&DesKxwrTKC-!M^Dea<&p&}ICdowj-&CD}zYU&>a
zK}1Dryo)DvVQGrG62mOsqDaM6su&B3xT>`vg_c5>l_Gd&5<@F-G4RcI?)koR&OP_t
zzwZ3*$I-6?z$kp2cMi#RLSiN($Oxl|Sj5QAa1(^EpT}v>e^YVZ&Dyg`E9{mMd=c!>
zMhL@+s!u_fW{3OTb}yOg9&Lr1P)w{*+=0f{rsY^!#x!$#8WUslX<W>-LTV%{_cZPT
z`zsgkGn3E1bR><Fek&~IjwJt;tz)Ru$-VqW@qX6sp>C2D$(;M<j)`OSf8KdH=@w`<
zjeB{T75^h$A1#uj1?sf>$t!8*o7~7^O|921X8u)rsxE%_`jF}>EACj;Q!nqQMVe>6
zTg5SFmhng`tO=1utThN>K9~Bk+iM3SvK~m|R#*#CVN?irRc5<)t$S6s5x<ru^EDqQ
zhx)6NtkUB0;K=7q_gQnQ$@V2#j5t#6Li73Z=Ez~tKnf8@#>UIWeIIQG#w|=vWX#uP
z6=f09m{KmRguYZ^1o4!`#4sf_G!heHD#~E)H$@xBDCUY78xicg5{5#-Fj1Dsh+$2M
zwF)YN1*6nN5_8QBlkShn)1W_GdIRhQ?)4q_mcKtXEdVUl0iYkUSZZapj;z7TXBiP`
zFG;0l<<rMcSoh9@TLwQJ2b;#J##RfZd1daE87Fe519xw8*H4X>H<n)rUA`Ow<X*k{
z>_%D07cVSjw`92+%PRrWZejRn`RBQlAV9Llr++vZ1c=wTyYi;TtNh>RKk-=A{8v9$
zn0Y)|wtqbsc;k<U+sjM8JvR~{TGP`PUaXFkOBc_2>B?rZT)5QqaZ=+mm)}}V3*CV$
z=e^<J>}K%r;QLW<Pr3i<mnZfiwfZir{qh$*>mRJb+-Sxk>ZePzG7S1@aY|`~h~GNR
zxV&wrss(16T84%XybnGF!{&PcJmRi>c>iGK6lhjme*3{QTNtre8*ON0v5>+TCI-<W
ecrq}`U4LSH@b)6uJ2nk1Hr7<Z;V+9|Y1=;t3`+F?

delta 298
zcmW-Yy-UMD96<S%Qe%Z8wO~PnBUrSMT<`mG#o%9|2*n|!6|3|ED(G6lu}d=q=_=}`
za7e+y!BJFjbMmhc#6hqK?``irz0RHg<nA)S8QLHbA@RQC+<1r)#(XkERNXv6XwYl6
z`!nm!Ufhj3t<ZfRS^?LYiZYBEruBb@P`i6Hh@*b19-oG8ASjkBkij@xa=77GX(f^<
zTV6P6H;)fOcXkrHm31J4YW5=cFmHRsQ8D>lTQ)-BKye~HpE4{2Q4~?au+&5w&o`I~
zWu!(_NGzCPp3ksh0hc^Et6U~a-hGO$AWc@P1v^~d5s<eZ4bTq3oGL}M{Tfe9`nU;B
kx>tje{d{S-)59X%Ow#Zf7N_600ITWl2ny5fH|UiA06%+G9{>OV

diff --git a/src/main.rs b/src/main.rs
index 7b5c8bda41..bfc8b2d0d5 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -162,9 +162,11 @@ fn main() -> ! {
     }
 }
 
+#[allow(clippy::enum_variant_names)]
 #[derive(Debug, Serialize, Deserialize)]
 enum Entrypoint {
     StartDiscover,
+    StartBoot,
     StartJoin { leader_address: String },
 }
 
@@ -172,6 +174,7 @@ impl Entrypoint {
     fn exec(self, args: args::Run, to_supervisor: ipc::Sender<IpcMessage>) {
         match self {
             Self::StartDiscover => start_discover(&args, to_supervisor),
+            Self::StartBoot => start_boot(&args),
             Self::StartJoin { leader_address } => start_join(&args, leader_address),
         }
     }
@@ -351,6 +354,8 @@ fn start_discover(args: &args::Run, to_supervisor: ipc::Sender<IpcMessage>) {
     picolib_setup(args);
     assert!(tarantool::cfg().is_none());
 
+    // Don't try to guess instance and replicaset uuids now,
+    // finally, the box will be rebootstraped after discovery.
     let mut cfg = tarantool::Cfg {
         listen: None,
         read_only: false,
@@ -364,14 +369,6 @@ fn start_discover(args: &args::Run, to_supervisor: ipc::Sender<IpcMessage>) {
     tarantool::set_cfg(&cfg);
 
     traft::Storage::init_schema();
-
-    // исходный discovery::discover() пришлось разбить на две части -
-    // init_global и wait_global. К сожалению, они не могут быть атомарны,
-    // потому что listen порт надо поднимать именно посередине. С неподнятым портом
-    // уходить в кишки discovery::discover() нельзя - на запросы отвечать будет некому.
-    // А если поднять порт до инициализации дискавери, то образуется временно́е окно,
-    // и прилетевший пакет приведёт к панике "discovery error: expected DISCOVERY
-    // to be set on instance startup"
     discovery::init_global(&args.peers);
     init_handlers();
 
@@ -386,7 +383,13 @@ fn start_discover(args: &args::Run, to_supervisor: ipc::Sender<IpcMessage>) {
 
     match role {
         discovery::Role::Leader { .. } => {
-            start_boot(args);
+            let next_entrypoint = Entrypoint::StartBoot {};
+            let msg = IpcMessage {
+                next_entrypoint,
+                drop_db: true,
+            };
+            to_supervisor.send(&msg);
+            std::process::exit(0);
         }
         discovery::Role::NonLeader { leader } => {
             let next_entrypoint = Entrypoint::StartJoin {
@@ -416,6 +419,26 @@ fn start_boot(args: &args::Run) {
     let peer = topology.diff().pop().unwrap();
     let raft_id = peer.raft_id;
 
+    picolib_setup(args);
+    assert!(tarantool::cfg().is_none());
+
+    let cfg = tarantool::Cfg {
+        listen: None,
+        read_only: false,
+        instance_uuid: Some(peer.instance_uuid.clone()),
+        replicaset_uuid: Some(peer.replicaset_uuid.clone()),
+        wal_dir: args.data_dir.clone(),
+        memtx_dir: args.data_dir.clone(),
+        log_level: args.log_level() as u8,
+        ..Default::default()
+    };
+
+    std::fs::create_dir_all(&args.data_dir).unwrap();
+    tarantool::set_cfg(&cfg);
+
+    traft::Storage::init_schema();
+    init_handlers();
+
     start_transaction(|| -> Result<(), Error> {
         let cs = raft::ConfState {
             voters: vec![raft_id],
diff --git a/test/int/test_joining.py b/test/int/test_joining.py
index f2bf7d320b..d153b1974f 100644
--- a/test/int/test_joining.py
+++ b/test/int/test_joining.py
@@ -78,27 +78,39 @@ def test_request_follower(cluster2: Cluster):
     assert e.value.args == ("ER_PROC_C", "not a leader")
 
 
-def test_instance_uuid(cluster2: Cluster):
+def test_uuids(cluster2: Cluster):
     i1, i2 = cluster2.instances
     i1.assert_raft_status("Leader")
 
-    ret = i1.call(
+    peer_1 = i1.call(
+        ".raft_join",
+        i1.instance_id,
+        None,  # replicaset_id
+        i1.listen,  # address
+        True,  # voter
+    )[0]["peer"]
+    assert peer_1["instance_id"] == i1.instance_id
+    assert peer_1["instance_uuid"] == i1.eval("return box.info.uuid")
+    assert peer_1["replicaset_uuid"] == i1.eval("return box.info.cluster.uuid")
+
+    peer_2 = i1.call(
         ".raft_join",
         i2.instance_id,
         None,  # replicaset_id
         i2.listen,  # address
         True,  # voter
     )[0]["peer"]
-    assert ret["instance_id"] == i2.instance_id
-    assert ret["instance_uuid"] == i2.eval("return box.info.uuid")
+    assert peer_2["instance_id"] == i2.instance_id
+    assert peer_2["instance_uuid"] == i2.eval("return box.info.uuid")
+    assert peer_2["replicaset_uuid"] == i2.eval("return box.info.cluster.uuid")
 
     # Two consequent requests must obtain same raft_id and instance_id
-    ret1 = fake_join(i1, "fake", timeout=1)[0]["peer"]
-    ret2 = fake_join(i1, "fake", timeout=1)[0]["peer"]
-    assert ret1["instance_id"] == "fake"
-    assert ret2["instance_id"] == "fake"
-    assert ret1["raft_id"] == ret2["raft_id"]
-    assert ret1["instance_uuid"] == ret2["instance_uuid"]
+    fake_peer_1 = fake_join(i1, "fake", timeout=1)[0]["peer"]
+    fake_peer_2 = fake_join(i1, "fake", timeout=1)[0]["peer"]
+    assert fake_peer_1["instance_id"] == "fake"
+    assert fake_peer_2["instance_id"] == "fake"
+    assert fake_peer_1["raft_id"] == fake_peer_2["raft_id"]
+    assert fake_peer_1["instance_uuid"] == fake_peer_2["instance_uuid"]
 
 
 def test_discovery(cluster: Cluster):
-- 
GitLab