forked from tarantool/tarantool
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
replication: recovery mixed transacrtions
See the docbot request for details. Closes tarantool#7932 @TarantoolBot document Title: correct recovery of mixed transactions Transaction boundaries in WAL were introduced in 2.1.2 (`872d6f1`), but replication applier started to apply transaction atomically only in 2.2.1(`e94ba2e`), so within this range it is possible that transactions are mixed in WAL. There was no problem with them until 2.8.1 because recovering process was still working row-by-row, but in 2.8.1 (`9311113`) it became transactional as well so starting from 2.8.1 tarantool fails to handle mixed transactions from WAL. Mixed transactions is obviously the one of unexpected scenarios in modern tarantool, so we should not handle it in normal mode, and we take care of it only in force recovery mode. But current 'force recovery' treats all the rows of the unexpected transaction as unexpected and thus just skips the entire transaction row-by-row. However we have enough information in this case to handle it more data-friendly. Note: Let there be two nodes (`node#1` and `node#2`). And let the data be replicated from `node#1` to `node#2`. Suppose that at some point in time, `node#1` is restoring data from an xlog containing mixed transactions. To replicate data from `node#1` to `node#2`, do the following: 1) Disable `node#2` 2) Start `node#1` by setting `force_recovery` to `true` 3) Connect `node#2`
- Loading branch information
1 parent
bc5a086
commit a9044f6
Showing
6 changed files
with
310 additions
and
1 deletion.
There are no files selected for viewing
14 changes: 14 additions & 0 deletions
14
changelogs/unreleased/gh-7932-recovery-mixed-transactions.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
## feature/replication | ||
|
||
* Implemented correct recovery of mixed transactions. To do this, set | ||
`box.cfg.force_recovery` to `true`. If you need to revert to the old | ||
behavior, don't set the `force_recovery` option. | ||
|
||
* Note: | ||
Let there be two nodes (`node#1` and `node#2`). And let the data be | ||
replicated from `node#1` to `node#2`. Suppose that at some point in time, | ||
`node#1` is restoring data from an xlog containing mixed transactions. To | ||
replicate data from `node#1` to `node#2`, do the following: | ||
1. Disable `node#2` | ||
2. Start `node#1` by setting `force_recovery` to `true` | ||
3. Connect `node#2` (gh-7932). |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Binary file added
BIN
+597 Bytes
test/replication-luatest/gh_7932_data/bad_xlog/00000000000000000000.xlog
Binary file not shown.
Binary file added
BIN
+646 Bytes
test/replication-luatest/gh_7932_data/good_xlog/00000000000000000000.xlog
Binary file not shown.
Binary file added
BIN
+595 Bytes
test/replication-luatest/gh_7932_data/not_finished_xlog/00000000000000000000.xlog
Binary file not shown.
127 changes: 127 additions & 0 deletions
127
test/replication-luatest/gh_7932_recovery_mixed_transactions_test.lua
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
local t = require('luatest') | ||
local cluster = require('luatest.replica_set') | ||
local fio = require('fio') | ||
|
||
local g = t.group('gh_7932') | ||
|
||
g.before_each(function(cg) | ||
cg.cluster = cluster:new({}) | ||
cg.server = cg.cluster:build_and_add_server{ | ||
alias = 'server', | ||
box_cfg = { | ||
instance_uuid = '1a685e63-24cd-4fc5-9532-bf85f649e0ab', | ||
}, | ||
} | ||
cg.cluster:start() | ||
end) | ||
|
||
g.after_each(function(cg) | ||
cg.cluster:drop() | ||
end) | ||
|
||
g.test_good_xlog_with_force_recovery = function(cg) | ||
--Delete all *.xlogs on the server | ||
cg.server:stop() | ||
local glob = fio.pathjoin(cg.server.workdir, '*.xlog') | ||
local xlog = fio.glob(glob) | ||
for _, file in pairs(xlog) do fio.unlink(file) end | ||
--Copying the prepared log to the server | ||
xlog = 'test/replication-luatest/gh_7932_data/good_xlog/'.. | ||
'00000000000000000000.xlog' | ||
fio.copyfile(xlog, cg.server.workdir) | ||
|
||
cg.server.box_cfg.force_recovery = true | ||
cg.server:start() | ||
-- The first transaction is applied and after the second transaction | ||
-- is applied. | ||
t.assert_equals(cg.server:exec(function() | ||
return box.space.test:select() | ||
end), {{1, 2}, {2, 2}, {3, 2}}) | ||
end | ||
|
||
g.test_good_xlog_without_force_recovery = function(cg) | ||
-- Delete all *.xlogs on the server | ||
cg.server:stop() | ||
local glob = fio.pathjoin(cg.server.workdir, '*.xlog') | ||
local xlog = fio.glob(glob) | ||
for _, file in pairs(xlog) do fio.unlink(file) end | ||
-- Copying the prepared log to the server | ||
xlog = 'test/replication-luatest/gh_7932_data/good_xlog/'.. | ||
'00000000000000000000.xlog' | ||
fio.copyfile(xlog, cg.server.workdir) | ||
|
||
cg.server.box_cfg.force_recovery = false | ||
cg.server:start({wait_until_ready = false}) | ||
local logfile = fio.pathjoin(cg.server.workdir, 'server.log') | ||
-- The transactions aren't applied without force_recovery | ||
t.helpers.retrying({}, function() | ||
t.assert(cg.server:grep_log("error at request", nil, | ||
{filename = logfile}), "{type: 'REPLACE', replica_id: 2, ".. | ||
"lsn: 32050, space_id: 512, index_id: 0, tuple: [1, 2]}") | ||
t.assert(cg.server:grep_log("XlogError", nil, | ||
{filename = logfile}), "found a next transaction with the ".. | ||
"previous one not yet committed") | ||
end) | ||
end | ||
|
||
g.test_bad_xlog_with_force_recovery = function(cg) | ||
--Delete all *.xlogs on the server | ||
cg.server:stop() | ||
local glob = fio.pathjoin(cg.server.workdir, '*.xlog') | ||
local xlog = fio.glob(glob) | ||
for _, file in pairs(xlog) do fio.unlink(file) end | ||
--Copying the prepared log to the server | ||
xlog = 'test/replication-luatest/gh_7932_data/bad_xlog/'.. | ||
'00000000000000000000.xlog' | ||
fio.copyfile(xlog, cg.server.workdir) | ||
|
||
cg.server.box_cfg.force_recovery = true | ||
cg.server:start() | ||
local logfile = fio.pathjoin(cg.server.workdir, 'server.log') | ||
|
||
-- The second transaction cannot be applied because the first global | ||
-- row in the transaction has an LSN/TSN mismatch. | ||
t.helpers.retrying({}, function() | ||
t.assert(cg.server:grep_log("error at request", nil, | ||
{filename = logfile}), "{type: 'REPLACE', replica_id: 2, ".. | ||
"lsn: 32051, space_id: 512, index_id: 0, tuple: [2, 2]}") | ||
t.assert(cg.server:grep_log("skipping row {2: 32052}", nil, | ||
{filename = logfile}), "skipping row") | ||
t.assert(cg.server:grep_log("XlogError", nil, | ||
{filename = logfile}), "found a first global row in a ".. | ||
"transaction with LSN/TSN mismatch") | ||
end) | ||
|
||
-- Only the first transaction is applied | ||
t.assert_equals(cg.server:exec(function() | ||
return box.space.test:select() | ||
end), {{1, 3}, {2, 3}, {3, 3}}) | ||
end | ||
|
||
g.test_not_finished_transaction = function(cg) | ||
--Delete all *.xlogs on the server | ||
cg.server:stop() | ||
local glob = fio.pathjoin(cg.server.workdir, '*.xlog') | ||
local xlog = fio.glob(glob) | ||
for _, file in pairs(xlog) do fio.unlink(file) end | ||
--Copying the prepared log to the server | ||
xlog = 'test/replication-luatest/gh_7932_data/not_finished_xlog/'.. | ||
'00000000000000000000.xlog' | ||
fio.copyfile(xlog, cg.server.workdir) | ||
|
||
cg.server.box_cfg.force_recovery = true | ||
cg.server:start() | ||
local logfile = fio.pathjoin(cg.server.workdir, 'server.log') | ||
|
||
-- The second transaction is not completed | ||
t.helpers.retrying({}, function() | ||
t.assert(cg.server:grep_log("XlogError", nil, | ||
{filename = logfile}), "found a not finished transaction " .. | ||
"in the log") | ||
end) | ||
|
||
-- Only the first transaction is applied | ||
t.assert_equals(cg.server:exec(function() | ||
return box.space.test:select() | ||
end), {{1, 3}, {2, 3}, {3, 3}}) | ||
end |