redis-copy 0.0.1

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.
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ # encoding: utf-8
2
+ source 'https://rubygems.org'
3
+
4
+ gemspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Ryan Biesemeyer
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,29 @@
1
+ # Redis::Copy
2
+
3
+ TODO: Write a gem description
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'redis-copy'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install redis-copy
18
+
19
+ ## Usage
20
+
21
+ TODO: Write usage instructions here
22
+
23
+ ## Contributing
24
+
25
+ 1. Fork it
26
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
27
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
28
+ 4. Push to the branch (`git push origin my-new-feature`)
29
+ 5. Create new Pull Request
@@ -0,0 +1,9 @@
1
+ # encoding: utf-8
2
+
3
+ require 'bundler/gem_tasks'
4
+
5
+ require 'rspec/core/rake_task'
6
+ RSpec::Core::RakeTask.new(:spec) do |spec|
7
+ spec.pattern = FileList['spec/**/*_spec.rb']
8
+ spec.verbose = true
9
+ end
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: utf-8
3
+ $LOAD_PATH.unshift(File.expand_path('../../lib', __FILE__))
4
+ require 'redis-copy/cli'
5
+
6
+ RedisCopy::CLI.new(ARGV).run!
@@ -0,0 +1,79 @@
1
+ # encoding: utf-8
2
+
3
+ require 'redis'
4
+ require 'active_support/inflector'
5
+ require 'active_support/core_ext/string/strip' # String#strip_heredoc
6
+
7
+ require 'redis-copy/version'
8
+ require 'redis-copy/ui'
9
+ require 'redis-copy/strategy'
10
+ require 'redis-copy/key-emitter'
11
+
12
+ module RedisCopy
13
+ class << self
14
+ # @param source [String]
15
+ # @param destination [String]
16
+ # @options options [Hash<Symbol,Object>]
17
+ def copy(source, destination, options = {})
18
+ puts options.inspect
19
+ ui = UI.load(options)
20
+
21
+ source = redis_from(source)
22
+ destination = redis_from(destination)
23
+
24
+ ui.abort('source cannot equal destination!') if same_redis?(source, destination)
25
+
26
+ key_emitter = KeyEmitter.load(source, ui, options)
27
+ strategem = Strategy.load(source, destination, ui, options)
28
+
29
+ dest_empty = !(destination.randomkey) # randomkey returns string unless db empty.
30
+
31
+ return false unless ui.confirm? <<-EODESC.strip_heredoc
32
+ Source: #{source.client.id}
33
+ Destination: #{destination.client.id} (#{dest_empty ? '' : 'NOT '}empty)
34
+ Key Emitter: #{key_emitter}
35
+ Strategy: #{strategem}
36
+ EODESC
37
+
38
+ ui.abort('Destination not empty!') unless dest_empty
39
+
40
+ key_emitter.keys.each_with_object(Hash.new {0}) do |key, stats|
41
+ success = strategem.copy(key)
42
+ stats[success ? :success : :failure] += 1
43
+ stats[:attempt] += 1
44
+
45
+ unless success
46
+ ui.notify("FAIL: #{key.dump}")
47
+ ui.abort if options[:fail_fast]
48
+ end
49
+ ui.notify(stats.inspect) if (stats[:attempt] % 1000).zero?
50
+ end.tap do |stats|
51
+ ui.notify("DONE: #{stats.inspect}")
52
+ end
53
+ end
54
+
55
+ private
56
+
57
+ def same_redis?(redis_a, redis_b)
58
+ # Redis::Client#id returns the connection uri
59
+ # e.g. 'redis://localhost:6379/0'
60
+ redis_a.client.id == redis_b.client.id
61
+ end
62
+
63
+ def redis_from(connection_string)
64
+ require 'uri'
65
+ connection_string = "redis://#{connection_string}" unless connection_string.start_with?("redis://")
66
+ uri = URI(connection_string)
67
+ ret = {uri: uri}
68
+
69
+ # Require the URL to have at least a host
70
+ raise ArgumentError, "invalid url: #{connection_string}" unless uri.host
71
+
72
+ host = uri.host
73
+ port = uri.port if uri.port
74
+ db = uri.path ? uri.path[1..-1].to_i : 0
75
+
76
+ Redis.new(host: host, port: port, db: db)
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,90 @@
1
+ # encoding: utf-8
2
+
3
+ require 'redis-copy'
4
+ require 'optparse'
5
+
6
+ module RedisCopy
7
+ class CLI
8
+ REDIS_URI = (/\A(?:redis:\/\/)?([a-z0-9\-.]+)(:[0-9]{1,5})?(\/(?:(?:1[0-5])|[0-9]))?\z/i).freeze
9
+ DEFAULTS = {
10
+ ui: :command_line,
11
+ key_emitter: :default,
12
+ strategy: :auto,
13
+ fail_fast: false,
14
+ yes: false,
15
+ }.freeze unless defined?(DEFAULTS)
16
+
17
+ def initialize(argv = ARGV)
18
+ argv = argv.dup
19
+ options = {}
20
+
21
+ OptionParser.new do |opts|
22
+ opts.banner = "Usage: #{opts.program_name} [options] <source> <destination>"
23
+ opts.version = RedisCopy::VERSION
24
+
25
+ indent_desc = proc do |desc|
26
+ desc.split("\n").join("\n#{opts.summary_indent}#{' '*opts.summary_width} ")
27
+ end
28
+
29
+ opts.separator " <source> and <destination> must be redis connection uris"
30
+ opts.separator " like [redis://]<hostname>[:<port>][/<db>]"
31
+ opts.separator ''
32
+ opts.separator "Specific options:"
33
+
34
+ opts.on('--strategy STRATEGY', [:auto, :new, :classic],
35
+ indent_desc.(
36
+ "Select strategy (auto, new, classic) (default #{DEFAULTS[:strategy]})\n" +
37
+ " auto: uses new if available, otherwise fallback\n" +
38
+ " new: use redis DUMP and RESTORE commands (faster)\n" +
39
+ " classic: migrates via multiple type-specific commands"
40
+ )
41
+ ) do |strategy|
42
+ options[:strategy] = strategy
43
+ end
44
+
45
+ opts.on('--[no-]dry-run', 'Output configuration and exit') do |d|
46
+ options[:dry_run] = true
47
+ end
48
+
49
+ opts.on('-d', '--[no-]debug', 'Write debug output') do |debug|
50
+ options[:debug] = debug
51
+ end
52
+
53
+ opts.on('-t', '--[no-]trace', 'Enable backtrace on failure') do |trace|
54
+ options[:trace] = trace
55
+ end
56
+
57
+ opts.on('-f', '--[no-]fail-fast', 'Abort on first failure') do |ff|
58
+ options[:fail_fast] = ff
59
+ end
60
+
61
+ opts.on('-y', '--yes', 'Automatically accept any prompts') do
62
+ options[:yes] = true
63
+ end
64
+
65
+ opts.parse!(argv)
66
+ unless argv.size == 2
67
+ opts.abort "Source and Destination must be specified\n\n" +
68
+ opts.help
69
+ end
70
+ @source = argv.shift
71
+ @destination = argv.shift
72
+
73
+ opts.abort "source is not valid URI" unless @source =~ REDIS_URI
74
+ opts.abort "destination is not valid URI" unless @destination =~ REDIS_URI
75
+ end
76
+
77
+ @config = DEFAULTS.merge(options)
78
+ end
79
+
80
+ def run!
81
+ (puts self.inspect; exit 1) if @config.delete(:dry_run)
82
+
83
+ RedisCopy::copy(@source, @destination, @config)
84
+ rescue => exception
85
+ $stderr.puts exception.message
86
+ $stderr.puts exception.backtrace if @config[:trace]
87
+ exit 1
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,3 @@
1
+ # encoding: utf-8
2
+
3
+ require_relative 'core_ext/string'
@@ -0,0 +1,45 @@
1
+ # encoding: utf-8
2
+
3
+ module RedisCopy
4
+ # A Key emitter emits keys.
5
+ # This is built to be an abstraction on top of
6
+ # redis.keys('*') (implemented by RedisCopy::KeyEmitter::Default),
7
+ # but should allow smarter implementations to be built that can handle
8
+ # billion-key dbs without blocking on IO.
9
+ module KeyEmitter
10
+ def self.load(redis, ui, options = {})
11
+ key_emitter = options.fetch(:key_emitter, :default)
12
+ const_name = key_emitter.to_s.camelize
13
+ require "redis-copy/key-emitter/#{key_emitter}" unless const_defined?(const_name)
14
+ const_get(const_name).new(redis, ui, options)
15
+ end
16
+
17
+ # @param redis [Redis]
18
+ # @param options [Hash<Symbol:String>]
19
+ def initialize(redis, ui, options = {})
20
+ @redis = redis
21
+ @ui = ui
22
+ @options = options
23
+ end
24
+
25
+ # @return [Enumerable<String>]
26
+ def keys
27
+ return super if defined?(super)
28
+ raise NotImplementedError
29
+ end
30
+
31
+ def to_s
32
+ self.class.name.demodulize.humanize
33
+ end
34
+
35
+ # The default strategy blindly uses `redis.keys('*')`
36
+ class Default
37
+ include KeyEmitter
38
+
39
+ def keys
40
+ @ui.debug "REDIS: #{@redis.client.id} KEYS *"
41
+ @redis.keys('*').to_enum
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,44 @@
1
+ # encoding: utf-8
2
+
3
+ require_relative 'strategy/new'
4
+ require_relative 'strategy/classic'
5
+
6
+ module RedisCopy
7
+ module Strategy
8
+ # @param source [Redis]
9
+ # @param destination [Redis]
10
+ def self.load(source, destination, ui, options = {})
11
+ strategy = options.fetch(:strategy, :auto).to_sym
12
+ new_compatible = [source, destination].all?(&New.method(:compatible?))
13
+ copierklass = case strategy
14
+ when :classic then Classic
15
+ when :new
16
+ raise ArgumentError unless new_compatible
17
+ New
18
+ when :auto
19
+ new_compatible ? New : Classic
20
+ end
21
+ copierklass.new(source, destination, ui, options)
22
+ end
23
+
24
+ # @param source [Redis]
25
+ # @param destination [Redis]
26
+ def initialize(source, destination, ui, options = {})
27
+ @src = source
28
+ @dst = destination
29
+ @ui = ui
30
+ @opt = options.dup
31
+ end
32
+
33
+ def to_s
34
+ self.class.name.demodulize.humanize
35
+ end
36
+
37
+ # @param key [String]
38
+ # @return [Boolean]
39
+ def copy(key)
40
+ return super if defined? super
41
+ raise NotImplementedError
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,55 @@
1
+ # encoding: utf-8
2
+
3
+ module RedisCopy
4
+ module Strategy
5
+ class Classic
6
+ include Strategy
7
+
8
+ def copy(key)
9
+ vtype = @src.type(key)
10
+ ttl = @src.ttl(key)
11
+
12
+ case vtype
13
+ when 'string'
14
+ string = @src.get(key)
15
+ @dst.set(key, string)
16
+ when "list"
17
+ list = @src.lrange(key, 0, -1)
18
+ if list.length == 0
19
+ # Empty list special case
20
+ @dst.lpush(key, '')
21
+ @dst.lpop(key)
22
+ else
23
+ list.each do |ele|
24
+ @dst.rpush(key, ele)
25
+ end
26
+ end
27
+ when "set"
28
+ set = @src.smembers(key)
29
+ if set.length == 0
30
+ # Empty set special case
31
+ @dst.sadd(key, '')
32
+ @dst.srem(key, '')
33
+ else
34
+ set.each do |ele|
35
+ @dst.sadd(key,ele)
36
+ end
37
+ end
38
+ when 'hash'
39
+ hash = @src.hgetall(key)
40
+ @dst.mapped_hmset(key, hash)
41
+ when 'zset'
42
+ vs_zset = @src.zrange(key, 0, -1, :with_scores => true)
43
+ sv_zset = vs_zset.map(&:reverse)
44
+ @dst.zadd(key, sv_zset)
45
+ else
46
+ return false
47
+ end
48
+
49
+ @dst.expire(key, ttl) unless ttl < 0 || vtype == 'none'
50
+
51
+ return true
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,32 @@
1
+ # encoding: utf-8
2
+
3
+ module RedisCopy
4
+ module Strategy
5
+ class New
6
+ include Strategy
7
+
8
+ def copy(key)
9
+ ttl = @src.ttl(key)
10
+ # TTL returns seconds, -1 means none set
11
+ # RESTORE ttl is in miliseconds, 0 means none set
12
+ translated_ttl = (ttl && ttl > 0) ? (ttl * 1000) : 0
13
+
14
+ dumped_value = @src.dump(key)
15
+ @dst.restore(key, translated_ttl, dumped_value)
16
+
17
+ return true
18
+ rescue Redis::CommandError => error
19
+ @ui.debug("ERROR: #{error}")
20
+ return false
21
+ end
22
+
23
+ def self.compatible?(redis)
24
+ maj, min, *_ = redis.info['redis_version'].split('.').map(&:to_i)
25
+ return false unless maj >= 2
26
+ return false unless min >= 6
27
+
28
+ return true
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,35 @@
1
+ # encoding: utf-8
2
+
3
+ module RedisCopy
4
+ module UI
5
+ def self.load(options = {})
6
+ ui = options.fetch(:ui, :auto_run)
7
+ const_name = ui.to_s.camelize
8
+ require "redis-copy/ui/#{ui}" unless const_defined?(const_name)
9
+ const_get(const_name).new(options)
10
+ end
11
+
12
+ def initialize(options)
13
+ @options = options
14
+ end
15
+
16
+ def confirm?(prompt)
17
+ return super if defined?(super)
18
+ raise NotImplementedError
19
+ end
20
+
21
+ def abort(message = nil)
22
+ return super if defined?(super)
23
+ raise NotImplementedError
24
+ end
25
+
26
+ def notify(message)
27
+ return super if defined?(super)
28
+ raise NotImplementedError
29
+ end
30
+
31
+ def debug(message)
32
+ notify(message) if @options[:debug]
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,20 @@
1
+ # encoding: utf-8
2
+
3
+ module RedisCopy
4
+ module UI
5
+ class AutoRun
6
+ include UI
7
+
8
+ def confirm?(prompt)
9
+ $stderr.puts(prompt)
10
+ true
11
+ end
12
+ def abort(message = nil)
13
+ raise RuntimeError, message
14
+ end
15
+ def notify(message)
16
+ $stderr.puts(message)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,26 @@
1
+ # encoding: utf-8
2
+
3
+ module RedisCopy
4
+ module UI
5
+ class CommandLine
6
+ include UI
7
+
8
+ def confirm?(prompt)
9
+ $stderr.puts(prompt)
10
+ return true if @options[:yes]
11
+ $stderr.puts("Continue? [yN]")
12
+ abort unless $stdin.gets.chomp =~ /y/i
13
+ true
14
+ end
15
+
16
+ def abort(message = nil)
17
+ notify(['ABORTED',message].compact.join(': '))
18
+ exit 1
19
+ end
20
+
21
+ def notify(message)
22
+ $stderr.puts(message)
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,5 @@
1
+ # encoding: utf-8
2
+
3
+ module RedisCopy
4
+ VERSION = '0.0.1'
5
+ end
@@ -0,0 +1,33 @@
1
+ # encoding: utf-8
2
+
3
+ lib = File.expand_path('../lib', __FILE__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require 'redis-copy/version'
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = 'redis-copy'
9
+ spec.version = RedisCopy::VERSION
10
+
11
+ authors_and_emails = [['Ryan Biesemeyer', 'ryan@yaauie.com']]
12
+ # authors_and_emails = (`sh git shortlog -sne`).lines.map do |l|
13
+ # (/(?<=\t)(.+) <(.+)>\z/).match(l.chomp).last(2)
14
+ # end.compact.map(&:to_a)
15
+
16
+ spec.authors = authors_and_emails.map(&:first)
17
+ spec.email = authors_and_emails.map(&:last)
18
+ spec.summary = 'Copy the contents of one redis db to another'
19
+ spec.homepage = 'https://github.com/yaauie/redis-copy'
20
+ spec.license = 'MIT'
21
+
22
+ spec.files = `git ls-files`.split($/)
23
+ spec.executables = spec.files.grep(/^bin\//) { |f| File.basename(f) }
24
+ spec.test_files = spec.files.grep(/^(test|spec|features)\//)
25
+ spec.require_paths = ['lib']
26
+
27
+ spec.add_development_dependency 'bundler', '~> 1.3'
28
+ spec.add_development_dependency 'rake'
29
+ spec.add_development_dependency 'rspec', '~> 2.14'
30
+
31
+ spec.add_runtime_dependency 'redis'
32
+ spec.add_runtime_dependency 'activesupport'
33
+ end
File without changes
@@ -0,0 +1,33 @@
1
+ # encoding: utf-8
2
+ require 'redis-copy'
3
+
4
+ describe RedisCopy::KeyEmitter::Default do
5
+ let(:redis) { double }
6
+ let(:ui) { double }
7
+ let(:instance) { RedisCopy::KeyEmitter::Default.new(redis, ui)}
8
+ let(:connection_uri) { 'redis://12.34.56.78:9000/15' }
9
+
10
+ before(:each) do
11
+ redis.stub_chain('client.id').and_return(connection_uri)
12
+ ui.stub(:debug).with(anything)
13
+ end
14
+
15
+ context '#keys' do
16
+ let(:mock_return) { ['foo:bar', 'asdf:qwer'] }
17
+ before(:each) do
18
+ redis.should_receive(:keys).with('*').exactly(:once).and_return(mock_return)
19
+ end
20
+ context 'the result' do
21
+ subject { instance.keys }
22
+ its(:to_a) { should eq mock_return }
23
+ end
24
+ context 'the supplied ui' do
25
+ it 'should get a debug message' do
26
+ ui.should_receive(:debug).
27
+ with(/#{Regexp.escape(connection_uri)} KEYS \*/).
28
+ exactly(:once)
29
+ instance.keys
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,343 @@
1
+ # encoding: utf-8
2
+ class RedisMultiplex < Struct.new(:source, :destination)
3
+ ResponseError = Class.new(RuntimeError)
4
+
5
+ def ensure_same!(&blk)
6
+ responses = {
7
+ source: capture_result(source, &blk),
8
+ destination: capture_result(destination, &blk)
9
+ }
10
+ unless responses[:source] == responses[:destination]
11
+ raise ResponseError.new(responses.to_s)
12
+ end
13
+ case responses[:destination].first
14
+ when :raised then raise responses[:destination].last
15
+ when :returned then return responses[:destination].last
16
+ end
17
+ end
18
+ alias_method :both!, :ensure_same!
19
+
20
+ def both(&blk)
21
+ both!(&blk)
22
+ true
23
+ rescue ResponseError
24
+ false
25
+ end
26
+
27
+ def capture_result(redis, &block)
28
+ return [:returned, block.call(redis)]
29
+ rescue Object => exception
30
+ return [:raised, exception]
31
+ end
32
+ end
33
+
34
+ shared_examples_for(RedisCopy::Strategy) do
35
+ let(:ui) { double.as_null_object }
36
+ let(:strategy) { strategy_class.new(source, destination, ui)}
37
+ let(:source) { Redis.new(db: 14) }
38
+ let(:destination) { Redis.new(db: 15) }
39
+ let(:multiplex) { RedisMultiplex.new(source, destination) }
40
+ let(:key) { rand(16**128).to_s(16) }
41
+ after(:each) { multiplex.both { |redis| redis.del(key) } }
42
+
43
+ context '#copy' do
44
+ context 'string' do
45
+ let(:source_string) { rand(16**256).to_s(16) }
46
+ before(:each) { source.set(key, source_string) }
47
+ [true,false].each do |with_expiry|
48
+ context "with_expiry(#{with_expiry})" do
49
+ before(:each) { source.expire(key, 100) } if with_expiry
50
+ context 'before' do
51
+ context 'source' do
52
+ subject { source.get(key) }
53
+ it { should_not be_nil }
54
+ it { should eq source_string }
55
+ context 'ttl' do
56
+ subject { source.ttl(key) }
57
+ it { should eq 100 } if with_expiry
58
+ it { should eq -1 } unless with_expiry
59
+ end
60
+ end
61
+ context 'destination' do
62
+ subject { destination.get(key) }
63
+ it { should be_nil }
64
+ context 'ttl' do
65
+ subject { destination.ttl(key) }
66
+ it { should eq -1 }
67
+ end
68
+ end
69
+ end
70
+
71
+ context 'after' do
72
+ before(:each) { strategy.copy(key) }
73
+ context 'source' do
74
+ subject { source.get(key) }
75
+ it { should_not be_nil }
76
+ it { should eq source_string }
77
+ context 'ttl' do
78
+ subject { source.ttl(key) }
79
+ it { should eq 100 } if with_expiry
80
+ it { should eq -1 } unless with_expiry
81
+ end
82
+ end
83
+ context 'destination' do
84
+ subject { destination.get(key) }
85
+ it { should_not be_nil }
86
+ it { should eq source_string }
87
+ context 'ttl' do
88
+ subject { destination.ttl(key) }
89
+ it { should eq 100 } if with_expiry
90
+ it { should eq -1 } unless with_expiry
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
97
+
98
+ context 'list' do
99
+ let(:source_list) do
100
+ %w(foo bar baz buz bingo jango)
101
+ end
102
+ before(:each) { source_list.each{|x| source.rpush(key, x)} }
103
+ [true,false].each do |with_expiry|
104
+ context "with_expiry(#{with_expiry})" do
105
+ before(:each) { source.expire(key, 100) } if with_expiry
106
+ context 'before' do
107
+ context 'source' do
108
+ subject { source.lrange(key, 0, -1) }
109
+ it { should_not be_empty }
110
+ it { should eq source_list }
111
+ context 'ttl' do
112
+ subject { source.ttl(key) }
113
+ it { should eq 100 } if with_expiry
114
+ it { should eq -1 } unless with_expiry
115
+ end
116
+ end
117
+ context 'destination' do
118
+ subject { destination.lrange(key, 0, -1) }
119
+ it { should be_empty }
120
+ context 'ttl' do
121
+ subject { destination.ttl(key) }
122
+ it { should eq -1 }
123
+ end
124
+ end
125
+ end
126
+
127
+ context 'after' do
128
+ before(:each) { strategy.copy(key) }
129
+ context 'source' do
130
+ subject { source.lrange(key, 0, -1) }
131
+ it { should_not be_empty }
132
+ it { should eq source_list }
133
+ context 'ttl' do
134
+ subject { source.ttl(key) }
135
+ it { should eq 100 } if with_expiry
136
+ it { should eq -1 } unless with_expiry
137
+ end
138
+ end
139
+ context 'destination' do
140
+ subject { destination.lrange(key, 0, -1) }
141
+ it { should_not be_empty }
142
+ it { should eq source_list }
143
+ context 'ttl' do
144
+ subject { destination.ttl(key) }
145
+ it { should eq 100 } if with_expiry
146
+ it { should eq -1 } unless with_expiry
147
+ end
148
+ end
149
+ end
150
+ end
151
+ end
152
+ end
153
+
154
+ context 'set' do
155
+ let(:source_list) do
156
+ %w(foo bar baz buz bingo jango)
157
+ end
158
+ before(:each) { source_list.each{|x| source.sadd(key, x)} }
159
+ [true,false].each do |with_expiry|
160
+ context "with_expiry(#{with_expiry})" do
161
+ before(:each) { source.expire(key, 100) } if with_expiry
162
+ context 'before' do
163
+ context 'source' do
164
+ subject { source.smembers(key) }
165
+ it { should_not be_empty }
166
+ it { should =~ source_list }
167
+ context 'ttl' do
168
+ subject { source.ttl(key) }
169
+ it { should eq 100 } if with_expiry
170
+ it { should eq -1 } unless with_expiry
171
+ end
172
+ end
173
+ context 'destination' do
174
+ subject { destination.smembers(key) }
175
+ it { should be_empty }
176
+ context 'ttl' do
177
+ subject { destination.ttl(key) }
178
+ it { should eq -1 }
179
+ end
180
+ end
181
+ end
182
+
183
+ context 'after' do
184
+ before(:each) { strategy.copy(key) }
185
+ context 'source' do
186
+ subject { source.smembers(key) }
187
+ it { should_not be_empty }
188
+ it { should =~ source_list }
189
+ context 'ttl' do
190
+ subject { source.ttl(key) }
191
+ it { should eq 100 } if with_expiry
192
+ it { should eq -1 } unless with_expiry
193
+ end
194
+ end
195
+ context 'destination' do
196
+ subject { destination.smembers(key) }
197
+ it { should_not be_empty }
198
+ it { should =~ source_list }
199
+ context 'ttl' do
200
+ subject { destination.ttl(key) }
201
+ it { should eq 100 } if with_expiry
202
+ it { should eq -1 } unless with_expiry
203
+ end
204
+ end
205
+ end
206
+ end
207
+ end
208
+ end
209
+
210
+ context 'hash' do
211
+ let(:source_hash) do
212
+ {
213
+ 'foo' => 'bar',
214
+ 'baz' => 'buz'
215
+ }
216
+ end
217
+ before(:each) { source.mapped_hmset(key, source_hash) }
218
+ [true,false].each do |with_expiry|
219
+ context "with_expiry(#{with_expiry})" do
220
+ before(:each) { source.expire(key, 100) } if with_expiry
221
+ context 'before' do
222
+ context 'source' do
223
+ subject { source.hgetall(key) }
224
+ it { should_not be_empty }
225
+ it { should eq source_hash }
226
+ context 'ttl' do
227
+ subject { source.ttl(key) }
228
+ it { should eq 100 } if with_expiry
229
+ it { should eq -1 } unless with_expiry
230
+ end
231
+ end
232
+ context 'destination' do
233
+ subject { destination.hgetall(key) }
234
+ it { should be_empty }
235
+ context 'ttl' do
236
+ subject { destination.ttl(key) }
237
+ it { should eq -1 }
238
+ end
239
+ end
240
+ end
241
+
242
+ context 'after' do
243
+ before(:each) { strategy.copy(key) }
244
+ context 'source' do
245
+ subject { source.hgetall(key) }
246
+ it { should_not be_empty }
247
+ it { should eq source_hash }
248
+ context 'ttl' do
249
+ subject { source.ttl(key) }
250
+ it { should eq 100 } if with_expiry
251
+ it { should eq -1 } unless with_expiry
252
+ end
253
+ end
254
+ context 'destination' do
255
+ subject { destination.hgetall(key) }
256
+ it { should_not be_empty }
257
+ it { should eq source_hash }
258
+ context 'ttl' do
259
+ subject { destination.ttl(key) }
260
+ it { should eq 100 } if with_expiry
261
+ it { should eq -1 } unless with_expiry
262
+ end
263
+ end
264
+ end
265
+ end
266
+ end
267
+ end
268
+
269
+ context 'zset' do
270
+ let(:source_zset) do
271
+ {
272
+ 'foo' => 1.0,
273
+ 'baz' => 2.5,
274
+ 'bar' => 1.1,
275
+ 'buz' => 2.7
276
+ }
277
+ end
278
+ let(:vs_source_zset) { source_zset.to_a }
279
+ let(:sv_source_zset) { vs_source_zset.map(&:reverse) }
280
+ before(:each) { source.zadd(key, sv_source_zset) }
281
+ [true,false].each do |with_expiry|
282
+ context "with_expiry(#{with_expiry})" do
283
+ before(:each) { source.expire(key, 100) } if with_expiry
284
+ context 'before' do
285
+ context 'source' do
286
+ subject { source.zrange(key, 0, -1, :with_scores => true) }
287
+ it { should_not be_empty }
288
+ it { should =~ vs_source_zset }
289
+ context 'ttl' do
290
+ subject { source.ttl(key) }
291
+ it { should eq 100 } if with_expiry
292
+ it { should eq -1 } unless with_expiry
293
+ end
294
+ end
295
+ context 'destination' do
296
+ subject { destination.zrange(key, 0, -1, :with_scores => true) }
297
+ it { should be_empty }
298
+ context 'ttl' do
299
+ subject { destination.ttl(key) }
300
+ it { should eq -1 }
301
+ end
302
+ end
303
+ end
304
+
305
+ context 'after' do
306
+ before(:each) { strategy.copy(key) }
307
+ context 'source' do
308
+ subject { source.zrange(key, 0, -1, :with_scores => true) }
309
+ it { should_not be_empty }
310
+ it { should =~ vs_source_zset }
311
+ context 'ttl' do
312
+ subject { source.ttl(key) }
313
+ it { should eq 100 } if with_expiry
314
+ it { should eq -1 } unless with_expiry
315
+ end
316
+ end
317
+ context 'destination' do
318
+ subject { destination.zrange(key, 0, -1, :with_scores => true) }
319
+ it { should_not be_empty }
320
+ it { should =~ vs_source_zset }
321
+ context 'ttl' do
322
+ subject { destination.ttl(key) }
323
+ it { should eq 100 } if with_expiry
324
+ it { should eq -1 } unless with_expiry
325
+ end
326
+ end
327
+ end
328
+ end
329
+ end
330
+ end
331
+ end
332
+ end
333
+
334
+ describe RedisCopy::Strategy do
335
+ describe :New do
336
+ let(:strategy_class) { RedisCopy::Strategy::New }
337
+ it_should_behave_like RedisCopy::Strategy
338
+ end
339
+ describe :Classic do
340
+ let(:strategy_class) { RedisCopy::Strategy::Classic }
341
+ it_should_behave_like RedisCopy::Strategy
342
+ end
343
+ end
metadata ADDED
@@ -0,0 +1,151 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: redis-copy
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Ryan Biesemeyer
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-10-11 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: bundler
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: '1.3'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: '1.3'
30
+ - !ruby/object:Gem::Dependency
31
+ name: rake
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :development
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: rspec
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ~>
52
+ - !ruby/object:Gem::Version
53
+ version: '2.14'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: '2.14'
62
+ - !ruby/object:Gem::Dependency
63
+ name: redis
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ type: :runtime
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ - !ruby/object:Gem::Dependency
79
+ name: activesupport
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ! '>='
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ type: :runtime
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ! '>='
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ description:
95
+ email:
96
+ - ryan@yaauie.com
97
+ executables:
98
+ - redis-copy
99
+ extensions: []
100
+ extra_rdoc_files: []
101
+ files:
102
+ - .gitignore
103
+ - Gemfile
104
+ - LICENSE.txt
105
+ - README.md
106
+ - Rakefile
107
+ - bin/redis-copy
108
+ - lib/redis-copy.rb
109
+ - lib/redis-copy/cli.rb
110
+ - lib/redis-copy/core_ext.rb
111
+ - lib/redis-copy/key-emitter.rb
112
+ - lib/redis-copy/strategy.rb
113
+ - lib/redis-copy/strategy/classic.rb
114
+ - lib/redis-copy/strategy/new.rb
115
+ - lib/redis-copy/ui.rb
116
+ - lib/redis-copy/ui/auto_run.rb
117
+ - lib/redis-copy/ui/command_line.rb
118
+ - lib/redis-copy/version.rb
119
+ - redis-copy.gemspec
120
+ - redis-copy_spec.rb
121
+ - spec/redis-copy/key-emitter_spec.rb
122
+ - spec/redis-copy/strategy_spec.rb
123
+ homepage: https://github.com/yaauie/redis-copy
124
+ licenses:
125
+ - MIT
126
+ post_install_message:
127
+ rdoc_options: []
128
+ require_paths:
129
+ - lib
130
+ required_ruby_version: !ruby/object:Gem::Requirement
131
+ none: false
132
+ requirements:
133
+ - - ! '>='
134
+ - !ruby/object:Gem::Version
135
+ version: '0'
136
+ required_rubygems_version: !ruby/object:Gem::Requirement
137
+ none: false
138
+ requirements:
139
+ - - ! '>='
140
+ - !ruby/object:Gem::Version
141
+ version: '0'
142
+ requirements: []
143
+ rubyforge_project:
144
+ rubygems_version: 1.8.24
145
+ signing_key:
146
+ specification_version: 3
147
+ summary: Copy the contents of one redis db to another
148
+ test_files:
149
+ - spec/redis-copy/key-emitter_spec.rb
150
+ - spec/redis-copy/strategy_spec.rb
151
+ has_rdoc: