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 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