redis-copy 0.0.6 → 1.0.0.rc.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.travis.yml +1 -0
- data/README.md +28 -12
- data/lib/redis-copy.rb +8 -3
- data/lib/redis-copy/cli.rb +34 -31
- data/lib/redis-copy/key-emitter.rb +11 -66
- data/lib/redis-copy/key-emitter/interface.spec.rb +79 -0
- data/lib/redis-copy/key-emitter/keys.rb +39 -0
- data/lib/redis-copy/key-emitter/scan.rb +20 -0
- data/lib/redis-copy/strategy.rb +5 -23
- data/lib/redis-copy/strategy/classic.rb +1 -5
- data/lib/redis-copy/strategy/{new.rb → dump-restore.rb} +11 -10
- data/lib/redis-copy/strategy/interface.spec.rb +299 -0
- data/lib/redis-copy/ui.rb +5 -9
- data/lib/redis-copy/ui/auto_run.rb +1 -1
- data/lib/redis-copy/ui/command_line.rb +3 -1
- data/lib/redis-copy/version.rb +1 -1
- data/redis-copy.gemspec +3 -0
- data/spec/redis-copy/{key-emitter_spec.rb → key-emitter/keys_spec.rb} +3 -34
- data/spec/redis-copy/key-emitter/scan_spec.rb +9 -0
- data/spec/redis-copy/strategy/classic_spec.rb +27 -0
- data/spec/redis-copy/strategy/dump-restore_spec.rb +9 -0
- data/spec/spec_helper.rb +2 -0
- metadata +36 -19
- data/.travis/Gemfile.redis-gem-3.0.lock +0 -44
- data/.travis/Gemfile.redis-gem-master.lock +0 -49
- data/spec/redis-copy/strategy_spec.rb +0 -314
data/.travis.yml
CHANGED
data/README.md
CHANGED
@@ -2,12 +2,11 @@
|
|
2
2
|
|
3
3
|
This utility provides a way to move the contents of one redis DB to another
|
4
4
|
redis DB. It is inspired by the [redis-copy.rb script][original] included in
|
5
|
-
the redis source, but
|
5
|
+
the redis source, but aims to always support all object types and to use the
|
6
|
+
most-efficient methods and commands available to your redis versions:
|
6
7
|
|
7
|
-
- all known data types (original supported `set`, `list`, and `string`,
|
8
|
-
dropping the others without warning)
|
9
8
|
- if available on both dbs, will use `DUMP`/`RESTORE` commands (redis v2.6+)
|
10
|
-
-
|
9
|
+
- if available on source db, will use `SCAN` instead of `KEYS` (redis v2.8+)
|
11
10
|
|
12
11
|
[original]: https://github.com/antirez/redis/commits/unstable/utils/redis-copy.rb
|
13
12
|
|
@@ -21,17 +20,18 @@ The current options can be grabbed using the `--help` flag.
|
|
21
20
|
|
22
21
|
```
|
23
22
|
$ redis-copy --help
|
24
|
-
redis-copy
|
23
|
+
redis-copy v1.0.0.rc.0 (with redis-rb 3.0.6)
|
25
24
|
Usage: redis-copy [options] <source> <destination>
|
26
25
|
<source> and <destination> must be redis connection uris
|
27
26
|
like [redis://][<username>:<password>@]<hostname>[:<port>][/<db>]
|
28
27
|
|
29
28
|
Specific options:
|
30
|
-
--
|
31
|
-
|
32
|
-
new: use redis DUMP and RESTORE commands (faster)
|
33
|
-
classic: migrates via multiple type-specific commands
|
29
|
+
--pattern PATTERN Only transfer matching keys (default *)
|
30
|
+
See http://redis.io/commands/keys for more info.
|
34
31
|
--[no-]pipeline Use redis pipeline where available (default true)
|
32
|
+
-r, --require FILENAME Require a script; useful for loading third-party
|
33
|
+
implementations of key-emitter or copy strategies.
|
34
|
+
Relative paths *must* begin with `../' or `./'.
|
35
35
|
-d, --[no-]debug Write debug output (default false)
|
36
36
|
-t, --[no-]trace Enable backtrace on failure (default false)
|
37
37
|
-f, --[no-]fail-fast Abort on first failure (default false)
|
@@ -44,11 +44,11 @@ Specific options:
|
|
44
44
|
## Example:
|
45
45
|
|
46
46
|
```
|
47
|
-
$ redis-copy --
|
47
|
+
$ redis-copy --no-prompt old.redis.host/9 new.redis.host:6380/3
|
48
48
|
Source: redis://old.redis.host:6379/9
|
49
49
|
Destination: redis://new.redis.host:6380/3 (empty)
|
50
|
-
Key Emitter:
|
51
|
-
Strategy:
|
50
|
+
Key Emitter: Scan
|
51
|
+
Strategy: DumpRestore
|
52
52
|
PROGRESS {:success=>1000, :attempt=>1000}
|
53
53
|
PROGRESS {:success=>2000, :attempt=>2000}
|
54
54
|
PROGRESS {:success=>3000, :attempt=>3000}
|
@@ -56,6 +56,22 @@ PROGRESS {:success=>4000, :attempt=>4000}
|
|
56
56
|
DONE: {:success=>4246, :attempt=>4246}
|
57
57
|
```
|
58
58
|
|
59
|
+
## Extensibility:
|
60
|
+
|
61
|
+
`RedisCopy` uses the [implements][] gem to define interfaces for key-emitter
|
62
|
+
and copy strategies, so implementations can be supplied by third-parties,
|
63
|
+
secondary gems, or even a local script; the interface shared examples are even
|
64
|
+
available on your load-path so you can ensure your implementation adheres to
|
65
|
+
the interface.
|
66
|
+
|
67
|
+
See the existing implementations and their specs for examples, and use the
|
68
|
+
`--require` command-line flag to load up your own. Since `implements` treats
|
69
|
+
last-loaded implementations as inherently better, `RedisCopy` will automatically
|
70
|
+
pick up your implementation and attempt to use it before the bundled
|
71
|
+
implementations.
|
72
|
+
|
73
|
+
[implements]: https://rubygems.org/gems/implements
|
74
|
+
|
59
75
|
## Contributing
|
60
76
|
|
61
77
|
1. Fork it
|
data/lib/redis-copy.rb
CHANGED
@@ -3,6 +3,7 @@
|
|
3
3
|
require 'redis'
|
4
4
|
require 'active_support/inflector'
|
5
5
|
require 'active_support/core_ext/string/strip' # String#strip_heredoc
|
6
|
+
require 'implements/global'
|
6
7
|
|
7
8
|
require 'redis-copy/version'
|
8
9
|
require 'redis-copy/ui'
|
@@ -15,21 +16,22 @@ module RedisCopy
|
|
15
16
|
# @param destination [String]
|
16
17
|
# @options options [Hash<Symbol,Object>]
|
17
18
|
def copy(source, destination, options = {})
|
18
|
-
ui = UI.
|
19
|
+
ui = UI.new(options)
|
19
20
|
|
20
21
|
source = redis_from(source)
|
21
22
|
destination = redis_from(destination)
|
22
23
|
|
23
24
|
ui.abort('source cannot equal destination!') if same_redis?(source, destination)
|
24
25
|
|
25
|
-
key_emitter = KeyEmitter.
|
26
|
-
strategem = Strategy.
|
26
|
+
key_emitter = KeyEmitter.new(source, ui, options)
|
27
|
+
strategem = Strategy.new(source, destination, ui, options)
|
27
28
|
|
28
29
|
dest_empty = !(destination.randomkey) # randomkey returns string unless db empty.
|
29
30
|
|
30
31
|
return false unless ui.confirm? <<-EODESC.strip_heredoc
|
31
32
|
Source: #{source.client.id}
|
32
33
|
Destination: #{destination.client.id} (#{dest_empty ? '' : 'NOT '}empty)
|
34
|
+
Pattern: #{options[:pattern]}
|
33
35
|
Key Emitter: #{key_emitter}
|
34
36
|
Strategy: #{strategem}
|
35
37
|
EODESC
|
@@ -60,6 +62,9 @@ module RedisCopy
|
|
60
62
|
end.tap do |stats|
|
61
63
|
ui.notify("DONE: #{stats.inspect}")
|
62
64
|
end
|
65
|
+
rescue Implements::Implementation::NotFound => e
|
66
|
+
ui.notify(e.to_s)
|
67
|
+
ui.abort
|
63
68
|
end
|
64
69
|
|
65
70
|
private
|
data/lib/redis-copy/cli.rb
CHANGED
@@ -8,8 +8,6 @@ module RedisCopy
|
|
8
8
|
REDIS_URI = (/\A(?:redis:\/\/)?(\w*:\w+@)?([a-z0-9\-.]+)(:[0-9]{1,5})?(\/(?:(?:1[0-5])|[0-9]))?\z/i).freeze
|
9
9
|
DEFAULTS = {
|
10
10
|
ui: :command_line,
|
11
|
-
key_emitter: :auto,
|
12
|
-
strategy: :auto,
|
13
11
|
verify: 0,
|
14
12
|
pipeline: :true,
|
15
13
|
fail_fast: false,
|
@@ -17,6 +15,7 @@ module RedisCopy
|
|
17
15
|
trace: false,
|
18
16
|
debug: false,
|
19
17
|
allow_nonempty: false,
|
18
|
+
pattern: '*',
|
20
19
|
}.freeze unless defined?(DEFAULTS)
|
21
20
|
|
22
21
|
def initialize(argv = ARGV)
|
@@ -37,26 +36,11 @@ module RedisCopy
|
|
37
36
|
opts.separator ''
|
38
37
|
opts.separator "Specific options:"
|
39
38
|
|
40
|
-
opts.on('--
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
" classic: migrates via multiple type-specific commands"
|
46
|
-
)
|
47
|
-
) do |strategy|
|
48
|
-
options[:strategy] = strategy
|
49
|
-
end
|
50
|
-
|
51
|
-
opts.on('--emitter EMITTER', [:auto, :scan, :keys],
|
52
|
-
indent_desc.(
|
53
|
-
"Select key emitter (auto, keys, scan) (default #{DEFAULTS[:strategy]})\n" +
|
54
|
-
" auto: uses scan if available, otherwise fallback\n" +
|
55
|
-
" scan: use redis SCAN command (faster, less blocking)\n" +
|
56
|
-
" keys: uses redis KEYS command (dangerous, esp. on large datasets)"
|
57
|
-
)
|
58
|
-
) do |emitter|
|
59
|
-
options[:key_emitter] = emitter
|
39
|
+
opts.on('--pattern PATTERN', indent_desc[
|
40
|
+
"Only transfer matching keys (default #{DEFAULTS[:pattern]})\n" +
|
41
|
+
"See http://redis.io/commands/keys for more info."
|
42
|
+
]) do |pattern|
|
43
|
+
options[:pattern] = pattern
|
60
44
|
end
|
61
45
|
|
62
46
|
opts.on('--[no-]pipeline',
|
@@ -65,6 +49,20 @@ module RedisCopy
|
|
65
49
|
options[:pipeline] = pipeline
|
66
50
|
end
|
67
51
|
|
52
|
+
opts.on('-r', '--require FILENAME', indent_desc.(
|
53
|
+
"Require a script; useful for loading third-party\n" +
|
54
|
+
"implementations of key-emitter or copy strategies.\n" +
|
55
|
+
"Relative paths *must* begin with `../' or `./'.")
|
56
|
+
) do |script|
|
57
|
+
begin
|
58
|
+
script = File.expand_path(script) if script[/\A..?\//]
|
59
|
+
require script
|
60
|
+
rescue LoadError => e
|
61
|
+
$stderr.puts e.message
|
62
|
+
exit 1
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
68
66
|
opts.on('-d', '--[no-]debug', "Write debug output (default #{DEFAULTS[:debug]})") do |debug|
|
69
67
|
options[:debug] = debug
|
70
68
|
end
|
@@ -102,16 +100,21 @@ module RedisCopy
|
|
102
100
|
options[:dry_run] = true
|
103
101
|
end
|
104
102
|
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
103
|
+
begin
|
104
|
+
opts.parse!(argv)
|
105
|
+
unless argv.size == 2
|
106
|
+
opts.abort "Source and Destination must be specified\n\n" +
|
107
|
+
opts.help
|
108
|
+
end
|
109
|
+
@source = argv.shift
|
110
|
+
@destination = argv.shift
|
111
|
+
|
112
|
+
opts.abort "source is not valid URI" unless @source =~ REDIS_URI
|
113
|
+
opts.abort "destination is not valid URI" unless @destination =~ REDIS_URI
|
114
|
+
rescue OptionParser::ParseError => error
|
115
|
+
$stderr.puts error
|
116
|
+
exit 1
|
109
117
|
end
|
110
|
-
@source = argv.shift
|
111
|
-
@destination = argv.shift
|
112
|
-
|
113
|
-
opts.abort "source is not valid URI" unless @source =~ REDIS_URI
|
114
|
-
opts.abort "destination is not valid URI" unless @destination =~ REDIS_URI
|
115
118
|
end
|
116
119
|
|
117
120
|
@config = DEFAULTS.merge(options)
|
@@ -7,21 +7,11 @@ module RedisCopy
|
|
7
7
|
# but should allow smarter implementations to be built that can handle
|
8
8
|
# billion-key dbs without blocking on IO.
|
9
9
|
module KeyEmitter
|
10
|
-
|
11
|
-
key_emitter = options.fetch(:key_emitter, :default)
|
12
|
-
scan_compatible = Scan::compatible?(redis)
|
13
|
-
emitklass = case key_emitter
|
14
|
-
when :keys then Keys
|
15
|
-
when :scan
|
16
|
-
raise ArgumentError unless scan_compatible
|
17
|
-
Scan
|
18
|
-
when :auto then scan_compatible ? Scan : Keys
|
19
|
-
end
|
20
|
-
emitklass.new(redis, ui, options)
|
21
|
-
end
|
10
|
+
extend Implements::Interface
|
22
11
|
|
23
12
|
# @param redis [Redis]
|
24
13
|
# @param options [Hash<Symbol:String>]
|
14
|
+
# @option options [String] :pattern ('*')
|
25
15
|
def initialize(redis, ui, options = {})
|
26
16
|
@redis = redis
|
27
17
|
@ui = ui
|
@@ -34,65 +24,20 @@ module RedisCopy
|
|
34
24
|
raise NotImplementedError
|
35
25
|
end
|
36
26
|
|
27
|
+
def pattern
|
28
|
+
@pattern ||= @options.fetch(:pattern) { '*' }
|
29
|
+
end
|
30
|
+
|
37
31
|
def dbsize
|
38
32
|
@redis.dbsize
|
39
33
|
end
|
40
34
|
|
41
35
|
def to_s
|
42
|
-
self.class.name.demodulize
|
43
|
-
end
|
44
|
-
|
45
|
-
# The default strategy blindly uses `redis.keys('*')`
|
46
|
-
class Keys
|
47
|
-
include KeyEmitter
|
48
|
-
|
49
|
-
def keys
|
50
|
-
dbsize = self.dbsize
|
51
|
-
|
52
|
-
# HT: http://stackoverflow.com/a/11466770
|
53
|
-
dbsize_str = dbsize.to_s.reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse
|
54
|
-
|
55
|
-
@ui.abort unless (dbsize < 10_000) || (@ui.confirm? <<-EOWARNING.strip_heredoc)
|
56
|
-
WARNING: #{self} key emitter uses redis.keys('*') to
|
57
|
-
get its list of keys, and you have #{dbsize_str} keys in
|
58
|
-
your source DB.
|
59
|
-
|
60
|
-
The redis keys command [reference](http://redis.io/commands/keys)
|
61
|
-
says this:
|
62
|
-
|
63
|
-
> Warning: consider KEYS as a command that should only be used
|
64
|
-
> in production environments with extreme care. It may ruin
|
65
|
-
> performance when it is executed against large databases.
|
66
|
-
> This command is intended for debugging and special operations,
|
67
|
-
> such as changing your keyspace layout. Don't use KEYS in your
|
68
|
-
> regular application code. If you're looking for a way to find
|
69
|
-
> keys in a subset of your keyspace, consider using sets.
|
70
|
-
EOWARNING
|
71
|
-
|
72
|
-
@ui.debug "REDIS: #{@redis.client.id} KEYS *"
|
73
|
-
@redis.keys('*').to_enum
|
74
|
-
end
|
75
|
-
|
76
|
-
def self.compatible?(redis)
|
77
|
-
true
|
78
|
-
end
|
79
|
-
end
|
80
|
-
|
81
|
-
class Scan
|
82
|
-
include KeyEmitter
|
83
|
-
|
84
|
-
def keys
|
85
|
-
@redis.scan_each(count: 1000)
|
86
|
-
end
|
87
|
-
|
88
|
-
def self.compatible?(redis)
|
89
|
-
bin_version = Gem::Version.new(redis.info['redis_version'])
|
90
|
-
bin_requirement = Gem::Requirement.new('>= 2.7.105')
|
91
|
-
|
92
|
-
return false unless bin_requirement.satisfied_by?(bin_version)
|
93
|
-
|
94
|
-
redis.respond_to?(:scan_each)
|
95
|
-
end
|
36
|
+
self.class.name.demodulize
|
96
37
|
end
|
97
38
|
end
|
98
39
|
end
|
40
|
+
|
41
|
+
# Load the bundled key-emitters:
|
42
|
+
require_relative 'key-emitter/keys'
|
43
|
+
require_relative 'key-emitter/scan'
|
@@ -0,0 +1,79 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
# The shared examples for RedisCopy::KeyEmitter are available to require
|
4
|
+
# into consuming libraries so they can verify their implementation of the
|
5
|
+
# RedisCopy::KeyEmitter interface. See the bundled specs for the bundled
|
6
|
+
# key-emitters for example usage.
|
7
|
+
if defined?(::RSpec)
|
8
|
+
shared_examples_for RedisCopy::KeyEmitter do
|
9
|
+
let(:resolved_implementation) do
|
10
|
+
begin
|
11
|
+
instance
|
12
|
+
true
|
13
|
+
rescue Implements::Implementation::NotFound
|
14
|
+
false
|
15
|
+
end
|
16
|
+
end
|
17
|
+
let(:emitter_klass) { described_class }
|
18
|
+
let(:redis) { Redis.new(REDIS_OPTIONS).tap(&:ping) }
|
19
|
+
let(:ui) { double.as_null_object }
|
20
|
+
let(:selector) { emitter_klass.name.underscore.dasherize } # see implements gem
|
21
|
+
let(:instance) { RedisCopy::KeyEmitter.implementation(selector).new(redis, ui, options) }
|
22
|
+
let(:options) { Hash.new }
|
23
|
+
let(:key_count) { 1 }
|
24
|
+
let(:keys) { key_count.times.map{|i| i.to_s(16) } }
|
25
|
+
let(:glob_matcher) do
|
26
|
+
lambda do |rglob|
|
27
|
+
fglob = rglob.gsub('*','**')
|
28
|
+
lambda do |key|
|
29
|
+
File::fnmatch(fglob,key)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
before(:each) do
|
35
|
+
unless resolved_implementation
|
36
|
+
pending "#{emitter_klass} not supported in your environment"
|
37
|
+
end
|
38
|
+
key_count.times.each_slice(50) do |keys|
|
39
|
+
kv = keys.map{|x| x.to_s(16)}.zip(keys)
|
40
|
+
redis.mset(*kv.flatten)
|
41
|
+
end
|
42
|
+
ui.stub(:debug).with(anything)
|
43
|
+
end
|
44
|
+
after(:each) { redis.flushdb }
|
45
|
+
|
46
|
+
context '#keys' do
|
47
|
+
let(:key_count) { 64 }
|
48
|
+
context 'the result' do
|
49
|
+
subject { instance.keys }
|
50
|
+
its(:to_a) { should =~ keys }
|
51
|
+
end
|
52
|
+
context 'with pattern "[139]*"' do
|
53
|
+
let(:options) { {pattern: '[139]*'} }
|
54
|
+
context 'the result' do
|
55
|
+
let(:key_count) { 256 }
|
56
|
+
subject { instance.keys }
|
57
|
+
its(:to_a) { should =~ keys.select(&glob_matcher['[139]*']) }
|
58
|
+
end
|
59
|
+
end
|
60
|
+
context 'with pattern "?[2468ace]"' do
|
61
|
+
let(:options) { {pattern: '?[2468ace]'} }
|
62
|
+
context 'the result' do
|
63
|
+
let(:key_count) { 256 }
|
64
|
+
subject { instance.keys }
|
65
|
+
its(:to_a) { should =~ keys.select(&glob_matcher['?[2468ace]']) }
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
context 'implementation resolution' do
|
71
|
+
subject { instance }
|
72
|
+
its(:class) { should eq described_class }
|
73
|
+
end
|
74
|
+
end
|
75
|
+
else
|
76
|
+
fail(LoadError,
|
77
|
+
"#{__FILE__} contains shared examples for RedisCopy::KeyEmitter. " +
|
78
|
+
"Require it in your specs, not your code.")
|
79
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module RedisCopy
|
4
|
+
# Keys uses the KEYS command, which has always been available in Redis,
|
5
|
+
# but comes with a rather staunch warning to *never* use it in production.
|
6
|
+
# This is the first-required implementation of KeyEmitter, and is thus the
|
7
|
+
# fallback for no other emitters are available. A Warning Prompt will appear
|
8
|
+
# if your dbsize is greater than 10,000 keys.
|
9
|
+
class KeyEmitter::Keys
|
10
|
+
implements KeyEmitter
|
11
|
+
|
12
|
+
def keys
|
13
|
+
dbsize = self.dbsize
|
14
|
+
|
15
|
+
# HT: http://stackoverflow.com/a/11466770
|
16
|
+
dbsize_str = dbsize.to_s.reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse
|
17
|
+
|
18
|
+
@ui.abort unless (dbsize < 10_000) || (@ui.confirm? <<-EOWARNING.strip_heredoc)
|
19
|
+
WARNING: #{self} key emitter uses redis.keys('*') to
|
20
|
+
get its list of keys, and you have #{dbsize_str} keys in
|
21
|
+
your source DB.
|
22
|
+
|
23
|
+
The redis keys command [reference](http://redis.io/commands/keys)
|
24
|
+
says this:
|
25
|
+
|
26
|
+
> Warning: consider KEYS as a command that should only be used
|
27
|
+
> in production environments with extreme care. It may ruin
|
28
|
+
> performance when it is executed against large databases.
|
29
|
+
> This command is intended for debugging and special operations,
|
30
|
+
> such as changing your keyspace layout. Don't use KEYS in your
|
31
|
+
> regular application code. If you're looking for a way to find
|
32
|
+
> keys in a subset of your keyspace, consider using sets.
|
33
|
+
EOWARNING
|
34
|
+
|
35
|
+
@ui.debug "REDIS: #{@redis.client.id} KEYS #{pattern}"
|
36
|
+
@redis.keys(pattern).to_enum
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|