redis-copy 0.0.6 → 1.0.0.rc.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.
- 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
|