From b4304df7f16dc751dbd5ffe8acdb4ec51fa41584 Mon Sep 17 00:00:00 2001
From: Vladimir Davydov <vdavydov@tarantool.org>
Date: Tue, 24 Sep 2024 15:20:29 +0300
Subject: [PATCH] vinyl: fix crash when empty PK DDL races with DML

Vinyl doesn't support altering the primary index of a non-empty space,
but the check forbidding this isn't entirely reliable - the DDL function
may yield to wait for pending WAL writes to finish after ensuring that
the space doesn't contain any tuples. If a new tuples is inserted into
the space in the meantime, the DDL operation will proceed rebuilding
the primary index and trigger a crash because the code is written on
the assumption that it's rebuilding a secondary index:

```
./src/box/vinyl.c:1572: vy_check_is_unique_secondary_one: Assertion `lsm->index_id > 0' failed.
```

Let's fix this by moving the check after syncing on WAL.

Closes #10603

NO_DOC=bug fix

(cherry picked from commit 955537b57c2aade58b7ca42501a9bbe50dd91f26)
---
 .../gh-10603-fix-vinyl-pk-alter-crash.md      |  4 +
 src/box/vinyl.c                               | 12 +--
 .../gh_10603_pk_alter_vs_dml_test.lua         | 92 +++++++++++++++++++
 3 files changed, 102 insertions(+), 6 deletions(-)
 create mode 100644 changelogs/unreleased/gh-10603-fix-vinyl-pk-alter-crash.md
 create mode 100644 test/vinyl-luatest/gh_10603_pk_alter_vs_dml_test.lua

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 0000000000..a5fc88e8d8
--- /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 27e6708a89..ebe59d4c0a 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 0000000000..a6fdf28d88
--- /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
-- 
GitLab