redis_migrator 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,110 @@
1
+ require 'forwardable'
2
+ require 'set'
3
+
4
+ class MockRedis
5
+ class Zset
6
+ include Enumerable
7
+ extend Forwardable
8
+
9
+ attr_reader :members, :scores
10
+
11
+ def_delegators :members, :empty?, :include?, :size
12
+
13
+ def initialize
14
+ @members = Set.new
15
+ @scores = Hash.new
16
+ end
17
+
18
+ def initialize_copy(source)
19
+ super
20
+ @members = @members.clone
21
+ @scores = @scores.clone
22
+ end
23
+
24
+ def add(score, member)
25
+ members.add(member)
26
+ if score.to_f.to_i == score.to_f
27
+ scores[member] = score.to_f.to_i
28
+ else
29
+ scores[member] = score.to_f
30
+ end
31
+ self
32
+ end
33
+
34
+ def delete?(member)
35
+ scores.delete(member)
36
+ members.delete?(member) and self
37
+ end
38
+
39
+ def each
40
+ members.each {|m| yield score(m), m}
41
+ end
42
+
43
+ def in_range(min, max)
44
+ in_from_the_left = case min
45
+ when "-inf"
46
+ lambda {|_| true }
47
+ when "+inf"
48
+ lambda {|_| false }
49
+ when /\((.*)$/
50
+ val = $1.to_f
51
+ lambda {|x| x.to_f > val }
52
+ else
53
+ lambda {|x| x.to_f >= min.to_f }
54
+ end
55
+
56
+ in_from_the_right = case max
57
+ when "-inf"
58
+ lambda {|_| false }
59
+ when "+inf"
60
+ lambda {|_| true }
61
+ when /\((.*)$/
62
+ val = $1.to_f
63
+ lambda {|x| x.to_f < val }
64
+ else
65
+ lambda {|x| x.to_f <= max.to_f }
66
+ end
67
+
68
+ sorted.find_all do |(score, member)|
69
+ in_from_the_left[score] && in_from_the_right[score]
70
+ end
71
+ end
72
+
73
+ def intersection(other)
74
+ if !block_given?
75
+ intersection(other, &:+)
76
+ else
77
+ self.members.intersection(other.members).reduce(self.class.new) do |acc, m|
78
+ new_score = yield(self.score(m), other.score(m))
79
+ acc.add(new_score, m)
80
+ end
81
+ end
82
+ end
83
+
84
+ def score(member)
85
+ scores[member]
86
+ end
87
+
88
+ def sorted
89
+ members.map do |m|
90
+ [score(m), m]
91
+ end.sort_by(&:first)
92
+ end
93
+
94
+ def sorted_members
95
+ sorted.map(&:last)
96
+ end
97
+
98
+ def union(other)
99
+ if !block_given?
100
+ union(other, &:+)
101
+ else
102
+ self.members.union(other.members).reduce(self.class.new) do |acc, m|
103
+ new_score = yield(self.score(m), other.score(m))
104
+ acc.add(new_score, m)
105
+ end
106
+ end
107
+ end
108
+
109
+ end
110
+ end
@@ -0,0 +1,210 @@
1
+ require 'mock_redis/assertions'
2
+ require 'mock_redis/utility_methods'
3
+ require 'mock_redis/zset'
4
+
5
+ class MockRedis
6
+ module ZsetMethods
7
+ include Assertions
8
+ include UtilityMethods
9
+
10
+ def zadd(key, score, member)
11
+ assert_scorey(score)
12
+
13
+ retval = !zscore(key, member)
14
+ with_zset_at(key) {|z| z.add(score, member.to_s)}
15
+ retval
16
+ end
17
+
18
+ def zcard(key)
19
+ with_zset_at(key, &:size)
20
+ end
21
+
22
+ def zcount(key, min, max)
23
+ assert_scorey(min, 'min or max')
24
+ assert_scorey(max, 'min or max')
25
+
26
+ with_zset_at(key) do |z|
27
+ z.count do |score, _|
28
+ score >= min && score <= max
29
+ end
30
+ end
31
+ end
32
+
33
+ def zincrby(key, increment, member)
34
+ assert_scorey(increment)
35
+ member = member.to_s
36
+ with_zset_at(key) do |z|
37
+ old_score = z.include?(member) ? z.score(member) : 0
38
+ new_score = old_score + increment
39
+ z.add(new_score, member)
40
+ new_score.to_s
41
+ end
42
+ end
43
+
44
+ def zinterstore(destination, keys, options={})
45
+ assert_has_args(keys, 'zinterstore')
46
+
47
+ data[destination] = combine_weighted_zsets(keys, options, :intersection)
48
+ zcard(destination)
49
+ end
50
+
51
+ def zrange(key, start, stop, options={})
52
+ with_zset_at(key) do |z|
53
+ to_response(z.sorted[start..stop] || [], options)
54
+ end
55
+ end
56
+
57
+ def zrangebyscore(key, min, max, options={})
58
+ with_zset_at(key) do |zset|
59
+ all_results = zset.in_range(min, max)
60
+ to_response(apply_limit(all_results, options[:limit]), options)
61
+ end
62
+ end
63
+
64
+ def zrank(key, member)
65
+ with_zset_at(key) {|z| z.sorted_members.index(member.to_s) }
66
+ end
67
+
68
+ def zrem(key, member)
69
+ with_zset_at(key) {|z| !!z.delete?(member.to_s)}
70
+ end
71
+
72
+ def zrevrange(key, start, stop, options={})
73
+ with_zset_at(key) do |z|
74
+ to_response(z.sorted.reverse[start..stop], options)
75
+ end
76
+ end
77
+
78
+ def zremrangebyrank(key, start, stop)
79
+ zrange(key, start, stop).
80
+ each {|member| zrem(key, member)}.
81
+ size
82
+ end
83
+
84
+ def zremrangebyscore(key, min, max)
85
+ zrangebyscore(key, min, max).
86
+ each {|member| zrem(key, member)}.
87
+ size
88
+ end
89
+
90
+ def zrevrangebyscore(key, max, min, options={})
91
+ with_zset_at(key) do |zset|
92
+ to_response(
93
+ apply_limit(
94
+ zset.in_range(min, max).reverse,
95
+ options[:limit]),
96
+ options)
97
+ end
98
+ end
99
+
100
+ def zrevrank(key, member)
101
+ with_zset_at(key) {|z| z.sorted_members.reverse.index(member.to_s) }
102
+ end
103
+
104
+ def zscore(key, member)
105
+ with_zset_at(key) do |z|
106
+ score = z.score(member.to_s)
107
+ score.to_s if score
108
+ end
109
+ end
110
+
111
+ def zunionstore(destination, keys, options={})
112
+ assert_has_args(keys, 'zunionstore')
113
+
114
+ data[destination] = combine_weighted_zsets(keys, options, :union)
115
+ zcard(destination)
116
+ end
117
+
118
+ private
119
+ def apply_limit(collection, limit)
120
+ if limit
121
+ if limit.is_a?(Array) && limit.length == 2
122
+ offset, count = limit
123
+ collection.drop(offset).take(count)
124
+ else
125
+ raise RuntimeError, "ERR syntax error"
126
+ end
127
+ else
128
+ collection
129
+ end
130
+ end
131
+
132
+ def to_response(score_member_pairs, options)
133
+ score_member_pairs.map do |(score,member)|
134
+ if options[:with_scores] || options[:withscores]
135
+ [member, score.to_s]
136
+ else
137
+ member
138
+ end
139
+ end.flatten
140
+ end
141
+
142
+ def combine_weighted_zsets(keys, options, how)
143
+ weights = options.fetch(:weights, keys.map { 1 })
144
+ if weights.length != keys.length
145
+ raise RuntimeError, "ERR syntax error"
146
+ end
147
+
148
+ aggregator = case options.fetch(:aggregate, :sum).to_s.downcase.to_sym
149
+ when :sum
150
+ proc {|a,b| [a,b].compact.reduce(&:+)}
151
+ when :min
152
+ proc {|a,b| [a,b].compact.min}
153
+ when :max
154
+ proc {|a,b| [a,b].compact.max}
155
+ else
156
+ raise RuntimeError, "ERR syntax error"
157
+ end
158
+
159
+ with_zsets_at(*keys) do |*zsets|
160
+ zsets.zip(weights).map do |(zset, weight)|
161
+ zset.reduce(Zset.new) do |acc, (score, member)|
162
+ acc.add(score * weight, member)
163
+ end
164
+ end.reduce do |za, zb|
165
+ za.send(how, zb, &aggregator)
166
+ end
167
+ end
168
+
169
+ end
170
+
171
+ def with_zset_at(key, &blk)
172
+ with_thing_at(key, :assert_zsety, proc {Zset.new}, &blk)
173
+ end
174
+
175
+ def with_zsets_at(*keys, &blk)
176
+ if keys.length == 1
177
+ with_zset_at(keys.first, &blk)
178
+ else
179
+ with_zset_at(keys.first) do |set|
180
+ with_zsets_at(*(keys[1..-1])) do |*sets|
181
+ blk.call(*([set] + sets))
182
+ end
183
+ end
184
+ end
185
+ end
186
+
187
+ def zsety?(key)
188
+ data[key].nil? || data[key].kind_of?(Zset)
189
+ end
190
+
191
+ def assert_zsety(key)
192
+ unless zsety?(key)
193
+ raise RuntimeError,
194
+ "ERR Operation against a key holding the wrong kind of value"
195
+ end
196
+ end
197
+
198
+ def looks_like_float?(x)
199
+ # ugh, exceptions for flow control.
200
+ !!Float(x) rescue false
201
+ end
202
+
203
+ def assert_scorey(value, what='value')
204
+ unless looks_like_float?(value)
205
+ raise RuntimeError, "ERR #{what} is not a double"
206
+ end
207
+ end
208
+
209
+ end
210
+ end
@@ -0,0 +1,119 @@
1
+ require 'set'
2
+
3
+ require 'mock_redis/assertions'
4
+ require 'mock_redis/database'
5
+ require 'mock_redis/distributed'
6
+ require 'mock_redis/expire_wrapper'
7
+ require 'mock_redis/multi_db_wrapper'
8
+ require 'mock_redis/transaction_wrapper'
9
+ require 'mock_redis/undef_redis_methods'
10
+
11
+ class MockRedis
12
+ include UndefRedisMethods
13
+
14
+ attr_reader :options
15
+
16
+ DEFAULTS = {
17
+ :scheme => "redis",
18
+ :host => "127.0.0.1",
19
+ :port => 6379,
20
+ :path => nil,
21
+ :timeout => 5.0,
22
+ :password => nil,
23
+ :db => 0,
24
+ }
25
+
26
+ def initialize(*args)
27
+ @options = _parse_options(args.first)
28
+
29
+ @db = TransactionWrapper.new(
30
+ ExpireWrapper.new(
31
+ MultiDbWrapper.new(
32
+ Database.new(*args))))
33
+ end
34
+
35
+ def id
36
+ "redis://#{self.host}:#{self.port}/#{self.db}"
37
+ end
38
+
39
+ def call(command, &block)
40
+ self.send(*command)
41
+ end
42
+
43
+ def host
44
+ self.options[:host]
45
+ end
46
+
47
+ def port
48
+ self.options[:port]
49
+ end
50
+
51
+ def db
52
+ self.options[:db]
53
+ end
54
+
55
+ def client
56
+ self
57
+ end
58
+
59
+ def respond_to?(method, include_private=false)
60
+ super || @db.respond_to?(method, include_private)
61
+ end
62
+
63
+ def method_missing(method, *args, &block)
64
+ @db.send(method, *args, &block)
65
+ end
66
+
67
+ def initialize_copy(source)
68
+ super
69
+ @db = @db.clone
70
+ end
71
+
72
+
73
+ protected
74
+
75
+ def _parse_options(options)
76
+ return {} if options.nil?
77
+
78
+ defaults = DEFAULTS.dup
79
+
80
+ url = options[:url] || ENV["REDIS_URL"]
81
+
82
+ # Override defaults from URL if given
83
+ if url
84
+ require "uri"
85
+
86
+ uri = URI(url)
87
+
88
+ if uri.scheme == "unix"
89
+ defaults[:path] = uri.path
90
+ else
91
+ # Require the URL to have at least a host
92
+ raise ArgumentError, "invalid url" unless uri.host
93
+
94
+ defaults[:scheme] = uri.scheme
95
+ defaults[:host] = uri.host
96
+ defaults[:port] = uri.port if uri.port
97
+ defaults[:password] = uri.password if uri.password
98
+ defaults[:db] = uri.path[1..-1].to_i if uri.path
99
+ end
100
+ end
101
+
102
+ options = defaults.merge(options)
103
+
104
+ if options[:path]
105
+ options[:scheme] = "unix"
106
+ options.delete(:host)
107
+ options.delete(:port)
108
+ else
109
+ options[:host] = options[:host].to_s
110
+ options[:port] = options[:port].to_i
111
+ end
112
+
113
+ options[:timeout] = options[:timeout].to_f
114
+ options[:db] = options[:db].to_i
115
+
116
+ options
117
+ end
118
+
119
+ end
@@ -0,0 +1,58 @@
1
+ describe Redis::Helper do
2
+ before do
3
+ Redis.should_receive(:new).any_number_of_times {|options|
4
+ MockRedis.new(options)
5
+ }
6
+
7
+ @migrator = Redis::Migrator.new(["redis://localhost:6379"],
8
+ ["redis://localhost:6377"])
9
+
10
+ @r1 = @migrator.old_cluster
11
+ @r2 = @migrator.new_cluster
12
+ @migrator.stub!(:redis).and_return(@r1)
13
+ @pipe = PipeMock.new(@r2)
14
+ end
15
+
16
+ it "should copy a string" do
17
+ @r1.set("a", "some_string")
18
+ @migrator.copy_string(@pipe, "a")
19
+
20
+ @r2.get("a").should == "some_string"
21
+ end
22
+
23
+ it "should copy a hash" do
24
+ @r1.hmset("myhash",
25
+ "first_name", "James",
26
+ "last_name", "Randi",
27
+ "age", "83")
28
+
29
+ @migrator.copy_hash(@pipe, "myhash")
30
+
31
+ @r2.hgetall("myhash").should == {"first_name" => "James", "last_name" => "Randi", "age" => "83"}
32
+ end
33
+
34
+ it "should copy a list" do
35
+ ('a'..'z').to_a.each { |val| @r1.lpush("mylist", val) }
36
+
37
+ @migrator.copy_list(@pipe, "mylist")
38
+
39
+ @r2.lrange("mylist", 0, -1).should == ('a'..'z').to_a
40
+ end
41
+
42
+ it "should copy a set" do
43
+ ('a'..'z').to_a.each { |val| @r1.sadd("myset", val) }
44
+
45
+ @migrator.copy_set(@pipe, "myset")
46
+
47
+ @r2.smembers("myset").should == ('a'..'z').to_a
48
+ end
49
+
50
+ it "should copy zset" do
51
+ ('a'..'z').to_a.each { |val| @r1.zadd("myzset", rand(100), val) }
52
+
53
+ @migrator.copy_zset(@pipe, "myzset")
54
+
55
+ @r2.zrange("myzset", 0, -1, :with_scores => true).sort.should == @r1.zrange("myzset", 0, -1, :with_scores => true).sort
56
+ end
57
+
58
+ end
@@ -0,0 +1,29 @@
1
+ $LOAD_PATH.unshift File.expand_path(File.join(File.dirname(__FILE__), "mock_redis/lib"))
2
+
3
+ require_relative "../lib/redis_migrator.rb"
4
+
5
+ # include patched version of mock_redis
6
+ # that works with Redis::Distributed
7
+ require "mock_redis.rb"
8
+
9
+ class PipeMock
10
+ def initialize(redis)
11
+ @redis = redis
12
+ end
13
+
14
+ def close; true; end
15
+
16
+ def <<(val)
17
+ val[0] = val[0].downcase.to_sym
18
+ @redis.send(*val)
19
+ end
20
+ end
21
+
22
+
23
+ class Redis
24
+ module Helper
25
+ def to_redis_proto(*cmd)
26
+ cmd
27
+ end
28
+ end
29
+ end
metadata ADDED
@@ -0,0 +1,107 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: redis_migrator
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Artem Yankov
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-07-05 00:00:00.000000000Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: redis
16
+ requirement: &70245281843860 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: 2.2.2
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *70245281843860
25
+ description: Redis-migrator takes a list of nodes for your old cluster and list of
26
+ nodes for your new cluster and determines for which keys routes were changed. Then
27
+ it moves those keys to new nodes.
28
+ email:
29
+ - artem.yankov@gmail.com
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - .gitignore
35
+ - .rspec
36
+ - README.md
37
+ - lib/redis_migrator.rb
38
+ - lib/redis_migrator/redis_helper.rb
39
+ - lib/redis_migrator/redis_populator.rb
40
+ - migrator_benchmark.rb
41
+ - redis_migrator.gemspec
42
+ - spec/migrator_spec.rb
43
+ - spec/mock_redis/lib/mock_redis.rb
44
+ - spec/mock_redis/lib/mock_redis/assertions.rb
45
+ - spec/mock_redis/lib/mock_redis/database.rb
46
+ - spec/mock_redis/lib/mock_redis/distributed.rb
47
+ - spec/mock_redis/lib/mock_redis/exceptions.rb
48
+ - spec/mock_redis/lib/mock_redis/expire_wrapper.rb
49
+ - spec/mock_redis/lib/mock_redis/hash_methods.rb
50
+ - spec/mock_redis/lib/mock_redis/list_methods.rb
51
+ - spec/mock_redis/lib/mock_redis/multi_db_wrapper.rb
52
+ - spec/mock_redis/lib/mock_redis/set_methods.rb
53
+ - spec/mock_redis/lib/mock_redis/string_methods.rb
54
+ - spec/mock_redis/lib/mock_redis/transaction_wrapper.rb
55
+ - spec/mock_redis/lib/mock_redis/undef_redis_methods.rb
56
+ - spec/mock_redis/lib/mock_redis/utility_methods.rb
57
+ - spec/mock_redis/lib/mock_redis/version.rb
58
+ - spec/mock_redis/lib/mock_redis/zset.rb
59
+ - spec/mock_redis/lib/mock_redis/zset_methods.rb
60
+ - spec/redis_helper_spec.rb
61
+ - spec/spec_helper.rb
62
+ homepage: http://rubygems.org/gems/redis_migrator
63
+ licenses: []
64
+ post_install_message:
65
+ rdoc_options: []
66
+ require_paths:
67
+ - lib
68
+ required_ruby_version: !ruby/object:Gem::Requirement
69
+ none: false
70
+ requirements:
71
+ - - ! '>='
72
+ - !ruby/object:Gem::Version
73
+ version: '0'
74
+ required_rubygems_version: !ruby/object:Gem::Requirement
75
+ none: false
76
+ requirements:
77
+ - - ! '>='
78
+ - !ruby/object:Gem::Version
79
+ version: '0'
80
+ requirements: []
81
+ rubyforge_project:
82
+ rubygems_version: 1.8.17
83
+ signing_key:
84
+ specification_version: 3
85
+ summary: A tool to redistribute keys in your redis cluster when its topography has
86
+ changed
87
+ test_files:
88
+ - spec/migrator_spec.rb
89
+ - spec/mock_redis/lib/mock_redis.rb
90
+ - spec/mock_redis/lib/mock_redis/assertions.rb
91
+ - spec/mock_redis/lib/mock_redis/database.rb
92
+ - spec/mock_redis/lib/mock_redis/distributed.rb
93
+ - spec/mock_redis/lib/mock_redis/exceptions.rb
94
+ - spec/mock_redis/lib/mock_redis/expire_wrapper.rb
95
+ - spec/mock_redis/lib/mock_redis/hash_methods.rb
96
+ - spec/mock_redis/lib/mock_redis/list_methods.rb
97
+ - spec/mock_redis/lib/mock_redis/multi_db_wrapper.rb
98
+ - spec/mock_redis/lib/mock_redis/set_methods.rb
99
+ - spec/mock_redis/lib/mock_redis/string_methods.rb
100
+ - spec/mock_redis/lib/mock_redis/transaction_wrapper.rb
101
+ - spec/mock_redis/lib/mock_redis/undef_redis_methods.rb
102
+ - spec/mock_redis/lib/mock_redis/utility_methods.rb
103
+ - spec/mock_redis/lib/mock_redis/version.rb
104
+ - spec/mock_redis/lib/mock_redis/zset.rb
105
+ - spec/mock_redis/lib/mock_redis/zset_methods.rb
106
+ - spec/redis_helper_spec.rb
107
+ - spec/spec_helper.rb