Repository: tarantool/tarantool
Pull request #11314: box: fix box.commit({wait = 'none'}) could yield
Author: login: nshy
Labels: name: full-ci
description: Enables all tests for a pull request
name: backport/3.2
description: Automatically create a 3.2 backport PR
name: backport/3.3
description: Automatically create a 3.3 backport PR
Created: 2025-03-28T14:47:00Z | Merged: 2025-04-10T07:49:57Z

The issue is in order to decide whether submitting request to journal would yield or not we check the journal queue size. But the size can be less then limit while queue is not empty. At this moment submitting new request will yield.

Closes #11224

Linked issue #11224: box.commit(wait = 'none') may yield
**Bug description**

`box.commit(wait = 'none')` may yield while is not expected to. The issue on commit we check that queue size is less then limit but there is a gap when we decrease queue size and when waiting fibers are actually poped off the queue.
 
Found in `3.4.0-entrypoint-158-g904e0a702e`.

**Steps to reproduce**

Reproducer script:
```lua
local fiber = require('fiber')
os.execute('rm -f *.snap *.xlog *.vylog')
box.cfg{log_level = 'warn', wal_queue_max_size = 100}
local s = box.schema.create_space('test')
s:create_index('pk')
fiber.create(function()
    box.begin()
    box.on_commit(function()
        fiber.new(function()
            box.begin()
            s:insert({3})
            print(fiber.self().info().csw)
            box.commit({wait = 'none'})
            print(fiber.self().info().csw)
        end)
    end)
    s:insert({1, string.rep('a', 1000)})
    box.commit()
end)
s:insert({2})
os.exit()
```

**Actual behavior**

The script prints:
```
0
1
```

**Expected behavior**

The `box.commit({wait = 'none'}) ` should fail because the txn can not be send to WAL immeditately.


Diff:
diff --git a/changelogs/unreleased/gh-11224-fix-commit-wait-none-yield.md b/changelogs/unreleased/gh-11224-fix-commit-wait-none-yield.md
new file mode 100644
index 000000000000..a5e69b860af1
--- /dev/null
+++ b/changelogs/unreleased/gh-11224-fix-commit-wait-none-yield.md
@@ -0,0 +1,3 @@
+## bugfix/box
+
+* Fixed bug when `box.commit({wait = 'none'})` could yield (gh-11224).
diff --git a/src/box/journal.c b/src/box/journal.c
index fcfb49211741..2eb8ce59270a 100644
--- a/src/box/journal.c
+++ b/src/box/journal.c
@@ -94,7 +94,7 @@ void
 journal_queue_wakeup(void)
 {
 	if (!stailq_empty(&journal_queue.requests) &&
-	    !journal_queue_is_full()) {
+	    journal_queue.size < journal_queue.max_size) {
 		struct journal_entry *req =
 				stailq_first_entry(&journal_queue.requests,
 						   typeof(*req), fifo);
@@ -105,7 +105,7 @@ journal_queue_wakeup(void)
 int
 journal_queue_wait(struct journal_entry *entry)
 {
-	if (!journal_queue_is_full() &&
+	if (journal_queue.size < journal_queue.max_size &&
 	    stailq_empty(&journal_queue.requests))
 		return 0;
 	int rc = -1;
diff --git a/src/box/journal.h b/src/box/journal.h
index d70b861151a1..a2cba85b84eb 100644
--- a/src/box/journal.h
+++ b/src/box/journal.h
@@ -187,17 +187,6 @@ struct journal {
 void
 journal_queue_wakeup(void);
 
-/**
- * Check whether any of the queue size limits is reached.
- * If the queue is full, we must wait for some of the entries to be written
- * before proceeding with a new asynchronous write request.
- */
-static inline bool
-journal_queue_is_full(void)
-{
-	return journal_queue.size >= journal_queue.max_size;
-}
-
 /** Yield until there's some space in the journal queue. */
 int
 journal_queue_wait(struct journal_entry *entry);
@@ -254,6 +243,14 @@ extern struct journal *current_journal;
 int
 journal_write_row(struct xrow_header *row);
 
+/** Checks whether journal_write_submit() call will yield. */
+static inline bool
+journal_queue_would_block(void)
+{
+	return journal_queue.size > journal_queue.max_size ||
+	       !stailq_empty(&journal_queue.requests);
+}
+
 /**
  * Queue a single entry to the journal in asynchronous way.
  *
diff --git a/src/box/txn.c b/src/box/txn.c
index 1bccd296dd27..f08884abe66b 100644
--- a/src/box/txn.c
+++ b/src/box/txn.c
@@ -1162,7 +1162,7 @@ txn_commit_impl(struct txn *txn, enum txn_commit_wait_mode wait_mode)
 			goto rollback_abort;
 		});
 		if (wait_mode == TXN_COMMIT_WAIT_MODE_NONE &&
-		    journal_queue_is_full()) {
+		    journal_queue_would_block()) {
 			diag_set(ClientError, ER_WAL_QUEUE_FULL);
 			goto rollback_abort;
 		}
diff --git a/test/box-luatest/gh_11224_fix_commit_wait_none_journal_queue_test.lua b/test/box-luatest/gh_11224_fix_commit_wait_none_journal_queue_test.lua
new file mode 100644
index 000000000000..2c878ae43572
--- /dev/null
+++ b/test/box-luatest/gh_11224_fix_commit_wait_none_journal_queue_test.lua
@@ -0,0 +1,60 @@
+local server = require('luatest.server')
+local t = require('luatest')
+
+local g = t.group()
+
+g.before_all(function(cg)
+    cg.server = server:new()
+    cg.server:start()
+end)
+
+g.after_all(function(cg)
+    cg.server:drop()
+end)
+
+g.after_test('test_box_commit_non_empty_journal_queue', function(cg)
+    cg.server:exec(function()
+        box.cfg{wal_queue_max_size = box.NULL}
+    end)
+end)
+
+g.test_box_commit_non_empty_journal_queue = function(cg)
+    cg.server:exec(function()
+        local fiber = require('fiber')
+        local s = box.schema.create_space('test')
+        s:create_index('pk')
+        box.cfg{wal_queue_max_size = 100}
+
+        local yielded
+        local commit_ok
+        local commit_err
+        fiber.create(function()
+            box.begin()
+            box.on_commit(function()
+                -- 3. When 1. is written to WAL queue size is decreased
+                -- but 2. is still in queue so `wait = 'none'` should fail.
+                fiber.create(function()
+                    box.begin()
+                    s:insert({3})
+                    local csw = fiber.self().info().csw
+                    commit_ok, commit_err = pcall(box.commit, {wait = 'none'})
+                    yielded = fiber.self().info().csw ~= csw
+                end)
+            end)
+            --- 1. Insert large tuple to block journal queue.
+            s:insert({1, string.rep('a', 1000)})
+            box.commit()
+        end)
+        -- 2. Next insertion will wait in journal queue.
+        s:insert({2})
+
+        t.assert_not(yielded)
+        t.assert_not(commit_ok)
+        t.assert_covers(commit_err:unpack(), {
+            type = 'ClientError',
+            code = box.error.WAL_QUEUE_FULL,
+            message = "The WAL queue is full",
+        })
+        t.assert_equals(s:select({1}, {iterator = 'gt'}), {{2}})
+    end)
+end


Reviews:
user: login: sergepetrenko
body: Thanks for finding and fixing this!
LGTM.
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-01T10:05:40Z
user: login: maryiaLichko
state: APPROVED
author_association: NONE
submitted_at: 2025-04-02T07:54:33Z
user: login: Gerold103
body: Thanks for the patch! Huh, it was really an unexpected bug in such a simple place!
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-02T18:19:03Z
user: login: nshy
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-04T09:57:08Z
user: login: nshy
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-04T10:22:24Z
user: login: Gerold103
body: Thank you, @nshy! The test is especially clean and nice, I like it ❤️.
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-07T20:31:42Z

Review comments:
diff_hunk: @@ -0,0 +1,60 @@
+local server = require('luatest.server')
path: test/box-luatest/gh-11224_fix_commit_wait_none_journal_queue_test.lua
user: login: sergepetrenko
body: Nit: we usually name tests starting with `gh_`, not `gh-`.

created_at: 2025-04-01T10:05:35Z
author_association: COLLABORATOR
diff_hunk: @@ -188,19 +188,12 @@ void
 journal_queue_wakeup(void);
 
 /**
- * Check whether any of the queue size limits is reached.
- * If the queue is full, we must wait for some of the entries to be written
- * before proceeding with a new asynchronous write request.
+ * Yield until there's some space in the journal queue.
+ *
+ * If no_wait is true then fail if request has to yield.
  */
-static inline bool
-journal_queue_is_full(void)
-{
-	return journal_queue.size >= journal_queue.max_size;
-}
-
-/** Yield until there's some space in the journal queue. */
 int
-journal_queue_wait(struct journal_entry *entry);
+journal_queue_wait(struct journal_entry *entry, bool no_wait);
path: src/box/journal.h
user: login: Gerold103
body: The API of sort `wait(bool no_wait)` looks not really good. Please, lets find a way to check for the potential waiting explicitly in the calling code.

Like add a method like `journal_queue_would_block()` which checks for the size + request queue being non-empty.

Alternatively I could suggest to add a new enum for `journal.h` like `enum journal_submit_mode { JOURNAL_SUBMIT_MODE_WAIT, JOURNAL_SUBMIT_MODE_NO_WAIT; }` instead of a flag and you would then translate txn wait mode into the journal wait mode. This magic true/false in the code is really bothering.
created_at: 2025-04-02T18:09:31Z
author_association: COLLABORATOR
diff_hunk: @@ -188,19 +188,12 @@ void
 journal_queue_wakeup(void);
 
 /**
- * Check whether any of the queue size limits is reached.
- * If the queue is full, we must wait for some of the entries to be written
- * before proceeding with a new asynchronous write request.
+ * Yield until there's some space in the journal queue.
+ *
+ * If no_wait is true then fail if request has to yield.
  */
-static inline bool
-journal_queue_is_full(void)
-{
-	return journal_queue.size >= journal_queue.max_size;
-}
-
-/** Yield until there's some space in the journal queue. */
 int
-journal_queue_wait(struct journal_entry *entry);
+journal_queue_wait(struct journal_entry *entry, bool no_wait);
path: src/box/journal.h
user: login: nshy
body: These solutions looks more heavy to me, especially the second. So I'll try the first one.
created_at: 2025-04-04T09:57:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,60 @@
+local server = require('luatest.server')
path: test/box-luatest/gh-11224_fix_commit_wait_none_journal_queue_test.lua
user: login: nshy
body: Fixed.
created_at: 2025-04-04T10:22:24Z
author_association: CONTRIBUTOR

PR comments:
user: login: coveralls
created_at: 2025-03-28T15:05:17Z
body: 
[![Coverage Status](https://coveralls.io/builds/73080098/badge)](https://coveralls.io/builds/73080098)

coverage: 87.471% (-0.02%) from 87.486%
when pulling **786004b3161ed86206c8327de9bf472d4d04f071 on nshy:gh-11224-fix-box-commit-wait-none**
into **061d4bb791efe3781ebbd9e1e3bf390f41b1e82a
 on tarantool:master**.

author_association: NONE
user: login: sergepetrenko
created_at: 2025-04-01T10:09:05Z
body: Let's also ask @Gerold103 to take a look.
author_association: COLLABORATOR
user: login: TarantoolBot
created_at: 2025-04-10T07:50:20Z
body: Successfully created backport PR for `release/3.2`:
- #11362
author_association: COLLABORATOR
user: login: TarantoolBot
created_at: 2025-04-10T07:50:25Z
body: Successfully created backport PR for `release/3.3`:
- #11363
author_association: COLLABORATOR
user: login: TarantoolBot
created_at: 2025-04-10T07:50:27Z
body: ### Backport summary

* Created #11362 to `release/3.2` to a future `3.2.2` release
* Created #11363 to `release/3.3` to a future `3.3.2` release
author_association: COLLABORATOR