Skip to content

Commit 863d766

Browse files
committed
Bump RDB format support up to v6 and fix a couple of glitches.
1 parent cad13c0 commit 863d766

8 files changed

+90
-19
lines changed

README.md

+14-5
Original file line numberDiff line numberDiff line change
@@ -51,13 +51,22 @@ for `RDB::Reader` with a few additional helper methods. You can find more about
5151
dumpers in [lib/rdb/dumper.rb](https://github.com/nrk/redis-rdb/blob/master/lib/rdb/dumper.rb)
5252
and [lib/rdb/dumpers/aof.rb](https://github.com/nrk/redis-rdb/blob/master/lib/rdb/dumpers/aof.rb).
5353

54-
## Additional notes and credits ##
54+
## RDB file format ##
5555

56-
Right now there is still no documentation about the binary format of .rdb files so we used
56+
Right now there is still no official documentation about the binary format of .rdb files beside
5757
the code in [rdb.c](https://github.com/antirez/redis/blob/unstable/src/rdb.c) as the reference
58-
implementation. Credit goes also to [sripathikrishnan](https://github.com/sripathikrishnan),
59-
his work on the [redis-rdb-tools](https://github.com/sripathikrishnan/redis-rdb-tools) Python
60-
library was quite an inspiration for the final design of `RDB::Reader` and we also reused the
58+
implementation.
59+
60+
[An unofficial](https://github.com/sripathikrishnan/redis-rdb-tools/wiki/Redis-RDB-Dump-File-Format)
61+
but comprehensive description of the RDB format has been recently made available, but there is
62+
still no official documentation about it beside the actual implementation that can be found in
63+
[rdb.c](https://github.com/antirez/redis/blob/unstable/src/rdb.c).
64+
65+
## Additional notes and credits ##
66+
67+
Credit goes to [sripathikrishnan](https://github.com/sripathikrishnan) for his work on the
68+
[redis-rdb-tools](https://github.com/sripathikrishnan/redis-rdb-tools) Python library that
69+
proved to be quite an inspiration for the final design of `RDB::Reader`.We also reused the
6170
.rdb files shipped with his project for testing.
6271

6372
## Dependencies ##

lib/rdb/reader.rb

+11-6
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,12 @@ def read(rdb, options = {})
2121

2222
case state.key_type_id
2323
when Opcode::EXPIRETIME_MS
24-
state.key_expiration = rdb.read(8).unpack('Q').first
24+
state.key_expiration = rdb.read(8).unpack('Q').first * 1000
2525
state.info[:precision] = :millisecond
2626
state.key_type_id = rdb.readbyte
2727

2828
when Opcode::EXPIRETIME
29-
state.key_expiration = rdb.read(4).unpack('L').first * 1000
29+
state.key_expiration = rdb.read(4).unpack('L').first * 1000000
3030
state.info[:precision] = :second
3131
state.key_type_id = rdb.readbyte
3232

@@ -65,7 +65,7 @@ def read_rdb_version(rdb)
6565
signature, version = rdb_header[0..4], rdb_header[5..9].to_i
6666

6767
raise ReaderError, 'Wrong signature trying to load DB from file' if signature != 'REDIS'
68-
raise ReaderError, "Can't handle RDB format version #{version}" if version < 1 or version > 4
68+
raise ReaderError, "Can't handle RDB format version #{version}" if version < 1 or version > 6
6969

7070
version
7171
end
@@ -288,6 +288,12 @@ def read_ziplist_entry(rdb, state)
288288
rdb.read(4).unpack('L').first
289289
elsif header >> 4 == 14
290290
rdb.read(8).unpack('Q').first
291+
elsif header == 240
292+
"0#{rdb.read(3)}".unpack('l').first
293+
elsif header == 254
294+
rdb.read(1).unpack('c').first
295+
elsif header >= 241 && header <= 253
296+
header - 241
291297
else
292298
raise ReaderError, "Invalid entry header - #{header}"
293299
end
@@ -325,9 +331,8 @@ def read_zipmap(rdb, state)
325331
def read_zipmap_next_length(rdb)
326332
length = rdb.readbyte
327333
case length
328-
when 1..252 then length
329-
when 253 then rdb.read(4).unpack('L').first
330-
when 254 then raise ReaderError, "Unexpected value for length field of zipmap - #{length}"
334+
when 1..253 then length
335+
when 254 then rdb.read(4).unpack('L').first
331336
else nil
332337
end
333338
end

test/helpers.rb

+4
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ def read_test_rdb(filename, options)
33
options[:callbacks]
44
end
55

6+
def pexpireat_to_time(pexpireat)
7+
Time.at(pexpireat / 1000000, pexpireat % 1000000).utc
8+
end
9+
610
class TestCallbacks
711
include RDB::ReaderCallbacks
812

test/rdb/database_empty.rdb

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
REDIS0003�
1+
REDIS0006�ܳC�Z��V

test/rdb/hash_with_big_values.rdb

1.13 KB
Binary file not shown.

test/rdb/keys_with_expiration.rdb

1 Byte
Binary file not shown.
59 Bytes
Binary file not shown.

test/test_reader.rb

+60-7
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
rdb = read_test_rdb('database_empty.rdb', options)
1515

1616
events = [
17-
[:start_rdb, [3]],
17+
[:start_rdb, [6]],
1818
[:end_rdb, []],
1919
]
2020

@@ -61,15 +61,16 @@
6161
rdb = read_test_rdb('keys_with_expiration.rdb', options)
6262

6363
events = [
64-
[:start_rdb, [3]],
64+
[:start_rdb, [4]],
6565
[:start_database, [0]],
66-
[:set, ['expires_ms_precision', '2022-12-25 10:11:12.000573']],
67-
[:pexpireat, ['expires_ms_precision', 1671943272573000]],
66+
[:set, ['expires_ms_precision', '2022-12-25 10:11:12.573 UTC']],
67+
[:pexpireat, ['expires_ms_precision', 1671963072573000]],
6868
[:end_database, [0]],
6969
[:end_rdb, []],
7070
]
7171

7272
assert events == rdb.events
73+
assert Time.parse(rdb.events[2][1][1]) == pexpireat_to_time(rdb.events[3][1][1])
7374
end
7475

7576
test 'should read LZF-compressed key strings' do |options|
@@ -118,12 +119,32 @@
118119
rdb = read_test_rdb('list_of_integers_as_ziplist.rdb', options)
119120

120121
events = [
121-
[:start_rdb, [3]],
122+
[:start_rdb, [6]],
122123
[:start_database, [0]],
123-
[:start_list, ['ziplist_with_integers', 4]],
124+
[:start_list, ['ziplist_with_integers', 24]],
125+
[:rpush, ['ziplist_with_integers', 0]],
126+
[:rpush, ['ziplist_with_integers', 1]],
127+
[:rpush, ['ziplist_with_integers', 2]],
128+
[:rpush, ['ziplist_with_integers', 3]],
129+
[:rpush, ['ziplist_with_integers', 4]],
130+
[:rpush, ['ziplist_with_integers', 5]],
131+
[:rpush, ['ziplist_with_integers', 6]],
132+
[:rpush, ['ziplist_with_integers', 7]],
133+
[:rpush, ['ziplist_with_integers', 8]],
134+
[:rpush, ['ziplist_with_integers', 9]],
135+
[:rpush, ['ziplist_with_integers', 10]],
136+
[:rpush, ['ziplist_with_integers', 11]],
137+
[:rpush, ['ziplist_with_integers', 12]],
138+
[:rpush, ['ziplist_with_integers', -2]],
139+
[:rpush, ['ziplist_with_integers', 13]],
140+
[:rpush, ['ziplist_with_integers', 25]],
141+
[:rpush, ['ziplist_with_integers', -61]],
124142
[:rpush, ['ziplist_with_integers', 63]],
125143
[:rpush, ['ziplist_with_integers', 16380]],
126-
[:rpush, ['ziplist_with_integers', 65535]],
144+
[:rpush, ['ziplist_with_integers', 49536]],
145+
[:rpush, ['ziplist_with_integers', 16777008]],
146+
[:rpush, ['ziplist_with_integers', -16773840]],
147+
[:rpush, ['ziplist_with_integers', 1073741872]],
127148
[:rpush, ['ziplist_with_integers', 9223372036854775807]],
128149
[:end_list, ['ziplist_with_integers']],
129150
[:end_database, [0]],
@@ -341,6 +362,38 @@
341362
assert events == rdb.events
342363
end
343364

365+
test 'should handle hashes with values between 253 and 255 bytes encoded as zipmaps' do |options|
366+
rdb = read_test_rdb('hash_with_big_values.rdb', options)
367+
368+
events = [
369+
[:start_rdb, [2]],
370+
[:start_database, [0]],
371+
[:start_hash, ['zipmap_with_big_values', 4]],
372+
[:hset, ['zipmap_with_big_values', '253bytes', 'NYKK5QA4TDYJFZH0FCVT39DWI89IH7HV9HV162MULYY9S6H67MGS6YZJ54Q2NISW'+
373+
'9U69VC6ZK3OJV6J095P0P5YNSEHGCBJGYNZ8BPK3GEFBB8ZMGPT2Y33WNSETHINM'+
374+
'SZ4VKWUE8CXE0Y9FO7L5ZZ02EO26TLXF5NUQ0KMA98973QY62ZO1M1WDDZNS25F3'+
375+
'7KGBQ8W4R5V1YJRR2XNSQKZ4VY7GW6X038UYQG30ZM0JY1NNMJ12BKQPF2IDQ']],
376+
[:hset, ['zipmap_with_big_values', '254bytes', 'IZ3PNCQQV5RG4XOAXDN7IPWJKEK0LWRARBE3393UYD89PSQFC40AG4RCNW2M4YAV'+
377+
'JR0WD8AVO2F8KFDGUV0TGU8GF8M2HZLZ9RDX6V0XKIOXJJ3EMWQGFEY7E56RAOPT'+
378+
'A60G6SQRZ59ZBUKA6OMEW3K0LH464C7XKAX3K8AXDUX63VGX99JDCW1W2KTXPQRN'+
379+
'1R1PY5LXNXPW7AAIYUM2PUKN2YN2MXWS5HR8TPMKYJIFTLK2DNQNGTVAWMULON']],
380+
[:hset, ['zipmap_with_big_values', '255bytes', '6EUW8XSNBHMEPY991GZVZH4ITUQVKXQYL7UBYS614RDQSE7BDRUW00M6Y4W6WUQB'+
381+
'DFVHH6V2EIAEQGLV72K4UY7XXKL6K6XH6IN4QVS15GU1AAH9UI40UXEA8IZ5CZRR'+
382+
'K6SAV3R3X283O2OO9KG4K0DG0HZX1MLFDQHXGCC96M9YUVKXOEC5X35Q4EKET0SD'+
383+
'FDSBF1QKGAVS9202EL7MP2KPOYAUKU1SZJW5OP30WAPSM9OG97EBHW2XOWGICZG']],
384+
[:hset, ['zipmap_with_big_values', '300bytes', 'IJXP54329MQ96A2M28QF6SFX3XGNWGAII3M32MSIMR0O478AMZKNXDUYD5JGMHJR'+
385+
'B9A85RZ3DC3AIS62YSDW2BDJ97IBSH7FKOVFWKJYS7XBMIBX0Z1WNLQRY7D27PFP'+
386+
'BBGBDFDCKL0FIOBYEADX6G5UK3B0XYMGS0379GRY6F0FY5Q9JUCJLGOGDNNP8XW3'+
387+
'SJX2L872UJZZL8G871G9THKYQ2WKPFEBIHOOTIGDNWC15NL5324W8FYDP97JHKCS'+
388+
'MLWXNMSTYIUE7F22ZGR4NZK3T0UTBZ2AFRCT5LMT3P6B']],
389+
[:end_hash, ['zipmap_with_big_values']],
390+
[:end_database, [0]],
391+
[:end_rdb, []],
392+
]
393+
394+
assert events == rdb.events
395+
end
396+
344397
test 'should filter top-level objects before raising events' do |options|
345398
options[:callbacks].filter = lambda do |state|
346399
state.database == 2 && state.key.match(/second/) && state.key_type == :string

0 commit comments

Comments
 (0)