redis_migrator 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,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