Skip to content

Commit

Permalink
Fix bug 1486747 / 76872 (InnoDB AUTO_INCREMENT produces same value tw…
Browse files Browse the repository at this point in the history
…ice)

The issue is ha_innobase::get_auto_increment not honoring increment
and offset parameters for the first value in the interval. This
results in the reserved autoinc interval having slightly lesser bounds
than it should. ha_innobase::get_auto_increment sets the next value
after this interval as the next table autoinc value. Then its caller
handler::update_auto_increment will bump the interval start value so
that increment and offset are honored. The bumped value, while
correct, will conflict with the next autoinc value for this
table. This conflict will be resolved at the end of
ha_innobase::write_row, which will update the autoinc value according
to the actual inserted value used, but before then a parallel
inserting thread will get a duplicate key error.

Fix by pulling the next value computation function
compute_next_insert_id from handler.cc to handler.h, and calling it
from ha_innobase::get_auto_increment so that the interval is correct
there before further processing. Assert in
handler::update_auto_increment that its own rounding is a no-op in
case of InnoDB.

Add a testcase. Re-record the existing testcases to account for
ER_AUTOINC_READ_FAILED being returned instead of
ER_WARN_DATA_OUT_OF_RANGE. Moreover, out-of-range auto increment keys
insert now can fail instead of being truncated.
  • Loading branch information
laurynas-biveinis committed Sep 4, 2015
1 parent 59dc74e commit bd9d2eb
Show file tree
Hide file tree
Showing 11 changed files with 117 additions and 27 deletions.
3 changes: 2 additions & 1 deletion mysql-test/include/strict_autoinc.inc
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,12 @@ select count(*) from t1;

set auto_increment_increment=1000;
set auto_increment_offset=700;
--error ER_WARN_DATA_OUT_OF_RANGE
--error ER_WARN_DATA_OUT_OF_RANGE,ER_AUTOINC_READ_FAILED
insert into t1 values(null);
select count(*) from t1;

set @@sql_mode=@org_mode;
--error 0,ER_AUTOINC_READ_FAILED
insert into t1 values(null);
select * from t1;

Expand Down
2 changes: 1 addition & 1 deletion mysql-test/r/strict_autoinc_1myisam.result
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ count(*)
set auto_increment_increment=1000;
set auto_increment_offset=700;
insert into t1 values(null);
ERROR 22003: Out of range value for column 'a' at row 1
Got one of the listed errors
select count(*) from t1;
count(*)
0
Expand Down
5 changes: 1 addition & 4 deletions mysql-test/r/strict_autoinc_2innodb.result
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,12 @@ count(*)
set auto_increment_increment=1000;
set auto_increment_offset=700;
insert into t1 values(null);
ERROR 22003: Out of range value for column 'a' at row 1
Got one of the listed errors
select count(*) from t1;
count(*)
0
set @@sql_mode=@org_mode;
insert into t1 values(null);
Warnings:
Warning 1264 Out of range value for column 'a' at row 1
select * from t1;
a
127
drop table t1;
2 changes: 1 addition & 1 deletion mysql-test/r/strict_autoinc_3heap.result
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ count(*)
set auto_increment_increment=1000;
set auto_increment_offset=700;
insert into t1 values(null);
ERROR 22003: Out of range value for column 'a' at row 1
Got one of the listed errors
select count(*) from t1;
count(*)
0
Expand Down
22 changes: 22 additions & 0 deletions mysql-test/suite/innodb/r/bug76872.result
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
SELECT @@innodb_autoinc_lock_mode;
@@innodb_autoinc_lock_mode
1
CREATE TABLE `test` (
`test_id` int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
`testcol` varchar(256)
) ENGINE=InnoDB;
INSERT INTO `test` (`testcol`) VALUES ('aldsldjfhasjk');
SET SESSION auto_increment_increment = 2;
SET DEBUG_SYNC="ib_after_row_insert SIGNAL insert_1_ready WAIT_FOR insert_1_finish";
INSERT INTO `test` (`testcol`) VALUES ('aldsldjfhasjk');
SET SESSION auto_increment_increment = 2;
SET DEBUG_SYNC="now WAIT_FOR insert_1_ready";
SET DEBUG_SYNC="ib_before_row_insert SIGNAL insert_1_finish WAIT_FOR insert_2_finish";
INSERT INTO `test` (`testcol`) VALUES ('aldsldjfhasjk');
SET DEBUG_SYNC="now SIGNAL insert_2_finish";
SELECT * FROM test;
test_id testcol
1 aldsldjfhasjk
3 aldsldjfhasjk
5 aldsldjfhasjk
DROP TABLE test;
2 changes: 1 addition & 1 deletion mysql-test/suite/innodb/r/innodb-autoinc.result
Original file line number Diff line number Diff line change
Expand Up @@ -567,7 +567,7 @@ Variable_name Value
auto_increment_increment 65535
auto_increment_offset 65535
INSERT INTO t1 VALUES (NULL),(NULL);
ERROR 22003: Out of range value for column 't1' at row 167
ERROR HY000: Failed to read auto-increment value from storage engine
SELECT * FROM t1;
c1
1
Expand Down
38 changes: 38 additions & 0 deletions mysql-test/suite/innodb/t/bug76872.test
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#
# Test for bug #76872: InnoDB AUTO_INCREMENT produces same value twice
#
--source include/have_innodb.inc
--source include/have_debug_sync.inc
--source include/count_sessions.inc

# Should be != 0
SELECT @@innodb_autoinc_lock_mode;

CREATE TABLE `test` (
`test_id` int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
`testcol` varchar(256)
) ENGINE=InnoDB;

INSERT INTO `test` (`testcol`) VALUES ('aldsldjfhasjk');

SET SESSION auto_increment_increment = 2;
SET DEBUG_SYNC="ib_after_row_insert SIGNAL insert_1_ready WAIT_FOR insert_1_finish";
send INSERT INTO `test` (`testcol`) VALUES ('aldsldjfhasjk');
--connect(con1,localhost,root,,)
--connection con1
SET SESSION auto_increment_increment = 2;
SET DEBUG_SYNC="now WAIT_FOR insert_1_ready";
SET DEBUG_SYNC="ib_before_row_insert SIGNAL insert_1_finish WAIT_FOR insert_2_finish";
send INSERT INTO `test` (`testcol`) VALUES ('aldsldjfhasjk');
--connection default
reap;
SET DEBUG_SYNC="now SIGNAL insert_2_finish";
--connection con1
reap;
--disconnect con1
--connection default

SELECT * FROM test;

DROP TABLE test;
--source include/wait_until_count_sessions.inc
2 changes: 1 addition & 1 deletion mysql-test/suite/innodb/t/innodb-autoinc.test
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,7 @@ INSERT INTO t1 VALUES (18446744073709551610); #-- 2^64 - 2
SELECT * FROM t1;
SET @@SESSION.AUTO_INCREMENT_INCREMENT=1152921504606846976, @@SESSION.AUTO_INCREMENT_OFFSET=1152921504606846976;
SHOW VARIABLES LIKE "%auto_inc%";
--error ER_WARN_DATA_OUT_OF_RANGE
--error ER_AUTOINC_READ_FAILED
INSERT INTO t1 VALUES (NULL),(NULL);
SELECT * FROM t1;
DROP TABLE t1;
Expand Down
26 changes: 8 additions & 18 deletions sql/handler.cc
Original file line number Diff line number Diff line change
Expand Up @@ -2447,26 +2447,11 @@ int handler::read_first_row(uchar * buf, uint primary_key)
If increment=10 and offset=5 and previous number is 1, we get:
@verbatim 1,5,15,25,35,... @endverbatim
*/
inline ulonglong
static ulonglong
compute_next_insert_id(ulonglong nr,struct system_variables *variables)
{
const ulonglong save_nr= nr;

if (variables->auto_increment_increment == 1)
nr= nr + 1; // optimization of the formula below
else
{
nr= (((nr+ variables->auto_increment_increment -
variables->auto_increment_offset)) /
(ulonglong) variables->auto_increment_increment);
nr= (nr* (ulonglong) variables->auto_increment_increment +
variables->auto_increment_offset);
}

if (unlikely(nr <= save_nr))
return ULONGLONG_MAX;

return nr;
return compute_next_insert_id(nr, variables->auto_increment_increment,
variables->auto_increment_offset);
}


Expand Down Expand Up @@ -2698,7 +2683,12 @@ int handler::update_auto_increment()
it. Hope that this rounding didn't push us out of the interval; even
if it did we cannot do anything about it (calling the engine again
will not help as we inserted no row).
Assert that this rounding is a no-op for InnoDB, which should respect
offset and increment. Otherwise we can go outside the interval and
result in duplicate key errors, see bug 76872.
*/
DBUG_ASSERT(compute_next_insert_id(nr - 1, variables) == nr
|| ht->db_type != DB_TYPE_INNODB);
nr= compute_next_insert_id(nr-1, variables);
}

Expand Down
33 changes: 33 additions & 0 deletions sql/handler.h
Original file line number Diff line number Diff line change
Expand Up @@ -2346,6 +2346,39 @@ static inline bool ha_storage_engine_is_enabled(const handlerton *db_type)
(db_type->state == SHOW_OPTION_YES) : FALSE;
}

/**
Generate the next auto-increment number based on increment and offset.
computes the lowest number
- strictly greater than "nr"
- of the form: auto_increment_offset + N * auto_increment_increment
If overflow happened then return MAX_ULONGLONG value as an
indication of overflow.
In most cases increment= offset= 1, in which case we get:
@verbatim 1,2,3,4,5,... @endverbatim
If increment=10 and offset=5 and previous number is 1, we get:
@verbatim 1,5,15,25,35,... @endverbatim
*/
inline ulonglong
compute_next_insert_id(ulonglong nr, ulonglong auto_increment_increment,
ulonglong auto_increment_offset)
{
const ulonglong save_nr= nr;

if (auto_increment_increment == 1)
nr= nr + 1; // optimization of the formula below
else
{
nr= (((nr + auto_increment_increment - auto_increment_offset)) /
auto_increment_increment);
nr= (nr * auto_increment_increment + auto_increment_offset);
}

if (unlikely(nr <= save_nr))
return ULONGLONG_MAX;

return nr;
}

/* basic stuff */
int ha_init_errors(void);
int ha_init(void);
Expand Down
9 changes: 9 additions & 0 deletions storage/innobase/handler/ha_innodb.cc
Original file line number Diff line number Diff line change
Expand Up @@ -6148,7 +6148,9 @@ ha_innobase::write_row(

innodb_srv_conc_enter_innodb(prebuilt->trx);

DEBUG_SYNC(user_thd, "ib_before_row_insert");
error = row_insert_for_mysql((byte*) record, prebuilt);
DEBUG_SYNC(user_thd, "ib_after_row_insert");

#ifdef EXTENDED_FOR_USERSTAT
if (UNIV_LIKELY(error == DB_SUCCESS && !trx->fake_changes)) {
Expand Down Expand Up @@ -11323,6 +11325,13 @@ ha_innobase::get_auto_increment(
}

set_if_bigger(*first_value, autoinc);

/* Honor increment and offset or handler will do that for us,
clobbering unused autoinc value past the end of interval */
set_if_bigger(*first_value,
compute_next_insert_id(*first_value - 1,
increment, offset));

/* Not in the middle of a mult-row INSERT. */
} else if (prebuilt->autoinc_last_value == 0) {
set_if_bigger(*first_value, autoinc);
Expand Down

0 comments on commit bd9d2eb

Please sign in to comment.