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.

Test Plan: main.db_read_only

Reviewers: ebergen, santoshb, pengt

Reviewed By: pengt
  • Loading branch information
tianx authored and jtolmer committed Jan 5, 2016
1 parent 7367a3e commit 8194409
Show file tree
Hide file tree
Showing 27 changed files with 1,631 additions and 18 deletions.
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;
Loading

0 comments on commit 8194409

Please sign in to comment.