Skip to content

Commit 71fae82

Browse files
MDEV-39101 Make BACKUP SERVER mutually exclusive with itself and BACKUP STAGE
BACKUP SERVER acquires backup lock before starting the backup. This prevents it from running concurrently with another instance of BACKUP SERVER or BACKUP STAGE, blocking when a backup is running concurrently in another connection. If BACKUP SERVER is run on a connection on which BACKUP STAGE has been started, it will fail with an error.
1 parent 39aab26 commit 71fae82

6 files changed

Lines changed: 198 additions & 15 deletions

File tree

mysql-test/main/backup_server_locking.result

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,31 @@
1+
connect blocking,localhost,root,,test;
2+
SET DEBUG_SYNC='after_backup_server_lock_acquired SIGNAL blocking WAIT_FOR unblock';
3+
SET DEBUG_SYNC='backup_server_finalizing SIGNAL finalizing WAIT_FOR finish';
4+
BACKUP SERVER TO '<MYSQLTEST_VARDIR>/some_directory';
5+
connect blocked,localhost,root,,test;
6+
SET DEBUG_SYNC='now WAIT_FOR blocking';
7+
BACKUP SERVER TO '<MYSQLTEST_VARDIR>/other_directory';
8+
connection default;
9+
SET @blocked_conn_id = $blocked_conn_id;
10+
SELECT COMMAND, STATE, INFO FROM INFORMATION_SCHEMA.PROCESSLIST
11+
WHERE ID = @blocked_conn_id;
12+
COMMAND STATE INFO
13+
Query Waiting for backup lock BACKUP SERVER TO '<MYSQLTEST_VARDIR>/other_directory'
14+
SET DEBUG_SYNC='now SIGNAL unblock';
15+
SET DEBUG_SYNC='now WAIT_FOR finalizing';
16+
connection blocked;
17+
SELECT COUNT(*) FROM INFORMATION_SCHEMA.PROCESSLIST
18+
WHERE Info LIKE 'BACKUP SERVER TO %' AND State = 'Waiting for backup lock';
19+
COUNT(*)
20+
0
21+
BACKUP SERVER TO '$MYSQLTEST_VARDIR/another_directory';
22+
disconnect blocked;
23+
connection default;
24+
SET DEBUG_SYNC='now SIGNAL finish';
25+
connection blocking;
26+
disconnect blocking;
27+
connection default;
28+
SET DEBUG_SYNC='RESET';
129
BACKUP SERVER TO '$MYSQLTEST_VARDIR/some_directory';
230
ERROR HY000: Can't create directory 'MYSQLTEST_VARDIR/some_directory' (Errcode: 17 "File exists")
331
BACKUP STAGE START;
@@ -15,3 +43,19 @@ BACKUP SERVER TO '$MYSQLTEST_VARDIR/some_directory';
1543
ERROR HY000: Can't create directory 'MYSQLTEST_VARDIR/some_directory' (Errcode: 17 "File exists")
1644
disconnect backup;
1745
connection default;
46+
BACKUP STAGE START;
47+
connect con1,localhost,root,,test;
48+
BACKUP SERVER TO '<MYSQLTEST_VARDIR>/some_directory';
49+
connection default;
50+
SET @con1_id = <con1_id>;
51+
SELECT COMMAND, STATE, INFO FROM INFORMATION_SCHEMA.PROCESSLIST WHERE ID = @con1_id;
52+
COMMAND STATE INFO
53+
Query Waiting for backup lock BACKUP SERVER TO '<MYSQLTEST_VARDIR>/some_directory'
54+
BACKUP STAGE END;
55+
connection con1;
56+
disconnect con1;
57+
connection default;
58+
BACKUP STAGE START;
59+
BACKUP SERVER TO '$MYSQLTEST_VARDIR/some_directory';
60+
ERROR HY000: Can't execute the command as you have a BACKUP STAGE active
61+
BACKUP STAGE END;

mysql-test/main/backup_server_locking.test

Lines changed: 121 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,88 @@
1+
# BACKUP SERVER acquires Bacup Lock, disallowing concurrent backup either
2+
# by another instance of BACKUP SERVER or using BACKUp STAGE.
3+
4+
# Using DEBUG_SYNC facility in debug configuration we directly test
5+
# that while one instance of BACKUP SERVER has already acquired the
6+
# backup lock, a second instance running on another connection will
7+
# block waiting on that lock. BACKUP SERVER releases the lock before
8+
# it returns, when it reaches its finalization stage, as actions
9+
# taken in that stage affect only the backup directory and may be
10+
# performed concurrently with other backup processes.
11+
112
--source include/not_embedded.inc
2-
--source include/count_sessions.inc
13+
--source include/maybe_debug.inc
14+
if ($have_debug) {
15+
16+
--source include/have_debug_sync.inc
17+
--connect (blocking,localhost,root,,test)
18+
# Simulate long-running backup by blocking it on a debug sync point
19+
SET DEBUG_SYNC='after_backup_server_lock_acquired SIGNAL blocking WAIT_FOR unblock';
20+
SET DEBUG_SYNC='backup_server_finalizing SIGNAL finalizing WAIT_FOR finish';
21+
--replace_result $MYSQLTEST_VARDIR <MYSQLTEST_VARDIR>
22+
--send_eval BACKUP SERVER TO '$MYSQLTEST_VARDIR/some_directory'
23+
24+
--connect (blocked,localhost,root,,test)
25+
# Wait for the "long-running" backup to lock the system
26+
SET DEBUG_SYNC='now WAIT_FOR blocking';
27+
let $blocked_conn_id= `SELECT CONNECTION_ID()`;
28+
# Attempt a backup while the other backup is running
29+
--replace_result $MYSQLTEST_VARDIR <MYSQLTEST_VARDIR>
30+
--send_eval BACKUP SERVER TO '$MYSQLTEST_VARDIR/other_directory'
31+
32+
--connection default
33+
# Check that the backup on "blocked" connection is waiting for the blocking
34+
# backup to finish
35+
evalp SET @blocked_conn_id = $blocked_conn_id;
36+
let $wait_condition=
37+
SELECT COUNT(*) = 1 FROM INFORMATION_SCHEMA.PROCESSLIST
38+
WHERE ID = @blocked_conn_id AND State = 'Waiting for backup lock';
39+
--source include/wait_condition.inc
40+
--replace_result $MYSQLTEST_VARDIR <MYSQLTEST_VARDIR>
41+
SELECT COMMAND, STATE, INFO FROM INFORMATION_SCHEMA.PROCESSLIST
42+
WHERE ID = @blocked_conn_id;
43+
44+
SET DEBUG_SYNC='now SIGNAL unblock';
45+
SET DEBUG_SYNC='now WAIT_FOR finalizing';
46+
# The blocking backup is still running, but no longer blocking
47+
48+
--connection blocked
49+
# Original blocked backup can complete now
50+
--reap
51+
# No blocked backup process at this point
52+
SELECT COUNT(*) FROM INFORMATION_SCHEMA.PROCESSLIST
53+
WHERE Info LIKE 'BACKUP SERVER TO %' AND State = 'Waiting for backup lock';
54+
# Run new BACKUP SERVER while the old one is finalizing
55+
--evalp BACKUP SERVER TO '$MYSQLTEST_VARDIR/another_directory'
56+
57+
--disconnect blocked
58+
59+
--connection default
60+
SET DEBUG_SYNC='now SIGNAL finish';
61+
62+
--connection blocking
63+
--reap
64+
--disconnect blocking
65+
66+
--connection default
67+
SET DEBUG_SYNC='RESET';
68+
69+
--rmdir $MYSQLTEST_VARDIR/some_directory
70+
--rmdir $MYSQLTEST_VARDIR/other_directory
71+
--rmdir $MYSQLTEST_VARDIR/another_directory
72+
73+
}
74+
75+
# To test that BACKUP SERVER blocks itself on a release server we rely
76+
# on timing: a BACKUP SERVER set up to fail because the target directory
77+
# exists will fail with error code 21 if it is not blocked, but if
78+
# blocked will time out if max_statement_time is sufficiently short.
79+
#
80+
# This is a heuristic test that might fail depending on how long it takes
81+
# the initial backup to fail; it may give false positives or false
82+
# negatives, and does not test that the lcok is released in the
83+
# finalization phase, but it may uncover bugs that the DEBUG_SYNC test
84+
# would miss.
85+
386

487
--mkdir $MYSQLTEST_VARDIR/some_directory
588
--replace_result $MYSQLTEST_VARDIR MYSQLTEST_VARDIR
@@ -28,4 +111,40 @@ BACKUP SERVER TO '$MYSQLTEST_VARDIR/some_directory';
28111

29112
--rmdir $MYSQLTEST_VARDIR/some_directory
30113

31-
--source include/wait_until_count_sessions.inc
114+
# Test that BACKUP SERVER blocks when a BACKUP STAGE process is in progress
115+
116+
BACKUP STAGE START;
117+
118+
--connect (con1,localhost,root,,test)
119+
let $con1_id= `SELECT CONNECTION_ID()`;
120+
--replace_result $MYSQLTEST_VARDIR <MYSQLTEST_VARDIR>
121+
--send_eval BACKUP SERVER TO '$MYSQLTEST_VARDIR/some_directory'
122+
123+
--connection default
124+
--replace_result $con1_id <con1_id>
125+
eval SET @con1_id = $con1_id;
126+
let $wait_condition=
127+
SELECT COUNT(*) = 1 FROM INFORMATION_SCHEMA.PROCESSLIST
128+
WHERE ID = @con1_id AND State = 'Waiting for backup lock';
129+
--source include/wait_condition.inc
130+
--replace_result $MYSQLTEST_VARDIR <MYSQLTEST_VARDIR>
131+
SELECT COMMAND, STATE, INFO FROM INFORMATION_SCHEMA.PROCESSLIST WHERE ID = @con1_id;
132+
133+
BACKUP STAGE END;
134+
135+
# BACKUP SERVER is no longer blocked
136+
137+
--connection con1
138+
--reap
139+
--disconnect con1
140+
141+
--connection default
142+
--rmdir $MYSQLTEST_VARDIR/some_directory
143+
144+
# Test that BACKUP SERVER will fail when ran on a connection which is
145+
# between BACKUP STAGE START and BACKUP STAGE END
146+
147+
BACKUP STAGE START;
148+
--error ER_BACKUP_LOCK_IS_ACTIVE
149+
--evalp BACKUP SERVER TO '$MYSQLTEST_VARDIR/some_directory'
150+
BACKUP STAGE END;

sql/handler.h

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1900,12 +1900,13 @@ struct handlerton : public transaction_participant
19001900

19011901
/**
19021902
Start of BACKUP SERVER: collect all files to be backed up
1903-
@param thd current session
1904-
@param target target directory
1903+
@param thd current session
1904+
@param target target directory
1905+
@param backup_lock backup lock held by the backup
19051906
@return error code
19061907
@retval 0 on success
19071908
*/
1908-
int (*backup_start)(THD *thd, IF_WIN(const char*,int) target);
1909+
int (*backup_start)(THD *thd, IF_WIN(const char*,int) target, MDL_ticket *backup_lock);
19091910
/**
19101911
Process a file that was collected in backup_start().
19111912
@param thd current session

sql/sql_backup.cc

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,20 @@
2020
#include "sql_backup.h"
2121
#include "sql_parse.h"
2222

23-
static my_bool backup_start(THD *thd, plugin_ref plugin, void *dst) noexcept
23+
namespace {
24+
struct start_arg_t
25+
{
26+
IF_WIN(const char*,int) target;
27+
MDL_ticket *backup_lock;
28+
};
29+
}
30+
31+
static my_bool backup_start(THD *thd, plugin_ref plugin, void *arg) noexcept
2432
{
2533
handlerton *hton= plugin_hton(plugin);
34+
start_arg_t *start_arg= static_cast<start_arg_t *>(arg);
2635
if (hton->backup_start)
27-
return hton->backup_start(thd,
28-
IF_WIN(static_cast<const char*>(dst),
29-
int(reinterpret_cast<uintptr_t>(dst))));
36+
return hton->backup_start(thd, start_arg->target, start_arg->backup_lock);
3037
return false;
3138
}
3239

@@ -79,6 +86,8 @@ bool Sql_cmd_backup::execute(THD *thd)
7986
thd->variables.lock_wait_timeout))
8087
return true;
8188

89+
DEBUG_SYNC(thd, "after_backup_server_lock_acquired");
90+
8291
if (my_mkdir(target.str, 0755, MYF(MY_WME)))
8392
{
8493
#ifndef _WIN32
@@ -97,11 +106,13 @@ bool Sql_cmd_backup::execute(THD *thd)
97106
}
98107
#endif
99108

109+
start_arg_t start_arg{IF_WIN(target.str, dir), mdl_request.ticket};
110+
100111
bool fail= plugin_foreach_with_mask(thd, backup_start,
101112
MYSQL_STORAGE_ENGINE_PLUGIN,
102113
PLUGIN_IS_DELETED|PLUGIN_IS_READY,
103-
IF_WIN(const_cast<char*>(target.str),
104-
reinterpret_cast<void*>(dir)));
114+
static_cast<void*>(&start_arg));
115+
105116
if (!fail)
106117
fail= plugin_foreach_with_mask(thd, backup_step,
107118
MYSQL_STORAGE_ENGINE_PLUGIN,
@@ -115,6 +126,9 @@ bool Sql_cmd_backup::execute(THD *thd)
115126
/* The final part will not interfere with the use of the server datadir.
116127
Release the locks. */
117128
thd->mdl_context.release_lock(mdl_request.ticket);
129+
130+
DEBUG_SYNC(thd, "backup_server_finalizing");
131+
118132
plugin_foreach_with_mask(thd, backup_finalize, MYSQL_STORAGE_ENGINE_PLUGIN,
119133
PLUGIN_IS_DELETED|PLUGIN_IS_READY, nullptr);
120134
#ifndef _WIN32

storage/innobase/handler/backup_innodb.cc

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -431,7 +431,9 @@ class InnoDB_backup
431431
static InnoDB_backup backup;
432432
}
433433

434-
int innodb_backup_start(THD *thd, IF_WIN(const char*,int) target) noexcept
434+
int innodb_backup_start(THD *thd,
435+
IF_WIN(const char*,int) target,
436+
[[maybe_unused]] MDL_ticket *backup_lock) noexcept
435437
{
436438
return backup.init(thd, target);
437439
}

storage/innobase/handler/backup_innodb.h

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,15 @@
1515

1616
/**
1717
Start of BACKUP SERVER: collect all files to be backed up
18-
@param thd current session
19-
@param target target directory
18+
@param thd current session
19+
@param target target directory
20+
@param backup_lock backup lock held by BACKUP SERVER
2021
@return error code
2122
@retval 0 on success
2223
*/
23-
int innodb_backup_start(THD *thd, IF_WIN(const char*,int) target) noexcept;
24+
int innodb_backup_start(THD *thd,
25+
IF_WIN(const char*,int) target,
26+
MDL_ticket *backup_lock) noexcept;
2427

2528
/**
2629
Process a file that was collected in backup_start().

0 commit comments

Comments
 (0)