redis-copy 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: