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