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 +1 -0
- data/README.md +28 -12
- data/lib/redis-copy.rb +8 -3
- data/lib/redis-copy/cli.rb +34 -31
- data/lib/redis-copy/key-emitter.rb +11 -66
- data/lib/redis-copy/key-emitter/interface.spec.rb +79 -0
- data/lib/redis-copy/key-emitter/keys.rb +39 -0
- data/lib/redis-copy/key-emitter/scan.rb +20 -0
- data/lib/redis-copy/strategy.rb +5 -23
- data/lib/redis-copy/strategy/classic.rb +1 -5
- data/lib/redis-copy/strategy/{new.rb → dump-restore.rb} +11 -10
- data/lib/redis-copy/strategy/interface.spec.rb +299 -0
- data/lib/redis-copy/ui.rb +5 -9
- data/lib/redis-copy/ui/auto_run.rb +1 -1
- data/lib/redis-copy/ui/command_line.rb +3 -1
- data/lib/redis-copy/version.rb +1 -1
- data/redis-copy.gemspec +3 -0
- data/spec/redis-copy/{key-emitter_spec.rb → key-emitter/keys_spec.rb} +3 -34
- data/spec/redis-copy/key-emitter/scan_spec.rb +9 -0
- data/spec/redis-copy/strategy/classic_spec.rb +27 -0
- data/spec/redis-copy/strategy/dump-restore_spec.rb +9 -0
- data/spec/spec_helper.rb +2 -0
- metadata +36 -19
- data/.travis/Gemfile.redis-gem-3.0.lock +0 -44
- data/.travis/Gemfile.redis-gem-master.lock +0 -49
- data/spec/redis-copy/strategy_spec.rb +0 -314
@@ -0,0 +1,20 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module RedisCopy
|
4
|
+
# Scan uses the SCAN family of commands, which were introduced in
|
5
|
+
# the 2.8 branch of Redis, and after 3.0.5 of the redis-rb gem.
|
6
|
+
class KeyEmitter::Scan
|
7
|
+
implements KeyEmitter do |redis, *_|
|
8
|
+
bin_version = Gem::Version.new(redis.info['redis_version'])
|
9
|
+
bin_requirement = Gem::Requirement.new('>= 2.7.105')
|
10
|
+
|
11
|
+
next false unless bin_requirement.satisfied_by?(bin_version)
|
12
|
+
|
13
|
+
redis.respond_to?(:scan_each)
|
14
|
+
end
|
15
|
+
|
16
|
+
def keys
|
17
|
+
@redis.scan_each(count: 1000, match: pattern)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
data/lib/redis-copy/strategy.rb
CHANGED
@@ -1,29 +1,8 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
|
3
|
-
require_relative 'strategy/new'
|
4
|
-
require_relative 'strategy/classic'
|
5
|
-
|
6
3
|
module RedisCopy
|
7
4
|
module Strategy
|
8
|
-
|
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
|
-
def self.included(base)
|
25
|
-
base.send(:include, Verifier)
|
26
|
-
end
|
5
|
+
extend Implements::Interface
|
27
6
|
|
28
7
|
# @param source [Redis]
|
29
8
|
# @param destination [Redis]
|
@@ -35,7 +14,7 @@ module RedisCopy
|
|
35
14
|
end
|
36
15
|
|
37
16
|
def to_s
|
38
|
-
self.class.name.demodulize
|
17
|
+
self.class.name.demodulize
|
39
18
|
end
|
40
19
|
|
41
20
|
# @param key [String]
|
@@ -87,3 +66,6 @@ module RedisCopy
|
|
87
66
|
end
|
88
67
|
end
|
89
68
|
end
|
69
|
+
|
70
|
+
require_relative 'strategy/classic'
|
71
|
+
require_relative 'strategy/dump-restore'
|
@@ -34,7 +34,7 @@
|
|
34
34
|
module RedisCopy
|
35
35
|
module Strategy
|
36
36
|
class Classic
|
37
|
-
|
37
|
+
implements Strategy
|
38
38
|
|
39
39
|
def copy(key)
|
40
40
|
@ui.debug("COPY: #{key.dump}")
|
@@ -106,10 +106,6 @@ module RedisCopy
|
|
106
106
|
def pipeline_enabled?
|
107
107
|
@pipeline_enabled ||= (false | @opt[:pipeline])
|
108
108
|
end
|
109
|
-
|
110
|
-
def self.compatible?(redis)
|
111
|
-
true
|
112
|
-
end
|
113
109
|
end
|
114
110
|
end
|
115
111
|
end
|
@@ -2,8 +2,17 @@
|
|
2
2
|
|
3
3
|
module RedisCopy
|
4
4
|
module Strategy
|
5
|
-
class
|
6
|
-
|
5
|
+
class DumpRestore
|
6
|
+
implements Strategy do |source, destination, *_|
|
7
|
+
[source, destination].all? do |redis|
|
8
|
+
bin_version = Gem::Version.new(redis.info['redis_version'])
|
9
|
+
bin_requirement = Gem::Requirement.new('>= 2.6.0')
|
10
|
+
|
11
|
+
next false unless bin_requirement.satisfied_by?(bin_version)
|
12
|
+
|
13
|
+
true
|
14
|
+
end
|
15
|
+
end
|
7
16
|
|
8
17
|
def copy(key)
|
9
18
|
@ui.debug("COPY: #{key.dump}")
|
@@ -21,14 +30,6 @@ module RedisCopy
|
|
21
30
|
@ui.debug("ERROR: #{error}")
|
22
31
|
return false
|
23
32
|
end
|
24
|
-
|
25
|
-
def self.compatible?(redis)
|
26
|
-
maj, min, *_ = redis.info['redis_version'].split('.').map(&:to_i)
|
27
|
-
return false unless maj >= 2
|
28
|
-
return false unless min >= 6
|
29
|
-
|
30
|
-
return true
|
31
|
-
end
|
32
33
|
end
|
33
34
|
end
|
34
35
|
end
|
@@ -0,0 +1,299 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
# The shared examples for RedisCopy::Strategy are available to require
|
4
|
+
# into consuming libraries so they can verify their implementation of the
|
5
|
+
# RedisCopy::Strategy interface. See the bundled specs for the bundled
|
6
|
+
# key-emitters for example usage.
|
7
|
+
if defined?(::RSpec)
|
8
|
+
shared_examples_for(RedisCopy::Strategy) do
|
9
|
+
let(:strategy_class) { described_class }
|
10
|
+
let(:options) { Hash.new } # append using before(:each) { options.update(foo: true) }
|
11
|
+
let(:ui) { RedisCopy::UI::CommandLine.new(options) }
|
12
|
+
let(:selector) { strategy_class.name.underscore.dasherize } # see implements gem
|
13
|
+
let(:strategy_class_finder) { RedisCopy::Strategy.implementation(selector) }
|
14
|
+
let(:strategy) { strategy_class_finder.new(source, destination, ui, options) }
|
15
|
+
let(:multiplex) { RedisMultiplex.new(source, destination) }
|
16
|
+
let(:source) { Redis.new(REDIS_OPTIONS.merge(db: 14)) }
|
17
|
+
let(:destination) { Redis.new(REDIS_OPTIONS.merge(db: 15)) }
|
18
|
+
|
19
|
+
let(:key) { rand(16**128).to_s(16) }
|
20
|
+
after(:each) { multiplex.both { |redis| redis.del(key) } }
|
21
|
+
let(:ttl) { 100 }
|
22
|
+
|
23
|
+
before(:each) do
|
24
|
+
begin
|
25
|
+
strategy.class.should eq strategy_class
|
26
|
+
rescue Implements::Implementation::NotFound
|
27
|
+
pending "#{strategy_class} not supported in your environment"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
context '#copy' do
|
32
|
+
before(:each) { populate.call }
|
33
|
+
context 'string' do
|
34
|
+
let(:source_string) { rand(16**256).to_s(16) }
|
35
|
+
let(:populate) { proc {source.set(key, source_string)} }
|
36
|
+
[true,false].each do |with_expiry|
|
37
|
+
context "with_expiry(#{with_expiry})" do
|
38
|
+
before(:each) { source.expire(key, ttl) } if with_expiry
|
39
|
+
context 'before' do
|
40
|
+
context 'source' do
|
41
|
+
let(:redis) { source }
|
42
|
+
subject { source.get(key) }
|
43
|
+
it { should_not be_nil }
|
44
|
+
it { should eq source_string }
|
45
|
+
it_should_behave_like (with_expiry ? :ttl_set : :no_ttl)
|
46
|
+
end
|
47
|
+
context 'destination' do
|
48
|
+
let(:redis) { destination }
|
49
|
+
subject { destination.get(key) }
|
50
|
+
it { should be_nil }
|
51
|
+
it_should_behave_like :no_ttl
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
context 'after' do
|
56
|
+
before(:each) { strategy.copy(key) }
|
57
|
+
context 'source' do
|
58
|
+
let(:redis) { source }
|
59
|
+
subject { source.get(key) }
|
60
|
+
it { should_not be_nil }
|
61
|
+
it { should eq source_string }
|
62
|
+
it_should_behave_like (with_expiry ? :ttl_set : :no_ttl)
|
63
|
+
end
|
64
|
+
context 'destination' do
|
65
|
+
let(:redis) { destination }
|
66
|
+
subject { destination.get(key) }
|
67
|
+
it { should_not be_nil }
|
68
|
+
it { should eq source_string }
|
69
|
+
it_should_behave_like (with_expiry ? :ttl_set : :no_ttl)
|
70
|
+
end
|
71
|
+
it_should_behave_like '#verify?'
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
context 'list' do
|
78
|
+
let(:source_list) do
|
79
|
+
%w(foo bar baz buz bingo jango)
|
80
|
+
end
|
81
|
+
let(:populate) { proc { source_list.each{|x| source.rpush(key, x)} } }
|
82
|
+
[true,false].each do |with_expiry|
|
83
|
+
context "with_expiry(#{with_expiry})" do
|
84
|
+
before(:each) { source.expire(key, 100) } if with_expiry
|
85
|
+
context 'before' do
|
86
|
+
context 'source' do
|
87
|
+
let(:redis) { source }
|
88
|
+
subject { source.lrange(key, 0, -1) }
|
89
|
+
it { should_not be_empty }
|
90
|
+
it { should eq source_list }
|
91
|
+
it_should_behave_like (with_expiry ? :ttl_set : :no_ttl)
|
92
|
+
end
|
93
|
+
context 'destination' do
|
94
|
+
let(:redis) { destination }
|
95
|
+
subject { destination.lrange(key, 0, -1) }
|
96
|
+
it { should be_empty }
|
97
|
+
it_should_behave_like :no_ttl
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
context 'after' do
|
102
|
+
before(:each) { strategy.copy(key) }
|
103
|
+
context 'source' do
|
104
|
+
let(:redis) { source }
|
105
|
+
subject { source.lrange(key, 0, -1) }
|
106
|
+
it { should_not be_empty }
|
107
|
+
it { should eq source_list }
|
108
|
+
it_should_behave_like (with_expiry ? :ttl_set : :no_ttl)
|
109
|
+
end
|
110
|
+
context 'destination' do
|
111
|
+
let(:redis) { destination }
|
112
|
+
subject { destination.lrange(key, 0, -1) }
|
113
|
+
it { should_not be_empty }
|
114
|
+
it { should eq source_list }
|
115
|
+
it_should_behave_like (with_expiry ? :ttl_set : :no_ttl)
|
116
|
+
end
|
117
|
+
it_should_behave_like '#verify?'
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
context 'set' do
|
124
|
+
let(:source_list) do
|
125
|
+
%w(foo bar baz buz bingo jango)
|
126
|
+
end
|
127
|
+
let(:populate) { proc { source_list.each{|x| source.sadd(key, x)} } }
|
128
|
+
[true,false].each do |with_expiry|
|
129
|
+
context "with_expiry(#{with_expiry})" do
|
130
|
+
before(:each) { source.expire(key, 100) } if with_expiry
|
131
|
+
context 'before' do
|
132
|
+
context 'source' do
|
133
|
+
let(:redis) { source }
|
134
|
+
subject { source.smembers(key) }
|
135
|
+
it { should_not be_empty }
|
136
|
+
it { should =~ source_list }
|
137
|
+
it_should_behave_like (with_expiry ? :ttl_set : :no_ttl)
|
138
|
+
end
|
139
|
+
context 'destination' do
|
140
|
+
let(:redis) { destination }
|
141
|
+
subject { destination.smembers(key) }
|
142
|
+
it { should be_empty }
|
143
|
+
it_should_behave_like :no_ttl
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
context 'after' do
|
148
|
+
before(:each) { strategy.copy(key) }
|
149
|
+
context 'source' do
|
150
|
+
let(:redis) { source }
|
151
|
+
subject { source.smembers(key) }
|
152
|
+
it { should_not be_empty }
|
153
|
+
it { should =~ source_list }
|
154
|
+
it_should_behave_like (with_expiry ? :ttl_set : :no_ttl)
|
155
|
+
end
|
156
|
+
context 'destination' do
|
157
|
+
let(:redis) { destination }
|
158
|
+
subject { destination.smembers(key) }
|
159
|
+
it { should_not be_empty }
|
160
|
+
it { should =~ source_list }
|
161
|
+
it_should_behave_like (with_expiry ? :ttl_set : :no_ttl)
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
context 'hash' do
|
169
|
+
let(:source_hash) do
|
170
|
+
{
|
171
|
+
'foo' => 'bar',
|
172
|
+
'baz' => 'buz'
|
173
|
+
}
|
174
|
+
end
|
175
|
+
let(:populate) { proc { source.mapped_hmset(key, source_hash) } }
|
176
|
+
[true,false].each do |with_expiry|
|
177
|
+
context "with_expiry(#{with_expiry})" do
|
178
|
+
before(:each) { source.expire(key, 100) } if with_expiry
|
179
|
+
context 'before' do
|
180
|
+
context 'source' do
|
181
|
+
let(:redis) { source }
|
182
|
+
subject { source.hgetall(key) }
|
183
|
+
it { should_not be_empty }
|
184
|
+
it { should eq source_hash }
|
185
|
+
it_should_behave_like (with_expiry ? :ttl_set : :no_ttl)
|
186
|
+
end
|
187
|
+
context 'destination' do
|
188
|
+
let(:redis) { destination }
|
189
|
+
subject { destination.hgetall(key) }
|
190
|
+
it { should be_empty }
|
191
|
+
it_should_behave_like :no_ttl
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
context 'after' do
|
196
|
+
before(:each) { strategy.copy(key) }
|
197
|
+
context 'source' do
|
198
|
+
let(:redis) { source }
|
199
|
+
subject { source.hgetall(key) }
|
200
|
+
it { should_not be_empty }
|
201
|
+
it { should eq source_hash }
|
202
|
+
it_should_behave_like (with_expiry ? :ttl_set : :no_ttl)
|
203
|
+
end
|
204
|
+
context 'destination' do
|
205
|
+
let(:redis) { destination }
|
206
|
+
subject { destination.hgetall(key) }
|
207
|
+
it { should_not be_empty }
|
208
|
+
it { should eq source_hash }
|
209
|
+
it_should_behave_like (with_expiry ? :ttl_set : :no_ttl)
|
210
|
+
end
|
211
|
+
it_should_behave_like '#verify?'
|
212
|
+
end
|
213
|
+
end
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
context 'zset' do
|
218
|
+
let(:source_zset) do
|
219
|
+
{
|
220
|
+
'foo' => 1.0,
|
221
|
+
'baz' => 2.5,
|
222
|
+
'bar' => 1.1,
|
223
|
+
'buz' => 2.7
|
224
|
+
}
|
225
|
+
end
|
226
|
+
let(:vs_source_zset) { source_zset.to_a }
|
227
|
+
let(:sv_source_zset) { vs_source_zset.map(&:reverse) }
|
228
|
+
let(:populate) { proc { source.zadd(key, sv_source_zset) } }
|
229
|
+
[true,false].each do |with_expiry|
|
230
|
+
context "with_expiry(#{with_expiry})" do
|
231
|
+
before(:each) { source.expire(key, 100) } if with_expiry
|
232
|
+
context 'before' do
|
233
|
+
context 'source' do
|
234
|
+
let(:redis) { source }
|
235
|
+
subject { source.zrange(key, 0, -1, :with_scores => true) }
|
236
|
+
it { should_not be_empty }
|
237
|
+
it { should =~ vs_source_zset }
|
238
|
+
it_should_behave_like (with_expiry ? :ttl_set : :no_ttl)
|
239
|
+
end
|
240
|
+
context 'destination' do
|
241
|
+
let(:redis) { destination }
|
242
|
+
subject { destination.zrange(key, 0, -1, :with_scores => true) }
|
243
|
+
it { should be_empty }
|
244
|
+
it_should_behave_like :no_ttl
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
context 'after' do
|
249
|
+
before(:each) { strategy.copy(key) }
|
250
|
+
context 'source' do
|
251
|
+
let(:redis) { source }
|
252
|
+
subject { source.zrange(key, 0, -1, :with_scores => true) }
|
253
|
+
it { should_not be_empty }
|
254
|
+
it { should =~ vs_source_zset }
|
255
|
+
it_should_behave_like (with_expiry ? :ttl_set : :no_ttl)
|
256
|
+
end
|
257
|
+
context 'destination' do
|
258
|
+
let(:redis) { destination }
|
259
|
+
subject { destination.zrange(key, 0, -1, :with_scores => true) }
|
260
|
+
it { should_not be_empty }
|
261
|
+
it { should =~ vs_source_zset }
|
262
|
+
it_should_behave_like (with_expiry ? :ttl_set : :no_ttl)
|
263
|
+
end
|
264
|
+
it_should_behave_like '#verify?'
|
265
|
+
end
|
266
|
+
end
|
267
|
+
end
|
268
|
+
end
|
269
|
+
end
|
270
|
+
end
|
271
|
+
|
272
|
+
shared_examples_for(:no_ttl) do
|
273
|
+
# key, redis,
|
274
|
+
subject { redis.ttl(key) }
|
275
|
+
it { should be < 0 }
|
276
|
+
end
|
277
|
+
|
278
|
+
shared_examples_for(:ttl_set) do
|
279
|
+
# key, redis, ttl
|
280
|
+
subject { redis.ttl(key) }
|
281
|
+
it { should be_within(1).of(ttl) }
|
282
|
+
end
|
283
|
+
|
284
|
+
shared_examples_for '#verify?' do
|
285
|
+
before(:each) do
|
286
|
+
ui.stub(:debug).and_call_original
|
287
|
+
ui.stub(:notify) do |message|
|
288
|
+
puts message
|
289
|
+
end
|
290
|
+
end
|
291
|
+
it 'should verify successfully' do
|
292
|
+
strategy.verify?(key).should be_true
|
293
|
+
end
|
294
|
+
end
|
295
|
+
else
|
296
|
+
fail(LoadError,
|
297
|
+
"#{__FILE__} contains shared examples for RedisCopy::Strategy. " +
|
298
|
+
"Require it in your specs, not your code.")
|
299
|
+
end
|
data/lib/redis-copy/ui.rb
CHANGED
@@ -1,16 +1,8 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
|
3
|
-
require_relative 'ui/auto_run'
|
4
|
-
require_relative 'ui/command_line'
|
5
|
-
|
6
3
|
module RedisCopy
|
7
4
|
module UI
|
8
|
-
|
9
|
-
ui = options.fetch(:ui, :auto_run)
|
10
|
-
const_name = ui.to_s.camelize
|
11
|
-
require "redis-copy/ui/#{ui}" unless const_defined?(const_name)
|
12
|
-
const_get(const_name).new(options)
|
13
|
-
end
|
5
|
+
extend Implements::Interface
|
14
6
|
|
15
7
|
def initialize(options)
|
16
8
|
@options = options
|
@@ -36,3 +28,7 @@ module RedisCopy
|
|
36
28
|
end
|
37
29
|
end
|
38
30
|
end
|
31
|
+
|
32
|
+
# load the bundled uis:
|
33
|
+
require_relative 'ui/auto_run'
|
34
|
+
require_relative 'ui/command_line'
|