From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from [87.239.111.99] (localhost [127.0.0.1]) by dev.tarantool.org (Postfix) with ESMTP id 824A66901E; Wed, 14 Apr 2021 17:20:23 +0300 (MSK) DKIM-Filter: OpenDKIM Filter v2.11.0 dev.tarantool.org 824A66901E DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=tarantool.org; s=dev; t=1618410023; bh=S68oaY8eXGJW9wklAJU5MxKVdqQW6EGgK/eRvskVZd4=; h=To:Date:In-Reply-To:References:Subject:List-Id:List-Unsubscribe: List-Archive:List-Post:List-Help:List-Subscribe:From:Reply-To:Cc: From; b=NBEP/3FWBWaOt+mSNl9yhD8NVz3cSNifiAl+5M2cPLGzO6re8Swof5wyJwkC2wt7w FSFD5Ws9JFbH3XPrOtM2gmVI7ZfW04lr2OxFTlXlH7LBajVxhKfE9p2VrxPVl0fDbM 97JE+ufwMO8FoA5LhAWPRRU3PqDjmVHg2yXdVhLE= Received: from smtp41.i.mail.ru (smtp41.i.mail.ru [94.100.177.101]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by dev.tarantool.org (Postfix) with ESMTPS id E603669013 for ; Wed, 14 Apr 2021 17:17:28 +0300 (MSK) DKIM-Filter: OpenDKIM Filter v2.11.0 dev.tarantool.org E603669013 Received: by smtp41.i.mail.ru with esmtpa (envelope-from ) id 1lWgKd-000456-Gq; Wed, 14 Apr 2021 17:17:28 +0300 To: v.shpilevoy@tarantool.org, gorcunov@gmail.com Date: Wed, 14 Apr 2021 17:17:16 +0300 Message-Id: <578222e82d897fd042d73afefa14766a0518de09.1618409665.git.sergepetrenko@tarantool.org> X-Mailer: git-send-email 2.24.3 (Apple Git-128) In-Reply-To: References: MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-7564579A: 646B95376F6C166E X-77F55803: 4F1203BC0FB41BD92FFCB8E6708E7480B1C8842CE613979723F2FB4628545A35182A05F5380850406DA18247BB42A86E9A07DF779C75089012AED463700B78EE3061E709693F527C X-7FA49CB5: FF5795518A3D127A4AD6D5ED66289B5278DA827A17800CE7837EFDF6F90DE77AEA1F7E6F0F101C67BD4B6F7A4D31EC0BCC500DACC3FED6E28638F802B75D45FF8AA50765F7900637225475490A77DC518638F802B75D45FF914D58D5BE9E6BC1A93B80C6DEB9DEE97C6FB206A91F05B2D5CC862B9AB6E4D6209C73A9008084EFFEEB761FB9071033D2E47CDBA5A96583C09775C1D3CA48CFCA5A41EBD8A3A0199FA2833FD35BB23D2EF20D2F80756B5F868A13BD56FB6657A471835C12D1D977725E5C173C3A84C3CA5A41EBD8A3A0199FA2833FD35BB23DF004C90652538430302FCEF25BFAB3454AD6D5ED66289B5278DA827A17800CE7098CB5C96EA516CED32BA5DBAC0009BE395957E7521B51C20BC6067A898B09E4090A508E0FED6299176DF2183F8FC7C0326150D8DED3CE20CD04E86FAF290E2D7E9C4E3C761E06A71DD303D21008E298D5E8D9A59859A8B6B372FE9A2E580EFC725E5C173C3A84C3C9EEE74C166EF7BC35872C767BF85DA2F004C90652538430E4A6367B16DE6309 X-B7AD71C0: AC4F5C86D027EB782CDD5689AFBDA7A2368A440D3B0F6089093C9A16E5BC824A2A04A2ABAA09D25379311020FFC8D4AD2C1D4D45F0910F18CB9693E69441C7D5 X-C1DE0DAB: C20DE7B7AB408E4181F030C43753B8183A4AFAF3EA6BDC44E1F4276B80994196B69F9342289A40B3AC0AA3A11FBF1A0AB95B1CD81E4252F29C2B6934AE262D3EE7EAB7254005DCED7532B743992DF240BDC6A1CF3F042BAD6DF99611D93F60EF0417BEADF48D1460699F904B3F4130E343918A1A30D5E7FCCB5012B2E24CD356 X-C8649E89: 4E36BF7865823D7055A7F0CF078B5EC49A30900B95165D341C998A3771F04153F6CFA84C6C1A3CC831E750DD60F49E98115E1F4E78FF9431AE414175921F5A851D7E09C32AA3244C6B034F77B4F2F162039BF029C58CC4FE435BF7150578642F927AC6DF5659F194 X-D57D3AED: 3ZO7eAau8CL7WIMRKs4sN3D3tLDjz0dLbV79QFUyzQ2Ujvy7cMT6pYYqY16iZVKkSc3dCLJ7zSJH7+u4VD18S7Vl4ZUrpaVfd2+vE6kuoey4m4VkSEu530nj6fImhcD4MUrOEAnl0W826KZ9Q+tr5ycPtXkTV4k65bRjmOUUP8cvGozZ33TWg5HZplvhhXbhDGzqmQDTd6OAevLeAnq3Ra9uf7zvY2zzsIhlcp/Y7m53TZgf2aB4JOg4gkr2biojrcJA+pXcDul9vVzxWc7nmw== X-Mailru-Sender: 583F1D7ACE8F49BDD2846D59FC20E9F832D6326595065D5CA931A6EEB6B42AD2B9FDA50507DC254E424AE0EB1F3D1D21E2978F233C3FAE6EE63DB1732555E4A8EE80603BA4A5B0BC112434F685709FCF0DA7A0AF5A3A8387 X-Mras: Ok Subject: [Tarantool-patches] [PATCH v3 06/10] raft: keep track of greatest known term and filter replication sources based on that X-BeenThere: tarantool-patches@dev.tarantool.org X-Mailman-Version: 2.1.34 Precedence: list List-Id: Tarantool development patches List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , From: Serge Petrenko via Tarantool-patches Reply-To: Serge Petrenko Cc: tarantool-patches@dev.tarantool.org Errors-To: tarantool-patches-bounces@dev.tarantool.org Sender: "Tarantool-patches" Start writing the actual leader term together with the PROMOTE request and process terms in PROMOTE requests on receiver side. Make applier only apply synchronous transactions from the instance which has the greatest term as received in PROMOTE requests. Closes #5445 --- ...very => qsync-multi-statement-recovery.md} | 0 changelogs/unreleased/raft-promote.md | 4 + src/box/applier.cc | 28 ++ src/box/box.cc | 24 +- src/lib/raft/raft.c | 1 + src/lib/raft/raft.h | 46 +++ .../gh-5445-leader-inconsistency.result | 291 ++++++++++++++++++ .../gh-5445-leader-inconsistency.test.lua | 128 ++++++++ test/replication/suite.cfg | 1 + 9 files changed, 516 insertions(+), 7 deletions(-) rename changelogs/unreleased/{qsync-multi-statement-recovery => qsync-multi-statement-recovery.md} (100%) create mode 100644 changelogs/unreleased/raft-promote.md create mode 100644 test/replication/gh-5445-leader-inconsistency.result create mode 100644 test/replication/gh-5445-leader-inconsistency.test.lua diff --git a/changelogs/unreleased/qsync-multi-statement-recovery b/changelogs/unreleased/qsync-multi-statement-recovery.md similarity index 100% rename from changelogs/unreleased/qsync-multi-statement-recovery rename to changelogs/unreleased/qsync-multi-statement-recovery.md diff --git a/changelogs/unreleased/raft-promote.md b/changelogs/unreleased/raft-promote.md new file mode 100644 index 000000000..e5dac599c --- /dev/null +++ b/changelogs/unreleased/raft-promote.md @@ -0,0 +1,4 @@ +## bugfix/replication + +* Fix a bug in synchronous replication when rolled back transactions could + reappear once a sufficiently old instance reconnected (gh-5445). diff --git a/src/box/applier.cc b/src/box/applier.cc index 4898f9f7b..3fb864686 100644 --- a/src/box/applier.cc +++ b/src/box/applier.cc @@ -790,6 +790,12 @@ apply_synchro_row_cb(struct journal_entry *entry) applier_rollback_by_wal_io(); } else { txn_limbo_process(&txn_limbo, synchro_entry->req); + if (iproto_type_is_promote_request(synchro_entry->req->type)) { + raft_source_update_term(box_raft(), + synchro_entry->req->origin_id, + synchro_entry->req->term); + + } trigger_run(&replicaset.applier.on_wal_write, NULL); } /* The fiber is the same on final join. */ @@ -1027,6 +1033,28 @@ applier_apply_tx(struct applier *applier, struct stailq *rows) } } + /* + * When elections are enabled we must filter out synchronous rows coming + * from an instance that fell behind the current leader. This includes + * both synchronous tx rows and rows for txs following unconfirmed + * synchronous transactions. + * The rows are replaced with NOPs to preserve the vclock consistency. + */ + struct applier_tx_row *item; + if (raft_source_has_outdated_term(box_raft(), applier->instance_id) && + (last_row->wait_sync || + (iproto_type_is_synchro_request(first_row->type) && + !iproto_type_is_promote_request(first_row->type)))) { + stailq_foreach_entry(item, rows, next) { + struct xrow_header *row = &item->row; + row->type = IPROTO_NOP; + /* + * Row body is saved to fiber's region and will be freed + * on next fiber_gc() call. + */ + row->bodycnt = 0; + } + } if (unlikely(iproto_type_is_synchro_request(first_row->type))) { /* * Synchro messages are not transactions, in terms diff --git a/src/box/box.cc b/src/box/box.cc index 722fc23b7..f44dd0e54 100644 --- a/src/box/box.cc +++ b/src/box/box.cc @@ -426,6 +426,10 @@ wal_stream_apply_synchro_row(struct wal_stream *stream, struct xrow_header *row) return -1; } txn_limbo_process(&txn_limbo, &syn_req); + if (syn_req.type == IPROTO_PROMOTE) { + raft_source_update_term(box_raft(), syn_req.origin_id, + syn_req.term); + } return 0; } @@ -1503,7 +1507,12 @@ box_clear_synchro_queue(bool try_wait) return -1; } - if (!is_box_configured) + /* + * Do nothing when box isn't configured and when PROMOTE was already + * written for this term. + */ + if (!is_box_configured || + raft_source_term(box_raft(), instance_id) == box_raft()->term) return 0; uint32_t former_leader_id = txn_limbo.owner_id; int64_t wait_lsn = txn_limbo.confirmed_lsn; @@ -1558,20 +1567,21 @@ box_clear_synchro_queue(bool try_wait) rc = -1; } else { promote: - /* - * Term parameter is unused now, We'll pass - * box_raft()->term there later. - */ - txn_limbo_write_promote(&txn_limbo, wait_lsn, 0); + /* We cannot possibly get here in a volatile state. */ + assert(box_raft()->volatile_term == box_raft()->term); + txn_limbo_write_promote(&txn_limbo, wait_lsn, + box_raft()->term); struct synchro_request req = { .type = 0, /* unused */ .replica_id = 0, /* unused */ .origin_id = instance_id, .lsn = wait_lsn, - .term = 0, /* unused */ + .term = box_raft()->term, }; txn_limbo_read_promote(&txn_limbo, &req); assert(txn_limbo_is_empty(&txn_limbo)); + raft_source_update_term(box_raft(), req.origin_id, + req.term); } } in_clear_synchro_queue = false; diff --git a/src/lib/raft/raft.c b/src/lib/raft/raft.c index 4ea4fc3f8..e9ce8cade 100644 --- a/src/lib/raft/raft.c +++ b/src/lib/raft/raft.c @@ -985,6 +985,7 @@ raft_create(struct raft *raft, const struct raft_vtab *vtab) .death_timeout = 5, .vtab = vtab, }; + vclock_create(&raft->term_map); raft_ev_timer_init(&raft->timer, raft_sm_schedule_new_election_cb, 0, 0); raft->timer.data = raft; diff --git a/src/lib/raft/raft.h b/src/lib/raft/raft.h index e447f6634..01f548fee 100644 --- a/src/lib/raft/raft.h +++ b/src/lib/raft/raft.h @@ -207,6 +207,19 @@ struct raft { * subsystems, such as Raft. */ const struct vclock *vclock; + /** + * The biggest term seen by this instance and persisted in WAL as part + * of a PROMOTE request. May be smaller than @a term, while there are + * ongoing elections, or the leader is already known, but this instance + * hasn't read its PROMOTE request yet. + * During other times must be equal to @a term. + */ + uint64_t greatest_known_term; + /** + * Latest terms received with PROMOTE entries from remote instances. + * Raft uses them to determine data from which sources may be applied. + */ + struct vclock term_map; /** State machine timed event trigger. */ struct ev_timer timer; /** Configured election timeout in seconds. */ @@ -243,6 +256,39 @@ raft_is_source_allowed(const struct raft *raft, uint32_t source_id) return !raft->is_enabled || raft->leader == source_id; } +/** + * Return the latest term as seen in PROMOTE requests from instance with id + * @a source_id. + */ +static inline uint64_t +raft_source_term(const struct raft *raft, uint32_t source_id) +{ + assert(source_id != 0 && source_id < VCLOCK_MAX); + return vclock_get(&raft->term_map, source_id); +} + +/** + * Check whether replica with id @a source_id is too old to apply synchronous + * data from it. The check is only valid when elections are enabled. + */ +static inline bool +raft_source_has_outdated_term(const struct raft *raft, uint32_t source_id) +{ + uint64_t source_term = vclock_get(&raft->term_map, source_id); + return raft->is_enabled && source_term < raft->greatest_known_term; +} + +/** Remember the last term seen for replica with id @a source_id. */ +static inline void +raft_source_update_term(struct raft *raft, uint32_t source_id, uint64_t term) +{ + if ((uint64_t) vclock_get(&raft->term_map, source_id) >= term) + return; + vclock_follow(&raft->term_map, source_id, term); + if (term > raft->greatest_known_term) + raft->greatest_known_term = term; +} + /** Check if Raft is enabled. */ static inline bool raft_is_enabled(const struct raft *raft) diff --git a/test/replication/gh-5445-leader-inconsistency.result b/test/replication/gh-5445-leader-inconsistency.result new file mode 100644 index 000000000..ff3104de5 --- /dev/null +++ b/test/replication/gh-5445-leader-inconsistency.result @@ -0,0 +1,291 @@ +-- test-run result file version 2 +test_run = require("test_run").new() + | --- + | ... + +is_leader_cmd = "return box.info.election.state == 'leader'" + | --- + | ... + +-- Auxiliary. +test_run:cmd('setopt delimiter ";"') + | --- + | - true + | ... +function get_leader(nrs) + local leader_nr = 0 + test_run:wait_cond(function() + for nr, do_check in pairs(nrs) do + if do_check then + local is_leader = test_run:eval('election_replica'..nr, + is_leader_cmd)[1] + if is_leader then + leader_nr = nr + return true + end + end + end + return false + end) + assert(leader_nr ~= 0) + return leader_nr +end; + | --- + | ... + +function name(id) + return 'election_replica'..id +end; + | --- + | ... +test_run:cmd('setopt delimiter ""'); + | --- + | - true + | ... + +-- +-- gh-5445: make sure rolled back rows do not reappear once old leader returns +-- to cluster. +-- +SERVERS = {'election_replica1', 'election_replica2' ,'election_replica3'} + | --- + | ... +test_run:create_cluster(SERVERS, "replication", {args='2 0.4'}) + | --- + | ... +test_run:wait_fullmesh(SERVERS) + | --- + | ... + +-- Any of the three instances may bootstrap the cluster and become leader. +is_possible_leader = {true, true, true} + | --- + | ... +leader_nr = get_leader(is_possible_leader) + | --- + | ... +leader = name(leader_nr) + | --- + | ... +next_leader_nr = ((leader_nr - 1) % 3 + 1) % 3 + 1 -- {1, 2, 3} -> {2, 3, 1} + | --- + | ... +next_leader = name(next_leader_nr) + | --- + | ... +other_nr = ((leader_nr - 1) % 3 + 2) % 3 + 1 -- {1, 2, 3} -> {3, 1, 2} + | --- + | ... +other = name(other_nr) + | --- + | ... + +test_run:switch(other) + | --- + | - true + | ... +box.cfg{election_mode='voter'} + | --- + | ... +test_run:switch('default') + | --- + | - true + | ... + +test_run:switch(next_leader) + | --- + | - true + | ... +box.cfg{election_mode='voter'} + | --- + | ... +test_run:switch('default') + | --- + | - true + | ... + +test_run:switch(leader) + | --- + | - true + | ... +box.ctl.wait_rw() + | --- + | ... +_ = box.schema.space.create('test', {is_sync=true}) + | --- + | ... +_ = box.space.test:create_index('pk') + | --- + | ... +box.space.test:insert{1} + | --- + | - [1] + | ... + +-- Simulate a situation when the instance which will become the next leader +-- doesn't know of unconfirmed rows. It should roll them back anyways and do not +-- accept them once they actually appear from the old leader. +-- So, stop the instance which'll be the next leader. +test_run:switch('default') + | --- + | - true + | ... +test_run:cmd('stop server '..next_leader) + | --- + | - true + | ... +test_run:switch(leader) + | --- + | - true + | ... +-- Insert some unconfirmed data. +box.cfg{replication_synchro_quorum=3, replication_synchro_timeout=1000} + | --- + | ... +fib = require('fiber').create(box.space.test.insert, box.space.test, {2}) + | --- + | ... +fib:status() + | --- + | - suspended + | ... + +-- 'other', 'leader', 'next_leader' are defined on 'default' node, hence the +-- double switches. +test_run:switch('default') + | --- + | - true + | ... +test_run:switch(other) + | --- + | - true + | ... +-- Wait until the rows are replicated to the other instance. +test_run:wait_cond(function() return box.space.test:get{2} ~= nil end) + | --- + | - true + | ... +-- Old leader is gone. +test_run:switch('default') + | --- + | - true + | ... +test_run:cmd('stop server '..leader) + | --- + | - true + | ... +is_possible_leader[leader_nr] = false + | --- + | ... + +-- Emulate a situation when next_leader wins the elections. It can't do that in +-- this configuration, obviously, because it's behind the 'other' node, so set +-- quorum to 1 and imagine there are 2 more servers which would vote for +-- next_leader. +-- Also, make the instance ignore synchronization with other replicas. +-- Otherwise it would stall for replication_sync_timeout. This is due to the +-- nature of the test and may be ignored (we restart the instance to simulate +-- a situation when some rows from the old leader were not received). +test_run:cmd('start server '..next_leader..' with args="1 0.4 candidate 1"') + | --- + | - true + | ... +assert(get_leader(is_possible_leader) == next_leader_nr) + | --- + | - true + | ... +test_run:switch(other) + | --- + | - true + | ... +-- New leader didn't know about the unconfirmed rows but still rolled them back. +test_run:wait_cond(function() return box.space.test:get{2} == nil end) + | --- + | - true + | ... + +test_run:switch('default') + | --- + | - true + | ... +test_run:switch(next_leader) + | --- + | - true + | ... +-- No signs of the unconfirmed transaction. +box.space.test:select{} -- 1 + | --- + | - - [1] + | ... + +test_run:switch('default') + | --- + | - true + | ... +-- Old leader returns and old unconfirmed rows from it must be ignored. +-- Note, it wins the elections fairly. +test_run:cmd('start server '..leader..' with args="3 0.4 voter"') + | --- + | - true + | ... +test_run:wait_lsn(leader, next_leader) + | --- + | ... +test_run:switch(leader) + | --- + | - true + | ... +test_run:wait_cond(function() return box.space.test:get{2} == nil end) + | --- + | - true + | ... +box.cfg{election_mode='candidate'} + | --- + | ... + +test_run:switch('default') + | --- + | - true + | ... +test_run:switch(next_leader) + | --- + | - true + | ... +-- Resign to make old leader win the elections. +box.cfg{election_mode='voter'} + | --- + | ... + +test_run:switch('default') + | --- + | - true + | ... +is_possible_leader[leader_nr] = true + | --- + | ... +assert(get_leader(is_possible_leader) == leader_nr) + | --- + | - true + | ... + +test_run:switch(next_leader) + | --- + | - true + | ... +test_run:wait_upstream(1, {status='follow'}) + | --- + | - true + | ... +box.space.test:select{} -- 1 + | --- + | - - [1] + | ... + +-- Cleanup. +test_run:switch('default') + | --- + | - true + | ... +test_run:drop_cluster(SERVERS) + | --- + | ... diff --git a/test/replication/gh-5445-leader-inconsistency.test.lua b/test/replication/gh-5445-leader-inconsistency.test.lua new file mode 100644 index 000000000..bf8b31886 --- /dev/null +++ b/test/replication/gh-5445-leader-inconsistency.test.lua @@ -0,0 +1,128 @@ +test_run = require("test_run").new() + +is_leader_cmd = "return box.info.election.state == 'leader'" + +-- Auxiliary. +test_run:cmd('setopt delimiter ";"') +function get_leader(nrs) + local leader_nr = 0 + test_run:wait_cond(function() + for nr, do_check in pairs(nrs) do + if do_check then + local is_leader = test_run:eval('election_replica'..nr, + is_leader_cmd)[1] + if is_leader then + leader_nr = nr + return true + end + end + end + return false + end) + assert(leader_nr ~= 0) + return leader_nr +end; + +function name(id) + return 'election_replica'..id +end; +test_run:cmd('setopt delimiter ""'); + +-- +-- gh-5445: make sure rolled back rows do not reappear once old leader returns +-- to cluster. +-- +SERVERS = {'election_replica1', 'election_replica2' ,'election_replica3'} +test_run:create_cluster(SERVERS, "replication", {args='2 0.4'}) +test_run:wait_fullmesh(SERVERS) + +-- Any of the three instances may bootstrap the cluster and become leader. +is_possible_leader = {true, true, true} +leader_nr = get_leader(is_possible_leader) +leader = name(leader_nr) +next_leader_nr = ((leader_nr - 1) % 3 + 1) % 3 + 1 -- {1, 2, 3} -> {2, 3, 1} +next_leader = name(next_leader_nr) +other_nr = ((leader_nr - 1) % 3 + 2) % 3 + 1 -- {1, 2, 3} -> {3, 1, 2} +other = name(other_nr) + +test_run:switch(other) +box.cfg{election_mode='voter'} +test_run:switch('default') + +test_run:switch(next_leader) +box.cfg{election_mode='voter'} +test_run:switch('default') + +test_run:switch(leader) +box.ctl.wait_rw() +_ = box.schema.space.create('test', {is_sync=true}) +_ = box.space.test:create_index('pk') +box.space.test:insert{1} + +-- Simulate a situation when the instance which will become the next leader +-- doesn't know of unconfirmed rows. It should roll them back anyways and do not +-- accept them once they actually appear from the old leader. +-- So, stop the instance which'll be the next leader. +test_run:switch('default') +test_run:cmd('stop server '..next_leader) +test_run:switch(leader) +-- Insert some unconfirmed data. +box.cfg{replication_synchro_quorum=3, replication_synchro_timeout=1000} +fib = require('fiber').create(box.space.test.insert, box.space.test, {2}) +fib:status() + +-- 'other', 'leader', 'next_leader' are defined on 'default' node, hence the +-- double switches. +test_run:switch('default') +test_run:switch(other) +-- Wait until the rows are replicated to the other instance. +test_run:wait_cond(function() return box.space.test:get{2} ~= nil end) +-- Old leader is gone. +test_run:switch('default') +test_run:cmd('stop server '..leader) +is_possible_leader[leader_nr] = false + +-- Emulate a situation when next_leader wins the elections. It can't do that in +-- this configuration, obviously, because it's behind the 'other' node, so set +-- quorum to 1 and imagine there are 2 more servers which would vote for +-- next_leader. +-- Also, make the instance ignore synchronization with other replicas. +-- Otherwise it would stall for replication_sync_timeout. This is due to the +-- nature of the test and may be ignored (we restart the instance to simulate +-- a situation when some rows from the old leader were not received). +test_run:cmd('start server '..next_leader..' with args="1 0.4 candidate 1"') +assert(get_leader(is_possible_leader) == next_leader_nr) +test_run:switch(other) +-- New leader didn't know about the unconfirmed rows but still rolled them back. +test_run:wait_cond(function() return box.space.test:get{2} == nil end) + +test_run:switch('default') +test_run:switch(next_leader) +-- No signs of the unconfirmed transaction. +box.space.test:select{} -- 1 + +test_run:switch('default') +-- Old leader returns and old unconfirmed rows from it must be ignored. +-- Note, it wins the elections fairly. +test_run:cmd('start server '..leader..' with args="3 0.4 voter"') +test_run:wait_lsn(leader, next_leader) +test_run:switch(leader) +test_run:wait_cond(function() return box.space.test:get{2} == nil end) +box.cfg{election_mode='candidate'} + +test_run:switch('default') +test_run:switch(next_leader) +-- Resign to make old leader win the elections. +box.cfg{election_mode='voter'} + +test_run:switch('default') +is_possible_leader[leader_nr] = true +assert(get_leader(is_possible_leader) == leader_nr) + +test_run:switch(next_leader) +test_run:wait_upstream(1, {status='follow'}) +box.space.test:select{} -- 1 + +-- Cleanup. +test_run:switch('default') +test_run:drop_cluster(SERVERS) diff --git a/test/replication/suite.cfg b/test/replication/suite.cfg index 4a9ca0a46..8b185ce7e 100644 --- a/test/replication/suite.cfg +++ b/test/replication/suite.cfg @@ -19,6 +19,7 @@ "gh-5213-qsync-applier-order-3.test.lua": {}, "gh-5426-election-on-off.test.lua": {}, "gh-5433-election-restart-recovery.test.lua": {}, + "gh-5445-leader-inconsistency.test.lua": {}, "gh-5506-election-on-off.test.lua": {}, "once.test.lua": {}, "on_replace.test.lua": {}, -- 2.24.3 (Apple Git-128)