redis_ring 0.0.2 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,42 @@
1
+ module RedisRing
2
+ class SlaveRPC
3
+
4
+ attr_reader :http_client
5
+
6
+ def initialize(http_client)
7
+ @http_client = http_client
8
+ end
9
+
10
+ def connection(host, port)
11
+ Connection.new(http_client, host, port)
12
+ end
13
+
14
+ class Connection
15
+
16
+ attr_reader :http_client, :host, :port
17
+
18
+ def initialize(http_client, host, port)
19
+ @http_client = http_client
20
+ @host = host
21
+ @port = port
22
+ end
23
+
24
+ def join
25
+ http_client.post(host, port, "/slave/join")
26
+ end
27
+
28
+ def status
29
+ JSON.parse(http_client.get(host, port, "/slave/status"))
30
+ end
31
+
32
+ def start_shard(shard_no)
33
+ http_client.post(host, port, "/slave/start_shard/#{shard_no}")
34
+ end
35
+
36
+ def stop_shard(shard_no)
37
+ http_client.post(host, port, "/slave/stop_shard/#{shard_no}")
38
+ end
39
+
40
+ end
41
+ end
42
+ end
@@ -1,3 +1,3 @@
1
1
  module RedisRing
2
- VERSION = "0.0.2"
2
+ VERSION = "0.1.0"
3
3
  end
@@ -1,14 +1,75 @@
1
1
  module RedisRing
2
2
 
3
+ class WebInterfaceRunner
4
+
5
+ include RedisRing::BackgroundThread
6
+
7
+ attr_reader :master, :slave
8
+
9
+ def initialize(port, master, slave)
10
+ @port = port
11
+ @master = master
12
+ @slave = slave
13
+ end
14
+
15
+ def do_work
16
+ handler = Rack::Handler.get("webrick")
17
+ handler.run(WebInterface, :Port => @port, :master => @master, :slave => @slave) do |server|
18
+ @server = server
19
+ WebInterface.set :master, master
20
+ WebInterface.set :slave, slave
21
+ WebInterface.set :running, true
22
+ end
23
+ end
24
+
25
+ def halt
26
+ super
27
+ @server.stop if @server
28
+ end
29
+
30
+ end
31
+
3
32
  class WebInterface < Sinatra::Base
4
33
 
34
+ def master
35
+ self.class.master
36
+ end
37
+
38
+ def slave
39
+ self.class.slave
40
+ end
41
+
5
42
  get "/" do
6
43
  "RedisRing is running"
7
44
  end
8
45
 
9
- get "/shards" do
46
+ post "/master/node_joined/:node_id" do
47
+ master.node_joined(params[:node_id])
48
+ "OK"
49
+ end
50
+
51
+ get "/slave/status" do
10
52
  content_type :json
11
- Application.instance.shards_hash.to_json
53
+ slave.status.to_json
54
+ end
55
+
56
+ post "/slave/join" do
57
+ slave.join
58
+ "OK"
59
+ end
60
+
61
+ post "/slave/start_shard/:shard_no" do
62
+ slave.start_shard(params[:shard_no].to_i)
63
+ "OK"
64
+ end
65
+
66
+ post "/slave/stop_shard/:shard_no" do
67
+ slave.stop_shard(params[:shard_no].to_i)
68
+ "OK"
69
+ end
70
+
71
+ class << self
72
+ attr_accessor :master, :slave
12
73
  end
13
74
 
14
75
  end
@@ -0,0 +1,73 @@
1
+ module RedisRing
2
+
3
+ class ZookeeperConnection
4
+
5
+ attr_reader :current_node
6
+
7
+ def initialize(host_name, base_port, zookeeper_address)
8
+ @host_name = host_name
9
+ @base_port = base_port
10
+ @zookeeper_address = zookeeper_address
11
+ @connected = false
12
+ @base_path = "/nodes"
13
+ @mutex = Mutex.new
14
+ end
15
+
16
+ def nodes_changed?
17
+ return true unless nodes_watcher
18
+ return nodes_watcher.completed?
19
+ end
20
+
21
+ def nodes
22
+ @nodes_watcher = Zookeeper::WatcherCallback.new
23
+ resp = zookeeper.get_children(:path => base_path, :watcher => nodes_watcher, :watcher_context => base_path)
24
+ return resp[:children].sort
25
+ end
26
+
27
+ def node_data(node)
28
+ resp = zookeeper.get(:path => "#{base_path}/#{node}")
29
+ data = resp[:data]
30
+ return data ? JSON.parse(data) : nil
31
+ end
32
+
33
+ def update_status(status)
34
+ zookeeper.set(:path => "#{base_path}/status", :data => status.to_json)
35
+ end
36
+
37
+ def connected?
38
+ @connected
39
+ end
40
+
41
+ def connect
42
+ @mutex.synchronize do
43
+ break if connected?
44
+
45
+ @zookeeper = Zookeeper.new(zookeeper_address)
46
+
47
+ if @zookeeper.state != Zookeeper::ZOO_CONNECTED_STATE
48
+ raise "Zookeeper not connected!"
49
+ end
50
+
51
+ resp = @zookeeper.create(:path => base_path)
52
+ #raise "Could not create base path" unless resp[:rc] == Zookeeper::ZOK
53
+
54
+ resp = @zookeeper.create(:path => "#{base_path}/node-", :ephemeral => true, :sequence => true, :data => current_node_data.to_json)
55
+ #raise "Could not create node" unless resp[:rc] == Zookeeper::ZOK
56
+
57
+ @current_node = resp[:path].gsub("#{base_path}/", '')
58
+
59
+ @connected = true
60
+ end
61
+ end
62
+
63
+ protected
64
+
65
+ attr_reader :zookeeper, :base_path, :nodes_watcher, :zookeeper_address, :host_name, :base_port
66
+
67
+ def current_node_data
68
+ {:host => host_name, :port => base_port}
69
+ end
70
+
71
+ end
72
+
73
+ end
@@ -0,0 +1,47 @@
1
+ module RedisRing
2
+
3
+ class ZookeeperObserver
4
+
5
+ include RedisRing::BackgroundThread
6
+
7
+ attr_reader :master, :slave, :zookeeper_connection
8
+
9
+ def initialize(zookeeper_connection, master, slave)
10
+ @zookeeper_connection = zookeeper_connection
11
+ @master = master
12
+ @slave = slave
13
+ @current_master = nil
14
+ end
15
+
16
+ def do_work
17
+ on_node_list_changed(zookeeper_connection.nodes) if zookeeper_connection.nodes_changed?
18
+ sleep(0.1)
19
+ end
20
+
21
+ protected
22
+
23
+ def on_node_list_changed(new_nodes)
24
+ current_master = new_nodes.first
25
+
26
+ unless @current_master == current_master
27
+ @current_master = current_master
28
+
29
+ if current_master == zookeeper_connection.current_node
30
+ master.became_master
31
+ else
32
+ master.no_longer_is_master
33
+ end
34
+
35
+ current_master_data = zookeeper_connection.node_data(current_master)
36
+ slave.current_master_host = current_master_data["host"]
37
+ slave.current_master_port = current_master_data["port"]
38
+
39
+ puts "NEW MASTER IS: #{slave.current_master_host}:#{slave.current_master_port}"
40
+ end
41
+
42
+ master.nodes_changed(new_nodes)
43
+ end
44
+
45
+ end
46
+
47
+ end
data/lib/redis_ring.rb CHANGED
@@ -2,17 +2,28 @@ require 'socket'
2
2
  require 'yaml'
3
3
  require 'erb'
4
4
  require 'fileutils'
5
+ require 'net/http'
5
6
 
6
7
  require 'sinatra'
7
8
  require 'json'
8
9
  require 'daemons'
10
+ require 'zookeeper'
9
11
 
10
12
  require 'monkey_patches'
11
13
 
14
+ require 'redis_ring/background_thread'
12
15
  require 'redis_ring/configuration'
13
16
  require 'redis_ring/shard_config'
14
17
  require 'redis_ring/shard'
18
+ require 'redis_ring/http_client'
19
+ require 'redis_ring/node'
15
20
  require 'redis_ring/application'
16
21
  require 'redis_ring/web_interface'
17
22
  require 'redis_ring/process_manager'
23
+ require 'redis_ring/master'
24
+ require 'redis_ring/master_rpc'
25
+ require 'redis_ring/slave'
26
+ require 'redis_ring/slave_rpc'
27
+ require 'redis_ring/zookeeper_observer'
28
+ require 'redis_ring/zookeeper_connection'
18
29
  require 'redis_ring/cli'
data/redis_ring.gemspec CHANGED
@@ -17,9 +17,11 @@ Gem::Specification.new do |s|
17
17
  s.add_dependency 'sinatra'
18
18
  s.add_dependency 'json'
19
19
  s.add_dependency 'daemons-mikehale'
20
+ s.add_dependency 'zookeeper'
20
21
 
21
22
  s.add_development_dependency 'rspec'
22
23
  s.add_development_dependency 'mocha'
24
+ s.add_development_dependency 'simplecov'
23
25
 
24
26
  s.files = `git ls-files`.split("\n")
25
27
  s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
@@ -0,0 +1,224 @@
1
+ class ClusterBuilder
2
+
3
+ def initialize
4
+ yield self
5
+ end
6
+
7
+ def nodes
8
+ @nodes ||= {}
9
+ end
10
+
11
+ def node(node_id)
12
+ nodes[node_id] ||= NodeBuilder.new(node_id, self)
13
+ end
14
+
15
+ def node_ids
16
+ return nodes.keys.sort
17
+ end
18
+
19
+ def start_shard(node_id, shard_number)
20
+ start_shard_callbacks.each do |block|
21
+ block.call(node_id, shard_number)
22
+ end
23
+ end
24
+
25
+ def stop_shard(node_id, shard_number)
26
+ stop_shard_callbacks.each do |block|
27
+ block.call(node_id, shard_number)
28
+ end
29
+ end
30
+
31
+ def on_start_shard(&block)
32
+ start_shard_callbacks << block
33
+ end
34
+
35
+ def on_stop_shard(&block)
36
+ stop_shard_callbacks << block
37
+ end
38
+
39
+ def start_shard_callbacks
40
+ @start_shard_callbacks ||= []
41
+ end
42
+
43
+ def stop_shard_callbacks
44
+ @stop_shard_callbacks ||= []
45
+ end
46
+
47
+ def fake_provider
48
+ @fake_provider ||= FakeNodeProvider.new(self)
49
+ end
50
+
51
+ def fake_connection
52
+ @fake_connection ||= FakeConnection.new(self)
53
+ end
54
+
55
+ class FakeNodeProvider
56
+ def initialize(cluster_builder)
57
+ @cluster_builder = cluster_builder
58
+ @count = 0
59
+ end
60
+
61
+ def new(host, port)
62
+ result = @cluster_builder.nodes.detect{|_, node| node.get.host == host && node.get.port == port }
63
+ return result[1].get if result
64
+ return @cluster_builder.node("unknown-node-#{@count += 1}").host(host).port(port).reachable(false).get
65
+ end
66
+ end
67
+
68
+ class FakeConnection
69
+ def initialize(cluster_builder)
70
+ @cluster_builder = cluster_builder
71
+ end
72
+
73
+ def node_data(node_id)
74
+ return nil unless @cluster_builder.nodes.key?(node_id)
75
+ node = @cluster_builder.node(node_id).get
76
+ return {"host" => node.host, "port" => node.port}
77
+ end
78
+
79
+ def update_status(status)
80
+ @status = status
81
+ end
82
+ end
83
+
84
+ class NodeBuilder
85
+ def initialize(node_id, cluster_builder)
86
+ @node_id = node_id
87
+ @joined = true
88
+ @running_shards = []
89
+ @available_shards = {}
90
+ @host = "localhost"
91
+ @port = 6400
92
+ @reachable = true
93
+ @cluster_builder = cluster_builder
94
+ end
95
+
96
+ def host(str)
97
+ @host = str
98
+ self
99
+ end
100
+
101
+ def port(int)
102
+ @port = int
103
+ self
104
+ end
105
+
106
+ def reachable(bool)
107
+ @reachable = bool
108
+ self
109
+ end
110
+
111
+ def joined(bool)
112
+ @joined = bool
113
+ self
114
+ end
115
+
116
+ def running_shards(arr)
117
+ @running_shards = arr
118
+ self
119
+ end
120
+
121
+ def running_shard(shard)
122
+ @running_shards << shard unless @running_shards.include?(shard)
123
+ self
124
+ end
125
+
126
+ def not_running_shard(shard)
127
+ @running_shards.delete(shard)
128
+ self
129
+ end
130
+
131
+ def available_shards(hash)
132
+ @available_shards = hash
133
+ self
134
+ end
135
+
136
+ def available_shard(shard, timestamp)
137
+ @available_shards[shard] = timestamp
138
+ self
139
+ end
140
+
141
+ def not_available_shard(shard)
142
+ @available_shards.delete(shard)
143
+ self
144
+ end
145
+
146
+ def load_properties(node)
147
+ node.load(props)
148
+ end
149
+
150
+ def props
151
+ {
152
+ :host => @host,
153
+ :port => @port,
154
+ :node_id => @node_id,
155
+ :running_shards => @running_shards,
156
+ :available_shards => @available_shards,
157
+ :joined => @joined,
158
+ :reachable => @reachable,
159
+ :cluster_builder => @cluster_builder
160
+ }
161
+ end
162
+
163
+ def get
164
+ @node ||= FakeNode.new(self)
165
+ end
166
+ end
167
+
168
+ class FakeNode
169
+
170
+ attr_reader :host, :port, :node_id
171
+
172
+ def initialize(builder)
173
+ @builder = builder
174
+ @builder.load_properties(self)
175
+ end
176
+
177
+ def load(opts)
178
+ @joined = opts[:joined]
179
+ @running_shards = opts[:running_shards]
180
+ @available_shards = opts[:available_shards]
181
+ @reachable = opts[:reachable]
182
+ @host = opts[:host]
183
+ @port = opts[:port]
184
+ @node_id = opts[:node_id]
185
+ @cluster_builder = opts[:cluster_builder]
186
+ end
187
+
188
+ def joined?
189
+ @joined
190
+ end
191
+
192
+ def available_shards
193
+ @available_shards
194
+ end
195
+
196
+ def running_shards
197
+ @running_shards
198
+ end
199
+
200
+ def start_shard(shard_number)
201
+ ensure_reachable
202
+ @running_shards << shard_number unless @running_shards.include?(shard_number)
203
+ @cluster_builder.start_shard(@node_id, shard_number)
204
+ end
205
+
206
+ def stop_shard(shard_number)
207
+ ensure_reachable
208
+ @running_shards.delete(shard_number)
209
+ @cluster_builder.stop_shard(@node_id, shard_number)
210
+ end
211
+
212
+ def update_status!
213
+ @builder.load_properties(self)
214
+ ensure_reachable
215
+ end
216
+
217
+ protected
218
+
219
+ def ensure_reachable
220
+ raise "Node #{@node_id} #{@host}:#{@port} is not reachable!" unless @reachable
221
+ end
222
+ end
223
+
224
+ end
@@ -0,0 +1,39 @@
1
+ class FakeHttpClient < RedisRing::HttpClient
2
+
3
+ def posts
4
+ @posts ||= []
5
+ end
6
+
7
+ def gets
8
+ @gets ||= []
9
+ end
10
+
11
+ def responses
12
+ @responses ||= Hash.new("OK")
13
+ end
14
+
15
+ def post(host, port, path, params = {})
16
+ url = uri(host, port, path, params).to_s
17
+ posts << url
18
+ return responses[url]
19
+ end
20
+
21
+ def get(host, port, path, params = {})
22
+ url = uri(host, port, path, params).to_s
23
+ gets << url
24
+ return responses[url]
25
+ end
26
+
27
+ def set_response(url, text)
28
+ responses[url] = text
29
+ end
30
+
31
+ def sent_post?(url)
32
+ posts.include?(url)
33
+ end
34
+
35
+ def sent_get?(url)
36
+ gets.include?(url)
37
+ end
38
+
39
+ end
@@ -0,0 +1,20 @@
1
+ class FakeMasterRPC
2
+
3
+ def connection(host, port)
4
+ @connections ||= {}
5
+ @connections["#{host}:#{port}"] ||= Connection.new
6
+ end
7
+
8
+ class Connection
9
+
10
+ def nodes_loaded
11
+ @node_loaded ||= []
12
+ end
13
+
14
+ def node_loaded(node_id)
15
+ nodes_loaded << node_id
16
+ end
17
+
18
+ end
19
+
20
+ end
@@ -0,0 +1,2 @@
1
+ class FakeNodeProvider
2
+ end
@@ -0,0 +1,19 @@
1
+ class FakeProcessManager
2
+
3
+ def started_shards
4
+ @started_shards ||= []
5
+ end
6
+
7
+ def start_shard(shard)
8
+ started_shards << shard
9
+ end
10
+
11
+ def stopped_shards
12
+ @stopped_shards ||= []
13
+ end
14
+
15
+ def stop_shard(shard)
16
+ stopped_shards << shard
17
+ end
18
+
19
+ end
@@ -0,0 +1,36 @@
1
+ class FakeSlaveRPC
2
+
3
+ def connection(host, port)
4
+ @connections ||= {}
5
+ @connections["#{host}:#{port}"] ||= Connection.new
6
+ end
7
+
8
+ class Connection
9
+
10
+ def status=(val)
11
+ @status = val
12
+ end
13
+
14
+ def status
15
+ @status ||= {"joined" => false, "running_shards" => [], "available_shards" => {}}
16
+ end
17
+
18
+ def started_shards
19
+ @started_shards ||= []
20
+ end
21
+
22
+ def start_shard(shard_number)
23
+ started_shards << shard_number
24
+ end
25
+
26
+ def stopped_shards
27
+ @stopped_shards ||= []
28
+ end
29
+
30
+ def stop_shard(shard_number)
31
+ stopped_shards << shard_number
32
+ end
33
+
34
+ end
35
+
36
+ end
@@ -0,0 +1,2 @@
1
+ class FakeZookeeperConnection
2
+ end
@@ -2,22 +2,24 @@ require File.expand_path("../../spec_helper", __FILE__)
2
2
 
3
3
  describe RedisRing::Application do
4
4
 
5
- describe "#shards_hash" do
6
- before(:each) do
7
- RedisRing::Shard.any_instance.stubs(:fork_redis_server => stub(:start => true, :stop => true, :running? => true))
8
- RedisRing::ShardConfig.any_instance.stubs(:save)
9
- RedisRing::ShardConfig.any_instance.stubs(:alive? => true)
5
+ before(:each) do
6
+ @conf = RedisRing::Configuration.from_yml_file(File.expand_path("../../test.conf", __FILE__))
7
+ @app = RedisRing::Application.new(@conf)
8
+ end
9
+
10
+ it "should run shards" do
11
+ @app.start
12
+ sleep(1)
10
13
 
11
- @application = RedisRing::Application.new(RedisRing::Configuration.new)
12
- @application.start
13
- end
14
+ @app.slave.join
15
+ sleep(3)
14
16
 
15
- it "should return all shards" do
16
- shard_hash = @application.shards_hash
17
+ @app.slave.running_shards.keys.sort.should == [0, 1, 2, 3]
18
+ end
17
19
 
18
- shard_hash[:count].should == @application.configuration.ring_size
19
- shard_hash[:shards].size.should == @application.configuration.ring_size
20
- end
20
+ after(:each) do
21
+ @app.stop
22
+ @app.wait
21
23
  end
22
24
 
23
25
  end
@@ -0,0 +1,20 @@
1
+ require File.expand_path("../../spec_helper", __FILE__)
2
+
3
+ describe RedisRing::MasterRPC do
4
+
5
+ before(:each) do
6
+ @http_client = FakeHttpClient.new
7
+ @host = "example.com"
8
+ @port = 666
9
+ @rpc = RedisRing::MasterRPC.new(@http_client).connection(@host, @port)
10
+ end
11
+
12
+ describe :node_joined do
13
+ it "should post to node joined url" do
14
+ @rpc.node_loaded("some_node_id")
15
+
16
+ @http_client.sent_post?("http://example.com:666/master/node_joined/some_node_id").should be_true
17
+ end
18
+ end
19
+
20
+ end