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 CHANGED
@@ -3,6 +3,7 @@ script: "bundle exec rake run"
3
3
 
4
4
  rvm:
5
5
  - 1.9.3
6
+ - 2.0.0
6
7
  gemfile:
7
8
  - .travis/Gemfile.redis-gem-3.0
8
9
  - .travis/Gemfile.redis-gem-master
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 supports the following additional features:
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
- - support for more than just db0
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 v0.0.5
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
- --strategy STRATEGY Select strategy (auto, new, classic) (default auto)
31
- auto: uses new if available, otherwise fallback
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 --fail-fast --yes old.redis.host/9 new.redis.host:6380/3
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: Default
51
- Strategy: New
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.load(options)
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.load(source, ui, options)
26
- strategem = Strategy.load(source, destination, ui, options)
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
@@ -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('--strategy STRATEGY', [:auto, :new, :classic],
41
- indent_desc.(
42
- "Select strategy (auto, new, classic) (default #{DEFAULTS[:strategy]})\n" +
43
- " auto: uses new if available, otherwise fallback\n" +
44
- " new: use redis DUMP and RESTORE commands (faster)\n" +
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
- opts.parse!(argv)
106
- unless argv.size == 2
107
- opts.abort "Source and Destination must be specified\n\n" +
108
- opts.help
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
- def self.load(redis, ui, options = {})
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.humanize
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