redis-rdb 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. data/.gitignore +2 -0
  2. data/LICENSE +22 -0
  3. data/README.md +91 -0
  4. data/Rakefile +7 -0
  5. data/examples/aof_dumper.rb +8 -0
  6. data/examples/read_rdb.rb +9 -0
  7. data/lib/rdb.rb +10 -0
  8. data/lib/rdb/callbacks.rb +129 -0
  9. data/lib/rdb/constants.rb +35 -0
  10. data/lib/rdb/dumper.rb +37 -0
  11. data/lib/rdb/dumpers/aof.rb +112 -0
  12. data/lib/rdb/errors.rb +4 -0
  13. data/lib/rdb/lzf.rb +48 -0
  14. data/lib/rdb/reader-state.rb +25 -0
  15. data/lib/rdb/reader.rb +387 -0
  16. data/lib/rdb/version.rb +3 -0
  17. data/redis-rdb.gemspec +30 -0
  18. data/test/helpers.rb +377 -0
  19. data/test/rdb/database_empty.rdb +1 -0
  20. data/test/rdb/database_multiple_logical_dbs.rdb +0 -0
  21. data/test/rdb/hash_as_ziplist.rdb +0 -0
  22. data/test/rdb/hash_normal.rdb +0 -0
  23. data/test/rdb/hash_with_big_values.rdb +0 -0
  24. data/test/rdb/hash_with_compressed_strings_as_zipmap.rdb +0 -0
  25. data/test/rdb/hash_with_uncompressed_strings_as_zipmap.rdb +0 -0
  26. data/test/rdb/keys_compressed.rdb +0 -0
  27. data/test/rdb/keys_integer.rdb +0 -0
  28. data/test/rdb/keys_uncompressed.rdb +0 -0
  29. data/test/rdb/keys_with_expiration.rdb +0 -0
  30. data/test/rdb/list_normal.rdb +0 -0
  31. data/test/rdb/list_of_compressed_strings_as_ziplist.rdb +0 -0
  32. data/test/rdb/list_of_integers_as_ziplist.rdb +0 -0
  33. data/test/rdb/list_of_uncompressed_strings_as_ziplist.rdb +0 -0
  34. data/test/rdb/set_as_intset_16bits.rdb +0 -0
  35. data/test/rdb/set_as_intset_32bits.rdb +0 -0
  36. data/test/rdb/set_as_intset_64bits.rdb +0 -0
  37. data/test/rdb/set_normal.rdb +0 -0
  38. data/test/rdb/sortedset_as_ziplist.rdb +0 -0
  39. data/test/rdb/sortedset_normal.rdb +0 -0
  40. data/test/test_reader.rb +416 -0
  41. metadata +109 -0
@@ -0,0 +1,2 @@
1
+ *.gem
2
+ *.aof
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Daniele Alessandri
2
+
3
+ Permission is hereby granted, free of charge, to any person
4
+ obtaining a copy of this software and associated documentation
5
+ files (the "Software"), to deal in the Software without
6
+ restriction, including without limitation the rights to use,
7
+ copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the
9
+ Software is furnished to do so, subject to the following
10
+ conditions:
11
+
12
+ The above copyright notice and this permission notice shall be
13
+ included in all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
17
+ OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
19
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
20
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22
+ OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,91 @@
1
+ # redis-rdb #
2
+
3
+ This library provides a set of modules and classes that make it easy to handle binary
4
+ database dumps generated by [Redis](http://redis.io) (.rdb files) in Ruby.
5
+
6
+ Currently redis-rdb allows developers to read .rdb files in a streamable flashion
7
+ with `RDB::Reader` by providing a set of callbacks. Database objects can optionally
8
+ be filtered by database / key / type using custom filters.
9
+
10
+ ```ruby
11
+ require 'rdb'
12
+
13
+ class MyCallbacks
14
+ include RDB::ReaderCallbacks
15
+
16
+ KEY_SELECTOR = Regexp.compile(/user:\d+/)
17
+
18
+ def accept_key?(state)
19
+ state.database == 15 && KEY_SELECTOR.match(state.key)
20
+ end
21
+
22
+ def set(key, value, state)
23
+ puts "SET \"#{key}\" \"#{value}\""
24
+ end
25
+ end
26
+
27
+ RDB::Reader.read_file('dump.rdb', callbacks: MyCallbacks.new)
28
+ ```
29
+
30
+ For more details about the supported callbacks you can take a look at the source code in
31
+ [lib/rdb/callbacks.rb](https://github.com/nrk/redis-rdb/blob/master/lib/rdb/callbacks.rb).
32
+
33
+ ### Data dumpers ###
34
+
35
+ The `RDB::Dumper` module can be used to create classes that dump the data read from an .rdb file
36
+ into a new file using a different format. An example would be to create an AOF file for Redis or
37
+ to store the data into JSON or CSV. This is an example of using `RDB::Dumpers::AOF`:
38
+
39
+ ```ruby
40
+ require 'rdb'
41
+
42
+ source = 'test/rdb/database_multiple_logical_dbs.rdb'
43
+ destination = File.basename(source, '.rdb') + '.aof'
44
+
45
+ dumper = RDB::Dumpers::AOF.new(source, destination)
46
+ dumper.dump
47
+ ```
48
+
49
+ A dumper really is no more than a slightly augmented version of a class defining the callbacks
50
+ for `RDB::Reader` with a few additional helper methods. You can find more about how to implement
51
+ dumpers in [lib/rdb/dumper.rb](https://github.com/nrk/redis-rdb/blob/master/lib/rdb/dumper.rb)
52
+ and [lib/rdb/dumpers/aof.rb](https://github.com/nrk/redis-rdb/blob/master/lib/rdb/dumpers/aof.rb).
53
+
54
+ ## RDB file format ##
55
+
56
+ Right now there is still no official documentation about the binary format of .rdb files beside
57
+ the code in [rdb.c](https://github.com/antirez/redis/blob/unstable/src/rdb.c) as the reference
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
70
+ .rdb files shipped with his project for testing.
71
+
72
+ ## Dependencies ##
73
+ - Ruby >= 1.9.0
74
+
75
+ ## Links ##
76
+
77
+ ### Project ###
78
+ - [Source code](https://github.com/nrk/redis-rdb/)
79
+ - [Issue tracker](https://github.com/nrk/redis-rdb/issues)
80
+
81
+ ### Related ###
82
+ - [Redis](http://redis.io/)
83
+ - [redis-rdb-tools](https://github.com/sripathikrishnan/redis-rdb-tools) (Python)
84
+
85
+ ## Author ##
86
+
87
+ - [Daniele Alessandri](mailto:suppakilla@gmail.com) ([twitter](http://twitter.com/JoL1hAHN))
88
+
89
+ ## License ##
90
+
91
+ The code for redis-rdb is distributed under the terms of the MIT license (see LICENSE).
@@ -0,0 +1,7 @@
1
+ require 'cutest'
2
+
3
+ task :test do
4
+ Cutest.run(Dir['test/test_*.rb'])
5
+ end
6
+
7
+ task :default => :test
@@ -0,0 +1,8 @@
1
+ $:.unshift File.expand_path('../lib', File.dirname(__FILE__))
2
+
3
+ require 'rdb'
4
+
5
+ source = 'test/rdb/database_multiple_logical_dbs.rdb'
6
+ destination = File.basename(source, '.rdb') + '.aof'
7
+
8
+ RDB::Dumpers::AOF.new(source, destination, variadic: true).dump
@@ -0,0 +1,9 @@
1
+ $:.unshift File.expand_path('../lib', File.dirname(__FILE__))
2
+
3
+ require 'rdb'
4
+
5
+ options = {
6
+ callbacks: RDB::DebugCallbacks.new,
7
+ }
8
+
9
+ RDB::Reader.read_file('test/rdb/database_multiple_logical_dbs.rdb', options)
@@ -0,0 +1,10 @@
1
+ require 'stringio'
2
+ require 'rdb/version'
3
+ require 'rdb/constants'
4
+ require 'rdb/errors'
5
+ require 'rdb/lzf'
6
+ require 'rdb/callbacks'
7
+ require 'rdb/reader-state'
8
+ require 'rdb/reader'
9
+ require 'rdb/dumper'
10
+ require 'rdb/dumpers/aof'
@@ -0,0 +1,129 @@
1
+ module RDB
2
+ module ReaderCallbacks
3
+ def accept_key?(state)
4
+ true
5
+ end
6
+
7
+ def start_rdb(rdb_version); end
8
+
9
+ def end_rdb(); end
10
+
11
+ def start_database(database); end
12
+
13
+ def end_database(database); end
14
+
15
+ def pexpireat(key, expiration, state); end
16
+
17
+ def set(key, value, state); end
18
+
19
+ def start_list(key, length, state); end
20
+
21
+ def rpush(key, value, state); end
22
+
23
+ def end_list(key, state); end
24
+
25
+ def start_set(key, length, state); end
26
+
27
+ def sadd(key, value, state); end
28
+
29
+ def end_set(key, state); end
30
+
31
+ def start_sortedset(key, length, state); end
32
+
33
+ def zadd(key, score, value, state); end
34
+
35
+ def end_sortedset(key, state); end
36
+
37
+ def start_hash(key, length, state); end
38
+
39
+ def hset(key, field, value, state); end
40
+
41
+ def end_hash(key, state); end
42
+
43
+ def skip_object(key, state); end
44
+ end
45
+
46
+ class EmptyCallbacks
47
+ include ReaderCallbacks
48
+ end
49
+
50
+ class DebugCallbacks
51
+ include ReaderCallbacks
52
+
53
+ def start_rdb(version)
54
+ puts "Start RDB file - version #{version}"
55
+ end
56
+
57
+ def end_rdb()
58
+ puts "Close RDB file"
59
+ end
60
+
61
+ def start_database(database)
62
+ puts "Open database #{database}."
63
+ end
64
+
65
+ def end_database(database)
66
+ puts "Close database #{database}."
67
+ end
68
+
69
+ def pexpireat(key, expiration, state)
70
+ puts "PEXPIREAT \"#{key}\" \"#{expiration}\""
71
+ end
72
+
73
+ def set(key, value, state)
74
+ puts "SET \"#{key}\" \"#{value}\""
75
+ end
76
+
77
+ def start_list(key, length, state)
78
+ puts "Start list \"#{key}\" of #{length} items."
79
+ end
80
+
81
+ def rpush(key, value, state)
82
+ puts "RPUSH \"#{key}\" \"#{value}\""
83
+ end
84
+
85
+ def end_list(key, state)
86
+ puts "End list \"#{key}\"."
87
+ end
88
+
89
+ def start_set(key, length, state)
90
+ puts "Start set \"#{key}\" of #{length} members."
91
+ end
92
+
93
+ def sadd(key, value, state)
94
+ puts "SADD \"#{key}\" \"#{value}\""
95
+ end
96
+
97
+ def end_set(key, state)
98
+ puts "End set \"#{key}\"."
99
+ end
100
+
101
+ def start_sortedset(key, length, state)
102
+ puts "Start sortedset \"#{key}\" of #{length} members."
103
+ end
104
+
105
+ def zadd(key, score, value, state)
106
+ puts "ZADD \"#{key}\" \"#{score}\" \"#{value}\""
107
+ end
108
+
109
+ def end_sortedset(key, state)
110
+ puts "End sortedset \"#{key}\"."
111
+ end
112
+
113
+ def start_hash(key, length, state)
114
+ puts "Start hash \"#{key}\" of #{length} members."
115
+ end
116
+
117
+ def hset(key, field, value, state)
118
+ puts "HSET \"#{key}\" \"#{field}\" \"#{value}\""
119
+ end
120
+
121
+ def end_hash(key, state)
122
+ puts "End hash \"#{key}\"."
123
+ end
124
+
125
+ def skip_object(key, state)
126
+ puts "Skipping object for key #{key} of type #{state.type}"
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,35 @@
1
+ module RDB
2
+ module Length
3
+ BITS_6 = 0
4
+ BITS_14 = 1
5
+ BITS_32 = 2
6
+ ENCODED = 3
7
+ end
8
+
9
+ module Encoding
10
+ INT8 = 0
11
+ INT16 = 1
12
+ INT32 = 2
13
+ LZF = 3
14
+ end
15
+
16
+ module Opcode
17
+ EXPIRETIME_MS = 252
18
+ EXPIRETIME = 253
19
+ SELECTDB = 254
20
+ EOF = 255
21
+ end
22
+
23
+ module Type
24
+ STRING = 0
25
+ LIST = 1
26
+ SET = 2
27
+ ZSET = 3
28
+ HASH = 4
29
+ HASH_ZIPMAP = 9
30
+ LIST_ZIPLIST = 10
31
+ SET_INTSET = 11
32
+ ZSET_ZIPLIST = 12
33
+ HASH_ZIPLIST = 13
34
+ end
35
+ end
@@ -0,0 +1,37 @@
1
+ module RDB
2
+ module Dumper
3
+ include ReaderCallbacks
4
+
5
+ def initialize(source, destination, options = {})
6
+ @source = source
7
+ @destination = destination
8
+ @options = options
9
+ @output = nil
10
+ end
11
+
12
+ def <<(buffer)
13
+ @output << buffer unless @output.nil?; nil
14
+ end
15
+
16
+ def with_streams(&block)
17
+ input = open(@source, 'rb') unless @source.kind_of? IO
18
+ output = open(@destination, 'wb') unless @destination.kind_of? IO
19
+
20
+ begin
21
+ block.call(input, output)
22
+ ensure
23
+ input.close
24
+ output.close
25
+ end
26
+ end
27
+
28
+ def dump
29
+ raise RuntimeException, 'Output stream already opened' if @output
30
+
31
+ with_streams do |input, output|
32
+ @output = output
33
+ Reader.read(input, callbacks: self)
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,112 @@
1
+ module RDB
2
+ module Dumpers
3
+ class AOF
4
+ include Dumper
5
+
6
+ REDIS_AOF_REWRITE_ITEMS_PER_CMD = 64
7
+
8
+ def start_database(database)
9
+ self << serialize_command(:select, [database])
10
+ end
11
+
12
+ def pexpireat(key, expiration, state)
13
+ command = if state.info[:precision] == :second
14
+ expiration = (expiration / 1000).to_i
15
+ :pexpire
16
+ else
17
+ :pexpireat
18
+ end
19
+ self << serialize_command(command, [key, expiration])
20
+ end
21
+
22
+ def set(key, value, state)
23
+ self << serialize_command(:set, [key, value])
24
+ end
25
+
26
+ def start_list(key, length, state)
27
+ reset_buffer(state)
28
+ end
29
+
30
+ def rpush(key, member, state)
31
+ handle(:rpush, state, key, member)
32
+ end
33
+
34
+ def end_list(key, state)
35
+ flush(:rpush, state)
36
+ end
37
+
38
+ def start_set(key, length, state)
39
+ reset_buffer(state)
40
+ end
41
+
42
+ def sadd(key, member, state)
43
+ handle(:sadd, state, key, member)
44
+ end
45
+
46
+ def end_set(key, state)
47
+ flush(:sadd, state)
48
+ end
49
+
50
+ def start_sortedset(key, length, state)
51
+ reset_buffer(state)
52
+ end
53
+
54
+ def zadd(key, score, member, state)
55
+ handle(:zadd, state, key, score, member)
56
+ end
57
+
58
+ def end_sortedset(key, state)
59
+ flush(:zadd, state)
60
+ end
61
+
62
+ def start_hash(key, length, state)
63
+ reset_buffer(state)
64
+ end
65
+
66
+ def hset(key, field, value, state)
67
+ handle(variadic? ? :hmset : :hset, state, key, field, value)
68
+ end
69
+
70
+ def end_hash(key, state)
71
+ flush(:hmset, state)
72
+ end
73
+
74
+ def handle(command, state, key, *arguments)
75
+ if variadic?
76
+ state.info[:buffer].push(arguments)
77
+ flush(command, state) if buffer_full?(state)
78
+ else
79
+ self << serialize_command(command, [key, *arguments])
80
+ end
81
+ end
82
+
83
+ def flush(command, state)
84
+ if buffer_some?(state)
85
+ self << serialize_command(command, [state.key] + state.info[:buffer].flatten)
86
+ reset_buffer(state)
87
+ end
88
+ end
89
+
90
+ def serialize_command(command, arguments)
91
+ buffer = "*#{arguments.length + 1}\r\n$#{command.length}\r\n#{command.upcase}\r\n"
92
+ buffer << arguments.map { |arg| "$#{arg.to_s.length}\r\n#{arg}\r\n" }.join
93
+ end
94
+
95
+ def variadic?
96
+ @options[:variadic] ||= false
97
+ end
98
+
99
+ def reset_buffer(state)
100
+ state.info[:buffer] = [];
101
+ end
102
+
103
+ def buffer_some?(state)
104
+ state.info[:buffer].length > 0
105
+ end
106
+
107
+ def buffer_full?(state)
108
+ state.info[:buffer].length == REDIS_AOF_REWRITE_ITEMS_PER_CMD
109
+ end
110
+ end
111
+ end
112
+ end