Skip to content

Commit

Permalink
Per Database Read-Only
Browse files Browse the repository at this point in the history
Summary:
Implements per-database scope read-only setting. The read-only can be
turned on in `alter database` statement. Privilege to set read-only
follows alter-database ACL.

  ALTER DATABASE db_name [SUPER_READ_ONLY | READ_ONLY = FALSE | TRUE]]

`SUPER_READ_ONLY = TRUE` will prevent all write transactions from
committing for any user (including super users).

`READ_ONLY = TRUE` will prevent any write transaction from committing
for regular users, while super users can still write to the database.

`SUPER_READ_ONLY = FALSE` will turn off super_read_only on the database,
and the database remains on read_only (for regular users),

`READ_ONLY = FALSE` will turn off read_only on the database,

The READ_ONLY database status can be shown by `show create database`.

  mysql> alter database test read_only = true;
  Query OK, 1 row affected (0.00 sec)

  mysql> show create database test;
  +----------+---------------------------------------------------------------------------+
  | Database | Create Database                                                           |
  +----------+---------------------------------------------------------------------------+
  | test     | CREATE DATABASE `test` /*!40100 DEFAULT CHARACTER SET latin1 READ_ONLY */ |
  +----------+---------------------------------------------------------------------------+
  1 row in set (0.00 sec)

  mysql> alter database test super_read_only = true;
  Query OK, 1 row affected (0.00 sec)

  mysql> show create database test;
  +----------+---------------------------------------------------------------------------------+
  | Database | Create Database                                                                 |
  +----------+---------------------------------------------------------------------------------+
  | test     | CREATE DATABASE `test` /*!40100 DEFAULT CHARACTER SET latin1 SUPER_READ_ONLY */ |
  +----------+---------------------------------------------------------------------------------+
  1 row in set (0.00 sec)

  mysql> alter database test super_read_only = false;
  Query OK, 1 row affected (0.00 sec)

  mysql> show create database test;
  +----------+---------------------------------------------------------------------------+
  | Database | Create Database                                                           |
  +----------+---------------------------------------------------------------------------+
  | test     | CREATE DATABASE `test` /*!40100 DEFAULT CHARACTER SET latin1 READ_ONLY */ |
  +----------+---------------------------------------------------------------------------+
  1 row in set (0.00 sec)

Details:

  * READ_ONLY flag is persisted in db.ops and survives restarts.

  * Alter database will only return when no write transaction is
    still committing. Uncommitted write transactions will fail after
    alter database succeeds.

  * DB options are stored in each thread (using hash map), so checking
    the read_only options doesn't grab a global lock. Only db_read_only
    that is turned on is stored in the local hash map.

  * The first time thead local hash map is initialized (once), shared db
    option map will be locked and accessed. Since the db options are not
    automatically loaded into the shared hash map (cache) at the
    beginning, we have to explicitly load the db options during the
    first time any thread initializes its local hash map.

  * create/alter/delete database DDLs will iterate through thread array
    to update local db opt (if needed), similar to how show processlist
    works.

  * write transactions are checked against local hashmap at commit time
    (either explicit commit or auto-commit). This will make the explicit
    commit and auto-commits consistent, and avoid problems such that
    explicit commit may become a "read-only" transaction if we block
    write-statements at the parsing time.

  * The list of databases accessed in a write transaction is obtained
    through the metadata locks (MDL) that the transaction holds. As I
    noted in the code, this may rollback some false positives, such as
    the read-only DB in the list may not actually be modified in the
    transaction (for cross-db transactions). We do not differentiate
    such cases.

Reference patch:
facebook@8194409
facebook@c971129
facebook@e26e9b2
facebook@a9f6f3d
facebook@b1ff9a6
facebook@548c7ca

Differential Revision: D9379904
  • Loading branch information
tianx authored and inikep committed Aug 6, 2024
1 parent 3afdc43 commit b357626
Show file tree
Hide file tree
Showing 45 changed files with 1,453 additions and 97 deletions.
56 changes: 28 additions & 28 deletions mysql-test/include/commit.inc
Original file line number Diff line number Diff line change
Expand Up @@ -673,7 +673,7 @@ begin
end|
delimiter ;|
--disable_warnings
call p_verify_status_increment(6, 4, 2, 0);
call p_verify_status_increment(7, 4, 2, 0);
--enable_warnings

--echo # 16. A function changes non-trans-table.
Expand Down Expand Up @@ -731,14 +731,14 @@ create temporary table t2 (a int);
let $have_debug = `SELECT VERSION() LIKE '%debug%'`;
if (!$have_debug)
{
--replace_regex /8/X/
call p_verify_status_increment(8, 0, 0, 0);
--replace_regex /9/X/
call p_verify_status_increment(9, 0, 0, 0);
}

if ($have_debug)
{
--replace_regex /9/X/
call p_verify_status_increment(9, 0, 0, 0);
--replace_regex /10/X/
call p_verify_status_increment(10, 0, 0, 0);
}
--enable_warnings
set sql_mode=default;
Expand Down Expand Up @@ -878,13 +878,13 @@ create table t2 (a int);
# COM_PREPARE.
if (`SELECT $PS_PROTOCOL = 0`)
{
--replace_regex /11/<commit_count>/
call p_verify_status_increment(11, 4, 6, 0);
--replace_regex /12/<commit_count>/
call p_verify_status_increment(12, 4, 6, 0);
}
if (`SELECT $PS_PROTOCOL > 0`)
{
--replace_regex /12/<commit_count>/
call p_verify_status_increment(12, 4, 6, 0);
--replace_regex /13/<commit_count>/
call p_verify_status_increment(13, 4, 6, 0);
}
--echo # One extra transaction to load table 't2' into data-dictionary cache.
do (select f1() from t1 where a=2);
Expand Down Expand Up @@ -938,48 +938,48 @@ create table t3 select a from t2;
# COM_PREPARE.
if (`SELECT $PS_PROTOCOL = 0`)
{
--replace_regex /12/<commit_count>/
call p_verify_status_increment(12, 4, 4, 4);
--replace_regex /13/<commit_count>/
call p_verify_status_increment(13, 4, 4, 4);
}
if (`SELECT $PS_PROTOCOL > 0`)
{
--replace_regex /13/<commit_count>/
call p_verify_status_increment(13, 4, 4, 4);
--replace_regex /14/<commit_count>/
call p_verify_status_increment(14, 4, 4, 4);
}
alter table t3 add column (b int);

if (!$have_debug)
{
--replace_regex /20/X/
call p_verify_status_increment(20, 4, 2, 0);
--replace_regex /21/X/
call p_verify_status_increment(21, 4, 2, 0);
}

if ($have_debug)
{
--replace_regex /21/X/
call p_verify_status_increment(21, 4, 2, 0);
--replace_regex /22/X/
call p_verify_status_increment(22, 4, 2, 0);
}

alter table t3 rename t4;
call p_verify_status_increment(20, 4, 3, 0);
call p_verify_status_increment(21, 4, 3, 0);
rename table t4 to t3;
call p_verify_status_increment(24, 4, 5, 0);
call p_verify_status_increment(25, 4, 5, 0);
truncate table t3;

if (!$have_debug)
{
--replace_regex /10/X/
call p_verify_status_increment(10, 4, 4, 0);
--replace_regex /12/X/
call p_verify_status_increment(12, 4, 4, 0);
}

if ($have_debug)
{
--replace_regex /11/X/
call p_verify_status_increment(11, 4, 4, 0);
--replace_regex /13/X/
call p_verify_status_increment(13, 4, 4, 0);
}

create view v1 as select * from t2;
call p_verify_status_increment(7, 4, 3, 0);
call p_verify_status_increment(8, 4, 3, 0);
check table t1;
call p_verify_status_increment(2, 0, 2, 0);
--echo # Sic: after this bug is fixed, CHECK leaves no pending transaction
Expand All @@ -995,14 +995,14 @@ drop view v1;
--echo # One extra transaction to load v1 into data-dictionary cache.
if (!$have_debug)
{
--replace_regex /\(6/(X/
call p_verify_status_increment(6, 4, 3, 0);
--replace_regex /\(7/(X/
call p_verify_status_increment(7, 4, 3, 0);
}

if ($have_debug)
{
--replace_regex /7/X/
call p_verify_status_increment(7, 4, 3, 0);
--replace_regex /8/X/
call p_verify_status_increment(8, 4, 3, 0);
}

--enable_warnings
Expand Down
31 changes: 31 additions & 0 deletions mysql-test/include/db_read_only_off.inc
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
truncate t1;
truncate t2;

begin;
insert into t1 values (0), (1), (2);
insert into t2 values (0), (1), (2);
update t1 set a = a + 1;
# multiple table updates
update t1, t2 set t1.a=t1.a+1, t2.a=t2.a*2;
select 't1', a from t1;
select 't2', a from t2;
commit;

alter table t1 add key (a);
describe t1;
drop index a on t1;

alter table t2 add key (a);
describe t2;
drop index a on t2;

truncate t1;
truncate t2;

--echo # populate some data
begin;
insert into t1 values (1), (2), (3);
insert into t2 values (1), (2), (3);
select 't1', a from t1;
select 't2', a from t2;
commit;
120 changes: 120 additions & 0 deletions mysql-test/include/db_read_only_on.inc
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
--echo # continue previous transaction
--echo # read_only was turned on in the middle of a transaction
--echo # new update/insert statement will be blocked immediately
update t1 set a = a + 1;
# multiple table updates
update t1, t2 set t1.a=t1.a+1, t2.a=t2.a*2;
select 't1', a from t1;
select 't2', a from t2;

--echo # write transaction was rolled back at the end
--error ER_DB_READ_ONLY
commit;

# insert/update rolled back
select 't1', a from t1;
select 't2', a from t2;

--echo # write transaction with 'begin'
begin;
insert into t1 values (4), (5), (6);
update t1 set a = a + 1;
# creating a table DDL is failed immdiately
--error ER_DB_READ_ONLY
create table t3 (a int) engine=innodb;
select a from t1;
--error ER_DB_READ_ONLY
commit;

--echo # read-only transactions are ok
begin;
select count(*) from t1;
select count(*) from t2;
commit;

--echo # transaction without 'begin'
insert into t1 values (4), (5), (6);
insert into t1 values (7), (8), (9);
select a from t1;
--error ER_DB_READ_ONLY
commit;
select a from t1;

--echo # rolled-back transaction
insert into t1 values (4), (5), (6);
insert into t1 values (7), (8), (9);
select a from t1;
rollback;
select a from t1;

set autocommit = 1;
--echo # multiple table updates (autocommit)
--error ER_DB_READ_ONLY
update t1, t2 set t1.a=t1.a+1, t2.a=t2.a*2;
select 't1', a from t1;
select 't2', a from t2;
set autocommit = 0;

# table update DDL is blocked
--error ER_DB_READ_ONLY
alter table t1 add key (a);

# table update DDL is blocked
--error ER_DB_READ_ONLY
create index a on t1 (a);

# drop table is not allowed
--error ER_DB_READ_ONLY
drop table t1;

# drop database is not allowed
--error ER_DB_READ_ONLY
drop database test;

--echo #
--echo # OK to create temporary table
--echo #
create temporary table temp1 (a int);
insert into temp1 select * from t1;
update temp1 set a = a + 1;
select * from temp1;
drop temporary table temp1;

--echo #
--echo # OK to switch and write another database
--echo # read_only scope is per database
--echo #
create database test2;
use test2;
show create database test2;
create table t1 (a int) engine = innodb;
insert into t1 values (0), (1), (2);
update t1 set a = a + 1;
select a from t1;

--echo #
--echo # cross-db/noncurrent-db transaction
--echo # Transaction writing to test db from session of test2 db
--echo #
begin;
insert into test.t1 values (4), (5), (6);
update test.t1 set a = a + 1;
select a from test.t1;
--error ER_DB_READ_ONLY
commit;
select a from test.t1;
select a from test2.t1;

begin;
insert into test.t1 values (4), (5), (6);
update test.t1 set a = a + 1;
select a from test.t1;
update test2.t1 set a = a + 1;
select a from test2.t1;
--error ER_DB_READ_ONLY
commit;
select a from test.t1;
select a from test2.t1;

use test;
drop database test2;
8 changes: 4 additions & 4 deletions mysql-test/r/commit_1innodb.result
Original file line number Diff line number Diff line change
Expand Up @@ -576,7 +576,7 @@ begin
insert t2 set a=2;
return 2;
end|
call p_verify_status_increment(6, 4, 2, 0);
call p_verify_status_increment(7, 4, 2, 0);
SUCCESS

# 16. A function changes non-trans-table.
Expand Down Expand Up @@ -853,19 +853,19 @@ call p_verify_status_increment(X, 4, 2, 0);
SUCCESS

alter table t3 rename t4;
call p_verify_status_increment(20, 4, 3, 0);
call p_verify_status_increment(21, 4, 3, 0);
SUCCESS

rename table t4 to t3;
call p_verify_status_increment(24, 4, 5, 0);
call p_verify_status_increment(25, 4, 5, 0);
SUCCESS

truncate table t3;
call p_verify_status_increment(X, 4, 4, 0);
SUCCESS

create view v1 as select * from t2;
call p_verify_status_increment(7, 4, 3, 0);
call p_verify_status_increment(8, 4, 3, 0);
SUCCESS

check table t1;
Expand Down
2 changes: 1 addition & 1 deletion mysql-test/r/create.result
Original file line number Diff line number Diff line change
Expand Up @@ -956,7 +956,7 @@ create table t2 select sql_big_result f1,count(f2) from t1 group by f1;
show status like 'handler_read%';
Variable_name Value
Handler_read_first 1
Handler_read_key 23
Handler_read_key 24
Handler_read_last 0
Handler_read_next 0
Handler_read_prev 0
Expand Down
Loading

0 comments on commit b357626

Please sign in to comment.