diff --git a/changelogs/unreleased/gh-10603-fix-vinyl-pk-alter-crash.md b/changelogs/unreleased/gh-10603-fix-vinyl-pk-alter-crash.md new file mode 100644 index 0000000000000000000000000000000000000000..a5fc88e8d89f3809bd42fc4d66af9ab07b2295cb --- /dev/null +++ b/changelogs/unreleased/gh-10603-fix-vinyl-pk-alter-crash.md @@ -0,0 +1,4 @@ +## bugfix/vinyl + +* Fixed a bug when an attempt to alter the primary index of an empty space + triggered a crash if executed concurrently with a DML request (gh-10603). diff --git a/src/box/vinyl.c b/src/box/vinyl.c index 27e6708a8924674986ba21148c656723c86ac5d1..ebe59d4c0a5fc830688d3be5f485a2b395970a21 100644 --- a/src/box/vinyl.c +++ b/src/box/vinyl.c @@ -4294,12 +4294,6 @@ vinyl_space_build_index(struct space *src_space, struct index *new_index, struct vy_lsm *pk = vy_lsm(src_space->index[0]); struct txn *txn = in_txn(); - if (new_index->def->iid == 0 && !vy_lsm_is_empty(pk)) { - diag_set(ClientError, ER_UNSUPPORTED, "Vinyl", - "rebuilding the primary index of a non-empty space"); - return -1; - } - struct errinj *inj = errinj(ERRINJ_BUILD_INDEX, ERRINJ_INT); if (inj != NULL && inj->iparam == (int)new_index->def->iid) { diag_set(ClientError, ER_INJECTION, "build index"); @@ -4327,6 +4321,12 @@ vinyl_space_build_index(struct space *src_space, struct index *new_index, if (!need_wal_sync && vy_lsm_is_empty(pk)) return 0; /* space is empty, nothing to do */ + if (new_index->def->iid == 0) { + diag_set(ClientError, ER_UNSUPPORTED, "Vinyl", + "rebuilding the primary index of a non-empty space"); + return -1; + } + if (txn_check_singlestatement(txn, "index build") != 0) return -1; diff --git a/test/vinyl-luatest/gh_10603_pk_alter_vs_dml_test.lua b/test/vinyl-luatest/gh_10603_pk_alter_vs_dml_test.lua new file mode 100644 index 0000000000000000000000000000000000000000..a6fdf28d88a2d9f83f3f97ebb2cdbbfd0395cf11 --- /dev/null +++ b/test/vinyl-luatest/gh_10603_pk_alter_vs_dml_test.lua @@ -0,0 +1,92 @@ +local server = require('luatest.server') +local t = require('luatest') + +local g = t.group() + +g.before_all(function(cg) + t.tarantool.skip_if_not_debug() + cg.server = server:new() + cg.server:start() +end) + +g.after_all(function(cg) + cg.server:drop() +end) + +g.after_each(function(cg) + cg.server:exec(function() + box.error.injection.set('ERRINJ_WAL_DELAY', false) + if box.space.test ~= nil then + box.space.test:drop() + end + end) +end) + +g.test_empty_pk_alter = function(cg) + cg.server:exec(function() + local fiber = require('fiber') + local timeout = 10 -- seconds + + local s = box.schema.space.create('test', {engine = 'vinyl'}) + s:create_index('pk') + + local c1 = fiber.channel(1) + local f1 = fiber.new(function() + c1:get() + s:replace({1, 10}) + end) + f1:set_joinable(true) + + local c2 = fiber.channel(1) + local f2 = fiber.new(function() + c2:get() + s.index.pk:alter({parts = {2, 'unsigned'}}) + end) + f2:set_joinable(true) + + local c3 = fiber.channel(1) + local f3 = fiber.new(function() + c3:get() + s:replace({2, 20}) + end) + f3:set_joinable(true) + + -- Fiber 1 (DML) inserts a tuple into the space and blocks on WAL. + box.error.injection.set('ERRINJ_WAL_DELAY', true) + c1:put(true) + fiber.yield() + + -- Fiber 2 (DDL) waits for pending WAL writes to complete. + c2:put(true) + fiber.yield() + + -- Fiber 3 (DML) inserts a tuple into the space and blocks on WAL. + c3:put(true) + fiber.yield() + + box.error.injection.set('ERRINJ_WAL_DELAY', false) + fiber.yield() + + -- Fiber 1 (DML) completes successfully. + t.assert_equals({f1:join(timeout)}, {true}) + + -- Fiber 2 (DDL) fails because the primary index isn't empty. + t.assert_error_covers({ + type = 'ClientError', + code = box.error.UNUSUPPORTED, + message = 'Vinyl does not support ' .. + 'rebuilding the primary index of a non-empty space', + }, function() + local ok, err = f2:join(timeout) + if not ok then + error(err) + end + end) + + -- Fiber 3 (DML) completes successfully. + t.assert_equals({f3:join(timeout)}, {true}) + + t.assert_equals(box.space.test:select({}, {fullscan = true}), + {{1, 10}, {2, 20}}) + end) +end