Skip to content

Commit a02773f

Browse files
MDEV-34057 Inconsistent FTS state in concurrent scenarios
Problem: ======= - This commit is a merge of mysql commit 129ee47ef994652081a11ee9040c0488e5275b14. InnoDB FTS can be in inconsistent state when sync operation terminates the server before committing the operation. This could lead to incorrect synced doc id and incorrect query results. Solution: ======== - During sync commit operation, InnoDB should pass the sync transaction to update the max doc id in the config table. fts_read_synced_doc_id() : This function is used to read only synced doc id from the config table.
1 parent 0406b2a commit a02773f

File tree

4 files changed

+201
-78
lines changed

4 files changed

+201
-78
lines changed
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
CREATE TABLE opening_lines (
2+
id INT UNSIGNED AUTO_INCREMENT NOT NULL PRIMARY KEY,
3+
opening_line TEXT(500),
4+
author VARCHAR(200),
5+
title VARCHAR(200)
6+
) ENGINE=InnoDB;
7+
CREATE FULLTEXT INDEX idx ON opening_lines(opening_line);
8+
CREATE FULLTEXT INDEX ft_idx1 ON opening_lines(title);
9+
INSERT INTO opening_lines(opening_line,author,title) VALUES
10+
('Call me Ishmael.','Herman Melville','Moby Dick'),
11+
('A screaming comes across the sky.','Thomas Pynchon','Gravity\'s Rainbow'),
12+
('I am an invisible man.','Ralph Ellison','Invisible Man'),
13+
('Where now? Who now? When now?','Samuel Beckett','The Unnamable'),
14+
('It was love at first sight.','Joseph Heller','Catch-22'),
15+
('All this happened, more or less.','Kurt Vonnegut','Slaughterhouse-Five'),
16+
('Mrs. Dalloway said she would buy the flowers herself.','Virginia Woolf','Mrs. Dalloway'),
17+
('It was a pleasure to burn.','Ray Bradbury','Fahrenheit 451');
18+
SET GLOBAL innodb_ft_aux_table='test/opening_lines';
19+
SELECT * FROM information_schema.innodb_ft_config;
20+
KEY VALUE
21+
optimize_checkpoint_limit 180
22+
synced_doc_id 0
23+
stopword_table_name
24+
use_stopword 1
25+
SELECT * FROM opening_lines WHERE MATCH(opening_line) AGAINST('Ishmael');
26+
id opening_line author title
27+
1 Call me Ishmael. Herman Melville Moby Dick
28+
SELECT * FROM opening_lines WHERE MATCH(opening_line) AGAINST('invisible');
29+
id opening_line author title
30+
3 I am an invisible man. Ralph Ellison Invisible Man
31+
SELECT * FROM opening_lines;
32+
id opening_line author title
33+
1 Call me Ishmael. Herman Melville Moby Dick
34+
2 A screaming comes across the sky. Thomas Pynchon Gravity's Rainbow
35+
3 I am an invisible man. Ralph Ellison Invisible Man
36+
4 Where now? Who now? When now? Samuel Beckett The Unnamable
37+
5 It was love at first sight. Joseph Heller Catch-22
38+
6 All this happened, more or less. Kurt Vonnegut Slaughterhouse-Five
39+
7 Mrs. Dalloway said she would buy the flowers herself. Virginia Woolf Mrs. Dalloway
40+
8 It was a pleasure to burn. Ray Bradbury Fahrenheit 451
41+
SET GLOBAL innodb_optimize_fulltext_only=ON;
42+
SET DEBUG_SYNC='fts_crash_before_commit_sync SIGNAL hung WAIT_FOR ever';
43+
OPTIMIZE TABLE opening_lines;
44+
connect con1,localhost,root,,;
45+
SET DEBUG_SYNC='now WAIT_FOR hung';
46+
# restart
47+
SELECT * FROM opening_lines WHERE MATCH(opening_line) AGAINST('Ishmael');
48+
id opening_line author title
49+
1 Call me Ishmael. Herman Melville Moby Dick
50+
SELECT * FROM opening_lines WHERE MATCH(opening_line) AGAINST('invisible');
51+
id opening_line author title
52+
3 I am an invisible man. Ralph Ellison Invisible Man
53+
SELECT * FROM opening_lines;
54+
id opening_line author title
55+
1 Call me Ishmael. Herman Melville Moby Dick
56+
2 A screaming comes across the sky. Thomas Pynchon Gravity's Rainbow
57+
3 I am an invisible man. Ralph Ellison Invisible Man
58+
4 Where now? Who now? When now? Samuel Beckett The Unnamable
59+
5 It was love at first sight. Joseph Heller Catch-22
60+
6 All this happened, more or less. Kurt Vonnegut Slaughterhouse-Five
61+
7 Mrs. Dalloway said she would buy the flowers herself. Virginia Woolf Mrs. Dalloway
62+
8 It was a pleasure to burn. Ray Bradbury Fahrenheit 451
63+
DROP TABLE opening_lines;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
--innodb_ft_config
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# Test database resiliency against scenario where the server crashes
2+
# right before fts_sync_commit commits its transaction
3+
source include/have_innodb.inc;
4+
source include/have_debug.inc;
5+
source include/not_embedded.inc;
6+
source include/have_debug_sync.inc;
7+
8+
CREATE TABLE opening_lines (
9+
id INT UNSIGNED AUTO_INCREMENT NOT NULL PRIMARY KEY,
10+
opening_line TEXT(500),
11+
author VARCHAR(200),
12+
title VARCHAR(200)
13+
) ENGINE=InnoDB;
14+
15+
CREATE FULLTEXT INDEX idx ON opening_lines(opening_line);
16+
CREATE FULLTEXT INDEX ft_idx1 ON opening_lines(title);
17+
18+
INSERT INTO opening_lines(opening_line,author,title) VALUES
19+
('Call me Ishmael.','Herman Melville','Moby Dick'),
20+
('A screaming comes across the sky.','Thomas Pynchon','Gravity\'s Rainbow'),
21+
('I am an invisible man.','Ralph Ellison','Invisible Man'),
22+
('Where now? Who now? When now?','Samuel Beckett','The Unnamable'),
23+
('It was love at first sight.','Joseph Heller','Catch-22'),
24+
('All this happened, more or less.','Kurt Vonnegut','Slaughterhouse-Five'),
25+
('Mrs. Dalloway said she would buy the flowers herself.','Virginia Woolf','Mrs. Dalloway'),
26+
('It was a pleasure to burn.','Ray Bradbury','Fahrenheit 451');
27+
28+
SET GLOBAL innodb_ft_aux_table='test/opening_lines';
29+
SELECT * FROM information_schema.innodb_ft_config;
30+
31+
SELECT * FROM opening_lines WHERE MATCH(opening_line) AGAINST('Ishmael');
32+
SELECT * FROM opening_lines WHERE MATCH(opening_line) AGAINST('invisible');
33+
SELECT * FROM opening_lines;
34+
35+
SET GLOBAL innodb_optimize_fulltext_only=ON;
36+
SET DEBUG_SYNC='fts_crash_before_commit_sync SIGNAL hung WAIT_FOR ever';
37+
send OPTIMIZE TABLE opening_lines;
38+
39+
connect(con1,localhost,root,,);
40+
SET DEBUG_SYNC='now WAIT_FOR hung';
41+
let $shutdown_timeout=0;
42+
--source include/restart_mysqld.inc
43+
44+
SELECT * FROM opening_lines WHERE MATCH(opening_line) AGAINST('Ishmael');
45+
SELECT * FROM opening_lines WHERE MATCH(opening_line) AGAINST('invisible');
46+
SELECT * FROM opening_lines;
47+
DROP TABLE opening_lines;

storage/innobase/fts/fts0fts.cc

Lines changed: 90 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -2607,89 +2607,85 @@ fts_get_next_doc_id(
26072607
return(DB_SUCCESS);
26082608
}
26092609

2610-
/*********************************************************************//**
2611-
This function fetch the Doc ID from CONFIG table, and compare with
2610+
/** Read the synced document id from the fts configuration table
2611+
@param table fts table
2612+
@param doc_id document id to be read
2613+
@param trx transaction to read from config table
2614+
@return DB_SUCCESS in case of success */
2615+
static
2616+
dberr_t fts_read_synced_doc_id(const dict_table_t *table,
2617+
doc_id_t *doc_id,
2618+
trx_t *trx)
2619+
{
2620+
dberr_t error;
2621+
que_t* graph= NULL;
2622+
char table_name[MAX_FULL_NAME_LEN];
2623+
2624+
fts_table_t fts_table;
2625+
fts_table.suffix= "CONFIG";
2626+
fts_table.table_id= table->id;
2627+
fts_table.type= FTS_COMMON_TABLE;
2628+
fts_table.table= table;
2629+
ut_a(table->fts->doc_col != ULINT_UNDEFINED);
2630+
2631+
trx->op_info = "update the next FTS document id";
2632+
pars_info_t *info= pars_info_create();
2633+
pars_info_bind_function(info, "my_func", fts_fetch_store_doc_id,
2634+
doc_id);
2635+
2636+
fts_get_table_name(&fts_table, table_name);
2637+
pars_info_bind_id(info, "config_table", table_name);
2638+
2639+
graph= fts_parse_sql(
2640+
&fts_table, info,
2641+
"DECLARE FUNCTION my_func;\n"
2642+
"DECLARE CURSOR c IS SELECT value FROM $config_table"
2643+
" WHERE key = 'synced_doc_id' FOR UPDATE;\n"
2644+
"BEGIN\n"
2645+
""
2646+
"OPEN c;\n"
2647+
"WHILE 1 = 1 LOOP\n"
2648+
" FETCH c INTO my_func();\n"
2649+
" IF c % NOTFOUND THEN\n"
2650+
" EXIT;\n"
2651+
" END IF;\n"
2652+
"END LOOP;\n"
2653+
"CLOSE c;");
2654+
2655+
*doc_id = 0;
2656+
error = fts_eval_sql(trx, graph);
2657+
fts_que_graph_free_check_lock(&fts_table, NULL, graph);
2658+
return error;
2659+
}
2660+
2661+
/** This function fetch the Doc ID from CONFIG table, and compare with
26122662
the Doc ID supplied. And store the larger one to the CONFIG table.
2663+
@param table fts table
2664+
@param cmp_doc_id Doc ID to compare
2665+
@param doc_id larger document id after comparing "cmp_doc_id" to
2666+
the one stored in CONFIG table
2667+
@param trx transaction
26132668
@return DB_SUCCESS if OK */
2614-
static MY_ATTRIBUTE((nonnull))
2669+
static
26152670
dberr_t
26162671
fts_cmp_set_sync_doc_id(
2617-
/*====================*/
2618-
const dict_table_t* table,/*!< in: table */
2619-
doc_id_tcmp_doc_id,/*!< in: Doc ID to compare */
2620-
ibool read_only,/*!< in: TRUE if read the
2621-
synced_doc_id only */
2622-
doc_id_t* doc_id)/*!< out: larger document id
2623-
after comparing "cmp_doc_id"
2624-
to the one stored in CONFIG
2625-
table */
2672+
const dict_table_t *table,
2673+
doc_id_t cmp_doc_id,
2674+
doc_id_t *doc_id,
2675+
trx_t *trx=nullptr)
26262676
{
2627-
trx_t* trx;
2628-
pars_info_t* info;
2629-
dberr_terror;
2630-
fts_table_tfts_table;
2631-
que_t* graph = NULL;
2632-
fts_cache_t* cache = table->fts->cache;
2633-
chartable_name[MAX_FULL_NAME_LEN];
2634-
retry:
2635-
ut_a(table->fts->doc_col != ULINT_UNDEFINED);
2677+
fts_cache_t* cache= table->fts->cache;
2678+
dberr_t error = DB_SUCCESS;
2679+
const trx_t*const caller_trx = trx;
26362680

2637-
fts_table.suffix = "CONFIG";
2638-
fts_table.table_id = table->id;
2639-
fts_table.type = FTS_COMMON_TABLE;
2640-
fts_table.table = table;
2641-
2642-
trx = trx_create();
2643-
if (srv_read_only_mode) {
2681+
if (trx == nullptr) {
2682+
trx = trx_create();
26442683
trx_start_internal_read_only(trx);
2645-
} else {
2646-
trx_start_internal(trx);
2647-
}
2648-
2649-
trx->op_info = "update the next FTS document id";
2650-
2651-
info = pars_info_create();
2652-
2653-
pars_info_bind_function(
2654-
info, "my_func", fts_fetch_store_doc_id, doc_id);
2655-
2656-
fts_get_table_name(&fts_table, table_name);
2657-
pars_info_bind_id(info, "config_table", table_name);
2658-
2659-
graph = fts_parse_sql(
2660-
&fts_table, info,
2661-
"DECLARE FUNCTION my_func;\n"
2662-
"DECLARE CURSOR c IS SELECT value FROM $config_table"
2663-
" WHERE key = 'synced_doc_id' FOR UPDATE;\n"
2664-
"BEGIN\n"
2665-
""
2666-
"OPEN c;\n"
2667-
"WHILE 1 = 1 LOOP\n"
2668-
" FETCH c INTO my_func();\n"
2669-
" IF c % NOTFOUND THEN\n"
2670-
" EXIT;\n"
2671-
" END IF;\n"
2672-
"END LOOP;\n"
2673-
"CLOSE c;");
2674-
2675-
*doc_id = 0;
2676-
2677-
error = fts_eval_sql(trx, graph);
2678-
2679-
fts_que_graph_free_check_lock(&fts_table, NULL, graph);
2680-
2681-
// FIXME: We need to retry deadlock errors
2682-
if (error != DB_SUCCESS) {
2683-
goto func_exit;
26842684
}
2685+
retry:
2686+
error = fts_read_synced_doc_id(table, doc_id, trx);
26852687

2686-
if (read_only) {
2687-
/* InnoDB stores actual synced_doc_id value + 1 in
2688-
FTS_CONFIG table. Reduce the value by 1 while reading
2689-
after startup. */
2690-
if (*doc_id) *doc_id -= 1;
2691-
goto func_exit;
2692-
}
2688+
if (error != DB_SUCCESS) goto func_exit;
26932689

26942690
if (cmp_doc_id == 0 && *doc_id) {
26952691
cache->synced_doc_id = *doc_id - 1;
@@ -2714,13 +2710,18 @@ fts_cmp_set_sync_doc_id(
27142710

27152711
func_exit:
27162712

2713+
if (caller_trx) {
2714+
return error;
2715+
}
2716+
27172717
if (UNIV_LIKELY(error == DB_SUCCESS)) {
27182718
fts_sql_commit(trx);
27192719
} else {
27202720
*doc_id = 0;
27212721

27222722
ib::error() << "(" << error << ") while getting next doc id "
27232723
"for table " << table->name;
2724+
27242725
fts_sql_rollback(trx);
27252726

27262727
if (error == DB_DEADLOCK) {
@@ -4201,8 +4202,8 @@ fts_sync_commit(
42014202

42024203
/* After each Sync, update the CONFIG table about the max doc id
42034204
we just sync-ed to index table */
4204-
error = fts_cmp_set_sync_doc_id(sync->table, sync->max_doc_id, FALSE,
4205-
&last_doc_id);
4205+
error = fts_cmp_set_sync_doc_id(sync->table, sync->max_doc_id,
4206+
&last_doc_id, trx);
42064207

42074208
/* Get the list of deleted documents that are either in the
42084209
cache or were headed there but were deleted before the add
@@ -4228,6 +4229,7 @@ fts_sync_commit(
42284229
rw_lock_x_unlock(&cache->lock);
42294230

42304231
if (UNIV_LIKELY(error == DB_SUCCESS)) {
4232+
DEBUG_SYNC_C("fts_crash_before_commit_sync");
42314233
fts_sql_commit(trx);
42324234
} else {
42334235
fts_sql_rollback(trx);
@@ -4901,7 +4903,7 @@ fts_init_doc_id(
49014903

49024904
/* Then compare this value with the ID value stored in the CONFIG
49034905
table. The larger one will be our new initial Doc ID */
4904-
fts_cmp_set_sync_doc_id(table, 0, FALSE, &max_doc_id);
4906+
fts_cmp_set_sync_doc_id(table, 0, &max_doc_id);
49054907

49064908
/* If DICT_TF2_FTS_ADD_DOC_ID is set, we are in the process of
49074909
creating index (and add doc id column. No need to recovery
@@ -6376,7 +6378,17 @@ fts_init_index(
63766378
start_doc = cache->synced_doc_id;
63776379

63786380
if (!start_doc) {
6379-
fts_cmp_set_sync_doc_id(table, 0, TRUE, &start_doc);
6381+
trx_t *trx = trx_create();
6382+
trx_start_internal_read_only(trx);
6383+
dberr_t err= fts_read_synced_doc_id(table, &start_doc, trx);
6384+
fts_sql_commit(trx);
6385+
trx->free();
6386+
if (err != DB_SUCCESS) {
6387+
goto func_exit;
6388+
}
6389+
if (start_doc) {
6390+
start_doc--;
6391+
}
63806392
cache->synced_doc_id = start_doc;
63816393
}
63826394

0 commit comments

Comments
 (0)