Skip to content

Commit 13d25dd

Browse files
authored
Fix crash due to delete entry from compress quicklistNode and wrongly split quicklistNode (redis#11242)
This PR mainly deals with 2 crashes introduced in redis#9357, and fix the QUICKLIST-PACKED-THRESHOLD mess in external test mode. 1. Fix crash due to deleting an entry from a compress quicklistNode When inserting a large element, we need to create a new quicklistNode first, and then delete its previous element, if the node where the deleted element is located is compressed, it will cause a crash. Now add `dont_compress` to quicklistNode, if we want to use a quicklistNode after some operation, we can use this flag like following: ```c node->dont_compress = 1; /* Prevent to be compressed */ some_operation(node); /* This operation might try to compress this node */ some_other_operation(node); /* We can use this node without decompress it */ node->dont_compress = 0; /* Re-able compression */ quicklistCompressNode(node); ``` Perhaps in the future, we could just disable the current entry from being compressed during the iterator loop, but that would require more work. 2. Fix crash due to wrongly split quicklist before redis#9357, the offset param of _quicklistSplitNode() will not negative. For now, when offset is negative, the split extent will be wrong. following example: ```c int orig_start = after ? offset + 1 : 0; int orig_extent = after ? -1 : offset; int new_start = after ? 0 : offset; int new_extent = after ? offset + 1 : -1; # offset: -2, after: 1, node->count: 2 # current wrong range: [-1,-1] [0,-1] # correct range: [1,-1] [0, 1] ``` Because only `_quicklistInsert()` splits the quicklistNode and only `quicklistInsertAfter()`, `quicklistInsertBefore()` call _quicklistInsert(), so `quicklistReplaceEntry()` and `listTypeInsert()` might occur this crash. But the iterator of `listTypeInsert()` is alway from head to tail(iter->offset is always positive), so it is not affected. The final conclusion is this crash only occur when we insert a large element with negative index into a list, that affects `LSET` command and `RM_ListSet` module api. 3. In external test mode, we need to restore quicklist packed threshold after when the end of test. 4. Show `node->count` in quicklistRepr(). 5. Add new tcl proc `config_get_set` to support restoring config in tests.
1 parent 464aa04 commit 13d25dd

File tree

5 files changed

+85
-17
lines changed

5 files changed

+85
-17
lines changed

src/debug.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -466,7 +466,7 @@ void debugCommand(client *c) {
466466
" default.",
467467
"QUICKLIST-PACKED-THRESHOLD <size>",
468468
" Sets the threshold for elements to be inserted as plain vs packed nodes",
469-
" Default value is 1GB, allows values up to 4GB",
469+
" Default value is 1GB, allows values up to 4GB. Setting to 0 restores to default.",
470470
"SET-SKIP-CHECKSUM-VALIDATION <0|1>",
471471
" Enables or disables checksum checks for RDB files and RESTORE's payload.",
472472
"SLEEP <seconds>",

src/quicklist.c

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ int quicklistisSetPackedThreshold(size_t sz) {
5757
/* Don't allow threshold to be set above or even slightly below 4GB */
5858
if (sz > (1ull<<32) - (1<<20)) {
5959
return 0;
60+
} else if (sz == 0) { /* 0 means restore threshold */
61+
sz = (1 << 30);
6062
}
6163
packed_threshold = sz;
6264
return 1;
@@ -177,6 +179,7 @@ REDIS_STATIC quicklistNode *quicklistCreateNode(void) {
177179
node->encoding = QUICKLIST_NODE_ENCODING_RAW;
178180
node->container = QUICKLIST_NODE_CONTAINER_PACKED;
179181
node->recompress = 0;
182+
node->dont_compress = 0;
180183
return node;
181184
}
182185

@@ -212,6 +215,7 @@ REDIS_STATIC int __quicklistCompressNode(quicklistNode *node) {
212215
#ifdef REDIS_TEST
213216
node->attempted_compress = 1;
214217
#endif
218+
if (node->dont_compress) return 0;
215219

216220
/* validate that the node is neither
217221
* tail nor head (it has prev and next)*/
@@ -748,12 +752,15 @@ void quicklistReplaceEntry(quicklistIter *iter, quicklistEntry *entry,
748752
__quicklistDelNode(quicklist, entry->node);
749753
}
750754
} else {
755+
entry->node->dont_compress = 1; /* Prevent compression in quicklistInsertAfter() */
751756
quicklistInsertAfter(iter, entry, data, sz);
752757
if (entry->node->count == 1) {
753758
__quicklistDelNode(quicklist, entry->node);
754759
} else {
755760
unsigned char *p = lpSeek(entry->node->entry, -1);
756761
quicklistDelIndex(quicklist, entry->node, &p);
762+
entry->node->dont_compress = 0; /* Re-enable compression */
763+
quicklistCompress(quicklist, entry->node);
757764
quicklistCompress(quicklist, entry->node->next);
758765
}
759766
}
@@ -905,6 +912,9 @@ REDIS_STATIC quicklistNode *_quicklistSplitNode(quicklistNode *node, int offset,
905912
/* Copy original listpack so we can split it */
906913
memcpy(new_node->entry, node->entry, zl_sz);
907914

915+
/* Need positive offset for calculating extent below. */
916+
if (offset < 0) offset = node->count + offset;
917+
908918
/* Ranges to be trimmed: -1 here means "continue deleting until the list ends" */
909919
int orig_start = after ? offset + 1 : 0;
910920
int orig_extent = after ? -1 : offset;
@@ -1608,10 +1618,11 @@ void quicklistRepr(unsigned char *ql, int full) {
16081618

16091619
while(node != NULL) {
16101620
printf("{quicklist node(%d)\n", i++);
1611-
printf("{container : %s, encoding: %s, size: %zu, recompress: %d, attempted_compress: %d}\n",
1621+
printf("{container : %s, encoding: %s, size: %zu, count: %d, recompress: %d, attempted_compress: %d}\n",
16121622
QL_NODE_IS_PLAIN(node) ? "PLAIN": "PACKED",
16131623
(node->encoding == QUICKLIST_NODE_ENCODING_RAW) ? "RAW": "LZF",
16141624
node->sz,
1625+
node->count,
16151626
node->recompress,
16161627
node->attempted_compress);
16171628

src/quicklist.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,8 @@ typedef struct quicklistNode {
5353
unsigned int container : 2; /* PLAIN==1 or PACKED==2 */
5454
unsigned int recompress : 1; /* was this node previous compressed? */
5555
unsigned int attempted_compress : 1; /* node can't compress; too small */
56-
unsigned int extra : 10; /* more bits to steal for future usage */
56+
unsigned int dont_compress : 1; /* prevent compression of entry that will be used later */
57+
unsigned int extra : 9; /* more bits to steal for future usage */
5758
} quicklistNode;
5859

5960
/* quicklistLZF is a 8+N byte struct holding 'sz' followed by 'compressed'.

tests/support/util.tcl

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -928,6 +928,12 @@ proc config_set {param value {options {}}} {
928928
}
929929
}
930930

931+
proc config_get_set {param value {options {}}} {
932+
set config [lindex [r config get $param] 1]
933+
config_set $param $value $options
934+
return $config
935+
}
936+
931937
proc delete_lines_with_pattern {filename tmpfilename pattern} {
932938
set fh_in [open $filename r]
933939
set fh_out [open $tmpfilename w]

tests/unit/type/list.tcl

Lines changed: 64 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,8 @@ start_server [list overrides [list save ""] ] {
6666
assert_equal [r lpop list4] [string repeat c 500]
6767
assert_equal [r lpop list4] [string repeat b 500]
6868
assert_equal [r lpop list4] [string repeat a 500]
69-
} {} {needs:debug}
69+
r debug quicklist-packed-threshold 0
70+
} {OK} {needs:debug}
7071

7172
test {plain node check compression with ltrim} {
7273
r debug quicklist-packed-threshold 1b
@@ -75,8 +76,9 @@ start_server [list overrides [list save ""] ] {
7576
r rpush list5 [string repeat c 500]
7677
assert_equal [string repeat b 500] [r lindex list5 1]
7778
r LTRIM list5 1 -1
78-
r llen list5
79-
} {2} {needs:debug}
79+
assert_equal [r llen list5] 2
80+
r debug quicklist-packed-threshold 0
81+
} {OK} {needs:debug}
8082

8183
test {plain node check compression using lset} {
8284
r debug quicklist-packed-threshold 1b
@@ -86,7 +88,8 @@ start_server [list overrides [list save ""] ] {
8688
r lpush list6 [string repeat c 500]
8789
r LSET list6 0 [string repeat d 500]
8890
assert_equal [string repeat d 500] [r lindex list6 0]
89-
} {} {needs:debug}
91+
r debug quicklist-packed-threshold 0
92+
} {OK} {needs:debug}
9093

9194
# revert config for external mode tests.
9295
r config set list-compress-depth 0
@@ -115,7 +118,8 @@ start_server [list overrides [list save ""] ] {
115118
r lpush lst bb
116119
r debug reload
117120
assert_equal [r rpop lst] "xxxxxxxxxx"
118-
} {} {needs:debug}
121+
r debug quicklist-packed-threshold 0
122+
} {OK} {needs:debug}
119123

120124
# basic command check for plain nodes - "LINDEX & LINSERT"
121125
test {Test LINDEX and LINSERT on plain nodes} {
@@ -129,7 +133,8 @@ start_server [list overrides [list save ""] ] {
129133
r linsert lst BEFORE "9" "7"
130134
r linsert lst BEFORE "9" "xxxxxxxxxxx"
131135
assert {[r lindex lst 3] eq "xxxxxxxxxxx"}
132-
} {} {needs:debug}
136+
r debug quicklist-packed-threshold 0
137+
} {OK} {needs:debug}
133138

134139
# basic command check for plain nodes - "LTRIM"
135140
test {Test LTRIM on plain nodes} {
@@ -140,7 +145,8 @@ start_server [list overrides [list save ""] ] {
140145
r lpush lst1 9
141146
r LTRIM lst1 1 -1
142147
assert_equal [r llen lst1] 2
143-
} {} {needs:debug}
148+
r debug quicklist-packed-threshold 0
149+
} {OK} {needs:debug}
144150

145151
# basic command check for plain nodes - "LREM"
146152
test {Test LREM on plain nodes} {
@@ -153,7 +159,8 @@ start_server [list overrides [list save ""] ] {
153159
r lpush lst 9
154160
r LREM lst -2 "one"
155161
assert_equal [r llen lst] 2
156-
} {} {needs:debug}
162+
r debug quicklist-packed-threshold 0
163+
} {OK} {needs:debug}
157164

158165
# basic command check for plain nodes - "LPOS"
159166
test {Test LPOS on plain nodes} {
@@ -164,7 +171,8 @@ start_server [list overrides [list save ""] ] {
164171
r RPUSH lst "cc"
165172
r LSET lst 0 "xxxxxxxxxxx"
166173
assert_equal [r LPOS lst "xxxxxxxxxxx"] 0
167-
} {} {needs:debug}
174+
r debug quicklist-packed-threshold 0
175+
} {OK} {needs:debug}
168176

169177
# basic command check for plain nodes - "LMOVE"
170178
test {Test LMOVE on plain nodes} {
@@ -183,7 +191,8 @@ start_server [list overrides [list save ""] ] {
183191
assert_equal [r lpop lst2{t}] "cc"
184192
assert_equal [r lpop lst{t}] "dd"
185193
assert_equal [r lpop lst{t}] "xxxxxxxxxxx"
186-
} {} {needs:debug}
194+
r debug quicklist-packed-threshold 0
195+
} {OK} {needs:debug}
187196

188197
# testing LSET with combinations of node types
189198
# plain->packed , packed->plain, plain->plain, packed->packed
@@ -206,7 +215,8 @@ start_server [list overrides [list save ""] ] {
206215
r lset lst 0 "cc"
207216
set s1 [r lpop lst]
208217
assert_equal $s1 "cc"
209-
} {} {needs:debug}
218+
r debug quicklist-packed-threshold 0
219+
} {OK} {needs:debug}
210220

211221
# checking LSET in case ziplist needs to be split
212222
test {Test LSET with packed is split in the middle} {
@@ -223,14 +233,15 @@ start_server [list overrides [list save ""] ] {
223233
assert_equal [r lpop lst] [string repeat e 10]
224234
assert_equal [r lpop lst] "dd"
225235
assert_equal [r lpop lst] "ee"
226-
} {} {needs:debug}
236+
r debug quicklist-packed-threshold 0
237+
} {OK} {needs:debug}
227238

228239

229240
# repeating "plain check LSET with combinations"
230241
# but now with single item in each ziplist
231242
test {Test LSET with packed consist only one item} {
232243
r flushdb
233-
r config set list-max-ziplist-size 1
244+
set original_config [config_get_set list-max-ziplist-size 1]
234245
r debug quicklist-packed-threshold 1b
235246
r RPUSH lst "aa"
236247
r RPUSH lst "bb"
@@ -249,7 +260,46 @@ start_server [list overrides [list save ""] ] {
249260
r lset lst 0 "cc"
250261
set s1 [r lpop lst]
251262
assert_equal $s1 "cc"
252-
} {} {needs:debug}
263+
r debug quicklist-packed-threshold 0
264+
r config set list-max-ziplist-size $original_config
265+
} {OK} {needs:debug}
266+
267+
test {Crash due to delete entry from a compress quicklist node} {
268+
r flushdb
269+
r debug quicklist-packed-threshold 100b
270+
set original_config [config_get_set list-compress-depth 1]
271+
272+
set small_ele [string repeat x 32]
273+
set large_ele [string repeat x 100]
274+
275+
# Push a large element
276+
r RPUSH lst $large_ele
277+
278+
# Insert two elements and keep them in the same node
279+
r RPUSH lst $small_ele
280+
r RPUSH lst $small_ele
281+
282+
# When setting the position of -1 to a large element, we first insert
283+
# a large element at the end and then delete its previous element.
284+
r LSET lst -1 $large_ele
285+
assert_equal "$large_ele $small_ele $large_ele" [r LRANGE lst 0 -1]
286+
287+
r debug quicklist-packed-threshold 0
288+
r config set list-compress-depth $original_config
289+
} {OK} {needs:debug}
290+
291+
test {Crash due to split quicklist node wrongly} {
292+
r flushdb
293+
r debug quicklist-packed-threshold 10b
294+
295+
r LPUSH lst "aa"
296+
r LPUSH lst "bb"
297+
r LSET lst -2 [string repeat x 10]
298+
r RPOP lst
299+
assert_equal [string repeat x 10] [r LRANGE lst 0 -1]
300+
301+
r debug quicklist-packed-threshold 0
302+
} {OK} {needs:debug}
253303
}
254304

255305
run_solo {list-large-memory} {

0 commit comments

Comments
 (0)