Skip to content

Commit f675abc

Browse files
committed
MDEV-40431 heap: classify rb-tree insert failures, mark the table crashed when key recovery fails
`hp_rb_write_key()` reported **every** rejected `tree_insert()` as `HA_ERR_FOUND_DUPP_KEY`, although `tree_insert()` also returns NULL when the allocation of the tree node fails. An out of memory during a BTREE-indexed UPDATE was therefore reported as a duplicate: the user got `ER_DUP_ENTRY` with a fabricated value, possibly naming a key that is not unique at all, `handler::is_fatal_error()` treated the allocation failure as not fatal for callers asking for `HA_CHECK_DUP_KEY`, and the `HA_ERR_OUT_OF_MEM` / `ENOMEM` arms of the `heap_update()` recovery list were unreachable on the rb-tree path. `tree_insert()` now records why it returned NULL in the new `TREE::error` (`TREE_ERROR_OOM` or `TREE_ERROR_DUP_KEY`), and `hp_rb_write_key()` maps that to `HA_ERR_OUT_OF_MEM` or `HA_ERR_FOUND_DUPP_KEY` (`HA_ERR_INTERNAL_ERROR` defensively, should a new NULL source appear). With the classification corrected, the allocation failure lands in the `HA_ERR_OUT_OF_MEM` arm of the `heap_update()` recovery, which moves the already changed keys back, so correcting the error report does not trade the wrong message for a silently corrupt index. The recovery at `err:` keeps its explicit list of error codes: those are the errors we know how to recover from, and recovering from errors whose meaning we do not know is worse than stopping. What changes around it: 1. `info->errkey` is only set for `HA_ERR_FOUND_DUPP_KEY`, the single error it describes; every other failure leaves the `-1` that `err:` starts with. 2. When the recovery itself cannot restore a key -- the re-insert of an old key value fails -- or the failure is outside the list, the index no longer describes the data and the table is marked **crashed** in the new `HP_SHARE::state_changed` (bits and macros modeled on the `state.changed` of Maria, see `storage/maria/maria_def.h`). A crashed table refuses every read and write with `HA_ERR_CRASHED` ("Index for table is corrupt"), `heap_check_heap()` reports it as damaged, and `hp_clear()` -- reached through TRUNCATE or DELETE without WHERE -- clears the state along with the data, because it rebuilds the indexes from nothing. Refusing a table known to be corrupt beats continuing and delivering wrong results. 3. `delete_key()` is assumed to succeed: it allocates no memory, so it cannot fail unless the table is already inconsistent, and building recovery logic and injected failures for that case would complicate the code for a scenario that cannot happen on a healthy table. If it ever does fail, the error falls outside the recovery list and the table is marked crashed, preserving the damaged state for analysis. The debug-build consistency check in `ha_heap::external_lock(F_UNLCK)` skips tables already marked crashed: their inconsistency is known and deliberate, and re-detecting it would raise a second error into a diagnostics area that can already hold OK, firing the `Diagnostics_area` assertion the check exists to prevent. The allocation failure is injected inside `tree_insert()` itself: `simulate_tree_insert_oom` fails every node allocation until the caller disarms it, `once_simulate_tree_insert_oom` fails only the next one and disarms itself. The two names must not be a prefix of one another, because `DBUG_SET()` matches an existing keyword by prefix and would silently merge instead of adding. Callers arm the keywords themselves and keep an inert guard keyword in the list, because a keyword list that becomes empty while debugging is on matches every keyword. The unit test `hp_test_update` drives the failures through the engine API: the classification and its duplicate-key counterpart, an already moved key being moved back, and the crashed lifecycle -- marking, refusal of reads and writes, and the reset on emptying. With the defects reintroduced, 8 of its 24 assertions fail. `heap.update_key_rollback` covers the same from SQL; without the fix its first UPDATE reports `ER_DUP_ENTRY 'Duplicate entry 101 for key k2'` on a key that is not unique.
1 parent 9482ccc commit f675abc

21 files changed

Lines changed: 672 additions & 8 deletions

include/heap.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,17 @@ typedef struct st_hp_blob_desc
143143
uint packlength; /* 1, 2, 3, or 4: length prefix size */
144144
} HP_BLOB_DESC;
145145

146+
/*
147+
Bits for HP_SHARE::state_changed, modeled on the state.changed bitmaps
148+
of Maria and MyISAM (see storage/maria/maria_def.h). A table marked
149+
crashed refuses every read and write with HA_ERR_CRASHED until it is
150+
re-created or emptied (hp_clear()).
151+
*/
152+
#define HEAP_STATE_CRASHED 1U
153+
154+
#define heap_mark_crashed(share) ((share)->state_changed|= HEAP_STATE_CRASHED)
155+
#define heap_is_crashed(share) ((share)->state_changed & HEAP_STATE_CRASHED)
156+
146157
typedef struct st_heap_share
147158
{
148159
HP_BLOCK block;
@@ -160,6 +171,7 @@ typedef struct st_heap_share
160171
uint reclength; /* Length of one record */
161172
uint visible; /* Offset to the flags byte (active/deleted/continuation) */
162173
uint changed;
174+
uint state_changed; /* Bitmap of HEAP_STATE_* flags */
163175
uint keys,max_key_length;
164176
uint currently_disabled_keys; /* saved value from "keys" when disabled */
165177
uint open_count;

include/my_tree.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ typedef uint32 element_count;
4848
typedef int (*tree_walk_action)(void *,element_count,void *);
4949

5050
typedef enum { free_init, free_free, free_end } TREE_FREE;
51+
typedef enum { TREE_ERROR_NONE, TREE_ERROR_OOM, TREE_ERROR_DUP_KEY }
52+
TREE_ERROR;
5153
typedef int (*tree_element_free)(void*, TREE_FREE, void *);
5254

5355
typedef struct st_tree_element {
@@ -70,6 +72,7 @@ typedef struct st_tree {
7072
tree_element_free free;
7173
myf my_flags;
7274
uint flag;
75+
TREE_ERROR error;
7376
} TREE;
7477

7578
/* Functions on whole tree */
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
#
2+
# Index consistency after a failed UPDATE on a HEAP table
3+
#
4+
# heap_update() moves the changed key entries to their new key values
5+
# before it updates the record. A failure raised after that has to
6+
# move them back. An rb-tree node allocation failure used to be
7+
# reported as a duplicate key, and when the recovery itself cannot
8+
# restore a key the table is now marked crashed, so that no statement
9+
# can use an index that no longer describes the data.
10+
#
11+
# The keyword lists armed below always carry the inert
12+
# "heap_test_guard" keyword: the one-shot injection removes its own
13+
# keyword when it fires, and a keyword list that becomes empty while
14+
# debugging is on matches every keyword.
15+
#
16+
CALL mtr.add_suppression("Index for table .t1. is corrupt");
17+
#
18+
# The hash key k1 is moved to the new value, then the rb-tree node
19+
# allocation for k2 fails once: the failure is reported as out of
20+
# memory, not as a duplicate, and both keys are moved back
21+
#
22+
CREATE TABLE t1 (
23+
id INT,
24+
a INT,
25+
b INT,
26+
KEY k1 (a),
27+
KEY k2 (b) USING BTREE
28+
) ENGINE=HEAP;
29+
INSERT INTO t1 VALUES (1, 10, 100), (2, 20, 200);
30+
SET SESSION debug_dbug="+d,once_simulate_tree_insert_oom,heap_test_guard";
31+
UPDATE t1 SET a= 11, b= 101 WHERE id = 1;
32+
ERROR HY000: Out of memory.
33+
SET SESSION debug_dbug="";
34+
CHECK TABLE t1;
35+
Table Op Msg_type Msg_text
36+
test.t1 check status OK
37+
# Both indexes must still find the row under its unchanged values
38+
SELECT id, a, b FROM t1 FORCE INDEX(k1) WHERE a = 10;
39+
id a b
40+
1 10 100
41+
SELECT id, a, b FROM t1 FORCE INDEX(k2) WHERE b = 100;
42+
id a b
43+
1 10 100
44+
SELECT COUNT(*) FROM t1 FORCE INDEX(k1) WHERE a = 11;
45+
COUNT(*)
46+
0
47+
SELECT COUNT(*) FROM t1 FORCE INDEX(k2) WHERE b = 101;
48+
COUNT(*)
49+
0
50+
SELECT id, a, b FROM t1 ORDER BY id;
51+
id a b
52+
1 10 100
53+
2 20 200
54+
#
55+
# A duplicate key is still reported as a duplicate
56+
#
57+
CREATE TABLE t2 (
58+
id INT,
59+
a INT,
60+
UNIQUE KEY k1 (a) USING BTREE
61+
) ENGINE=HEAP;
62+
INSERT INTO t2 VALUES (1, 10), (2, 20);
63+
UPDATE t2 SET a= 20 WHERE id = 1;
64+
ERROR 23000: Duplicate entry '20' for key 'k1'
65+
CHECK TABLE t2;
66+
Table Op Msg_type Msg_text
67+
test.t2 check status OK
68+
SELECT id, a FROM t2 ORDER BY id;
69+
id a
70+
1 10
71+
2 20
72+
DROP TABLE t2;
73+
#
74+
# The allocation failure persists, so restoring the old k2 entry
75+
# fails as well: the already moved k1 no longer describes the
76+
# record, the table is marked crashed, and every read and write on
77+
# it is refused until it is emptied
78+
#
79+
SET SESSION debug_dbug="+d,simulate_tree_insert_oom,heap_test_guard";
80+
UPDATE t1 SET a= 11, b= 101 WHERE id = 1;
81+
ERROR HY000: Out of memory.
82+
SET SESSION debug_dbug="";
83+
CHECK TABLE t1;
84+
Table Op Msg_type Msg_text
85+
test.t1 check error Corrupt
86+
SELECT id, a, b FROM t1 ORDER BY id;
87+
ERROR HY000: Index for table 't1' is corrupt; try to repair it
88+
SELECT id, a, b FROM t1 FORCE INDEX(k1) WHERE a = 10;
89+
ERROR HY000: Index for table 't1' is corrupt; try to repair it
90+
INSERT INTO t1 VALUES (3, 30, 300);
91+
ERROR HY000: Index for table 't1' is corrupt; try to repair it
92+
UPDATE t1 SET a= 12 WHERE id = 2;
93+
ERROR HY000: Index for table 't1' is corrupt; try to repair it
94+
DELETE FROM t1 WHERE id = 2;
95+
ERROR HY000: Index for table 't1' is corrupt; try to repair it
96+
# Emptying the table rebuilds the indexes from nothing
97+
TRUNCATE TABLE t1;
98+
CHECK TABLE t1;
99+
Table Op Msg_type Msg_text
100+
test.t1 check status OK
101+
INSERT INTO t1 VALUES (5, 50, 500);
102+
SELECT id, a, b FROM t1 FORCE INDEX(k2) WHERE b = 500;
103+
id a b
104+
5 50 500
105+
DROP TABLE t1;
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
--source include/have_debug.inc
2+
3+
--echo #
4+
--echo # Index consistency after a failed UPDATE on a HEAP table
5+
--echo #
6+
--echo # heap_update() moves the changed key entries to their new key values
7+
--echo # before it updates the record. A failure raised after that has to
8+
--echo # move them back. An rb-tree node allocation failure used to be
9+
--echo # reported as a duplicate key, and when the recovery itself cannot
10+
--echo # restore a key the table is now marked crashed, so that no statement
11+
--echo # can use an index that no longer describes the data.
12+
--echo #
13+
--echo # The keyword lists armed below always carry the inert
14+
--echo # "heap_test_guard" keyword: the one-shot injection removes its own
15+
--echo # keyword when it fires, and a keyword list that becomes empty while
16+
--echo # debugging is on matches every keyword.
17+
--echo #
18+
19+
CALL mtr.add_suppression("Index for table .t1. is corrupt");
20+
21+
--echo #
22+
--echo # The hash key k1 is moved to the new value, then the rb-tree node
23+
--echo # allocation for k2 fails once: the failure is reported as out of
24+
--echo # memory, not as a duplicate, and both keys are moved back
25+
--echo #
26+
27+
CREATE TABLE t1 (
28+
id INT,
29+
a INT,
30+
b INT,
31+
KEY k1 (a),
32+
KEY k2 (b) USING BTREE
33+
) ENGINE=HEAP;
34+
35+
INSERT INTO t1 VALUES (1, 10, 100), (2, 20, 200);
36+
37+
SET SESSION debug_dbug="+d,once_simulate_tree_insert_oom,heap_test_guard";
38+
--error ER_OUT_OF_RESOURCES
39+
UPDATE t1 SET a= 11, b= 101 WHERE id = 1;
40+
SET SESSION debug_dbug="";
41+
42+
CHECK TABLE t1;
43+
--echo # Both indexes must still find the row under its unchanged values
44+
SELECT id, a, b FROM t1 FORCE INDEX(k1) WHERE a = 10;
45+
SELECT id, a, b FROM t1 FORCE INDEX(k2) WHERE b = 100;
46+
SELECT COUNT(*) FROM t1 FORCE INDEX(k1) WHERE a = 11;
47+
SELECT COUNT(*) FROM t1 FORCE INDEX(k2) WHERE b = 101;
48+
SELECT id, a, b FROM t1 ORDER BY id;
49+
50+
--echo #
51+
--echo # A duplicate key is still reported as a duplicate
52+
--echo #
53+
54+
CREATE TABLE t2 (
55+
id INT,
56+
a INT,
57+
UNIQUE KEY k1 (a) USING BTREE
58+
) ENGINE=HEAP;
59+
60+
INSERT INTO t2 VALUES (1, 10), (2, 20);
61+
62+
--error ER_DUP_ENTRY
63+
UPDATE t2 SET a= 20 WHERE id = 1;
64+
65+
CHECK TABLE t2;
66+
SELECT id, a FROM t2 ORDER BY id;
67+
DROP TABLE t2;
68+
69+
--echo #
70+
--echo # The allocation failure persists, so restoring the old k2 entry
71+
--echo # fails as well: the already moved k1 no longer describes the
72+
--echo # record, the table is marked crashed, and every read and write on
73+
--echo # it is refused until it is emptied
74+
--echo #
75+
76+
SET SESSION debug_dbug="+d,simulate_tree_insert_oom,heap_test_guard";
77+
--error ER_OUT_OF_RESOURCES
78+
UPDATE t1 SET a= 11, b= 101 WHERE id = 1;
79+
SET SESSION debug_dbug="";
80+
81+
CHECK TABLE t1;
82+
--error ER_NOT_KEYFILE
83+
SELECT id, a, b FROM t1 ORDER BY id;
84+
--error ER_NOT_KEYFILE
85+
SELECT id, a, b FROM t1 FORCE INDEX(k1) WHERE a = 10;
86+
--error ER_NOT_KEYFILE
87+
INSERT INTO t1 VALUES (3, 30, 300);
88+
--error ER_NOT_KEYFILE
89+
UPDATE t1 SET a= 12 WHERE id = 2;
90+
--error ER_NOT_KEYFILE
91+
DELETE FROM t1 WHERE id = 2;
92+
93+
--echo # Emptying the table rebuilds the indexes from nothing
94+
TRUNCATE TABLE t1;
95+
CHECK TABLE t1;
96+
INSERT INTO t1 VALUES (5, 50, 500);
97+
SELECT id, a, b FROM t1 FORCE INDEX(k2) WHERE b = 500;
98+
99+
DROP TABLE t1;

mysys/tree.c

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ void init_tree(TREE *tree, size_t default_alloc_size, size_t memory_limit,
106106
tree->custom_arg = custom_arg;
107107
tree->my_flags= my_flags;
108108
tree->flag= 0;
109+
tree->error= TREE_ERROR_NONE;
109110
if (!free_element && size >= 0 &&
110111
((uint) size <= sizeof(void*) || ((uint) size & (sizeof(void*)-1))))
111112
{
@@ -274,13 +275,35 @@ TREE_ELEMENT *tree_insert(TREE *tree, void *key, uint key_size,
274275
}
275276

276277
key_size+=tree->size_of_element;
278+
/*
279+
Debug hooks that make this node allocation fail as if the allocator
280+
had returned NULL. "simulate_tree_insert_oom" fails every insert
281+
until the caller disarms it; "once_simulate_tree_insert_oom" fails
282+
only the next one and disarms itself. Neither name may be a prefix
283+
of the other: DBUG_SET() matches an existing list entry by prefix
284+
and would merge into it instead of adding the keyword.
285+
*/
286+
DBUG_EXECUTE_IF("once_simulate_tree_insert_oom",
287+
{
288+
DBUG_SET("-d,once_simulate_tree_insert_oom");
289+
tree->error= TREE_ERROR_OOM;
290+
return(NULL);
291+
});
292+
DBUG_EXECUTE_IF("simulate_tree_insert_oom",
293+
{
294+
tree->error= TREE_ERROR_OOM;
295+
return(NULL);
296+
});
277297
if (tree->with_delete)
278298
element=(TREE_ELEMENT *) my_malloc(key_memory_TREE, alloc_size,
279299
MYF(tree->my_flags | MY_WME));
280300
else
281301
element=(TREE_ELEMENT *) alloc_root(&tree->mem_root,alloc_size);
282302
if (!element)
303+
{
304+
tree->error= TREE_ERROR_OOM;
283305
return(NULL);
306+
}
284307
**parent=element;
285308
element->left=element->right= &null_element;
286309
if (!tree->offset_to_key)
@@ -303,7 +326,10 @@ TREE_ELEMENT *tree_insert(TREE *tree, void *key, uint key_size,
303326
else
304327
{
305328
if (tree->flag & TREE_NO_DUPS)
329+
{
330+
tree->error= TREE_ERROR_DUP_KEY;
306331
return(NULL);
332+
}
307333
element->count++;
308334
/* Avoid a wrap over of the count. */
309335
if (! element->count)

storage/heap/CMakeLists.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ IF(WITH_UNIT_TESTS)
3232
TARGET_LINK_LIBRARIES(hp_test1 heap mysys dbug strings)
3333
ADD_EXECUTABLE(hp_test2 hp_test2.c)
3434
TARGET_LINK_LIBRARIES(hp_test2 heap mysys dbug strings)
35-
MY_ADD_TESTS(hp_test_hash hp_test_scan hp_test_freelist hp_test_concurrent LINK_LIBRARIES heap mysys dbug strings)
35+
MY_ADD_TESTS(hp_test_hash hp_test_scan hp_test_freelist hp_test_concurrent
36+
hp_test_update LINK_LIBRARIES heap mysys dbug strings)
3637

3738
INCLUDE_DIRECTORIES(${CMAKE_SOURCE_DIR}/sql
3839
${CMAKE_SOURCE_DIR}/include)

storage/heap/_check.c

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@ int heap_check_heap(const HP_INFO *info, my_bool print_status)
5151
uchar *current_ptr= info->current_ptr;
5252
DBUG_ENTER("heap_check_heap");
5353

54+
if (heap_is_crashed(share))
55+
DBUG_RETURN(1); /* Already known to be damaged */
56+
5457
for (error=key= 0 ; key < share->keys ; key++)
5558
{
5659
if (share->keydef[key].algorithm == HA_KEY_ALG_BTREE)

storage/heap/ha_heap.cc

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -528,7 +528,15 @@ int ha_heap::reset_auto_increment(ulonglong value)
528528
int ha_heap::external_lock(THD *thd, int lock_type)
529529
{
530530
#if !defined(DBUG_OFF) && defined(EXTRA_HEAP_DEBUG)
531-
if (lock_type == F_UNLCK && file->s->changed && heap_check_heap(file, 0))
531+
/*
532+
A table already marked crashed is knowingly inconsistent; every data
533+
access on it fails with HA_ERR_CRASHED, so re-detecting the damage
534+
here would only raise a second error into a diagnostics area that
535+
can already be OK (e.g. after UNLOCK TABLES) and fire the
536+
Diagnostics_area assertion.
537+
*/
538+
if (lock_type == F_UNLCK && file->s->changed &&
539+
!heap_is_crashed(file->s) && heap_check_heap(file, 0))
532540
return HA_ERR_CRASHED;
533541
#endif
534542
if (lock_type == F_UNLCK)

storage/heap/hp_clear.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ void hp_clear(HP_SHARE *info)
4343
info->data_length= 0;
4444
info->blength=1;
4545
info->changed=0;
46+
info->state_changed=0;
4647
info->del_link=0;
4748
info->key_version++;
4849
info->file_version++;

storage/heap/hp_delete.c

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,8 @@ int heap_delete(HP_INFO *info, const uchar *record)
146146
DBUG_ENTER("heap_delete");
147147
DBUG_PRINT("enter",("info: %p record: %p", info, record));
148148

149+
if (heap_is_crashed(share))
150+
DBUG_RETURN(my_errno= HA_ERR_CRASHED);
149151
test_active(info);
150152
hp_flush_pending_blob_free(info);
151153

0 commit comments

Comments
 (0)