redis_ring 0.0.2 → 0.1.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/.gitignore CHANGED
@@ -1,4 +1,7 @@
1
1
  pkg/*
2
+ tmp/*
3
+ coverage/*
2
4
  *.gem
3
5
  .bundle
4
6
  .rvmrc
7
+ tags
data/Gemfile.lock CHANGED
@@ -1,10 +1,11 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- redis_ring (0.0.1)
4
+ redis_ring (0.0.2)
5
5
  daemons-mikehale
6
6
  json
7
7
  sinatra
8
+ zookeeper
8
9
 
9
10
  GEM
10
11
  remote: http://rubygems.org/
@@ -13,7 +14,7 @@ GEM
13
14
  diff-lcs (1.1.2)
14
15
  json (1.5.1)
15
16
  mocha (0.9.12)
16
- rack (1.2.1)
17
+ rack (1.2.2)
17
18
  rspec (2.5.0)
18
19
  rspec-core (~> 2.5.0)
19
20
  rspec-expectations (~> 2.5.0)
@@ -22,10 +23,14 @@ GEM
22
23
  rspec-expectations (2.5.0)
23
24
  diff-lcs (~> 1.1.2)
24
25
  rspec-mocks (2.5.0)
25
- sinatra (1.2.0)
26
+ simplecov (0.4.1)
27
+ simplecov-html (~> 0.4.3)
28
+ simplecov-html (0.4.3)
29
+ sinatra (1.2.1)
26
30
  rack (~> 1.1)
27
31
  tilt (>= 1.2.2, < 2.0)
28
32
  tilt (1.2.2)
33
+ zookeeper (0.4.3)
29
34
 
30
35
  PLATFORMS
31
36
  ruby
@@ -34,3 +39,4 @@ DEPENDENCIES
34
39
  mocha
35
40
  redis_ring!
36
41
  rspec
42
+ simplecov
@@ -20,18 +20,16 @@ loglevel notice
20
20
 
21
21
  databases 2048
22
22
 
23
- save 900 1
24
- save 300 10
25
- save 60 10000
23
+ # save 900 1
24
+ # save 300 10
25
+ # save 60 10000
26
26
 
27
27
  rdbcompression yes
28
28
 
29
29
  # maxclients 128
30
30
  # maxmemory <bytes>
31
31
 
32
- appendonly no
33
-
34
- # The name of the append only file (default: "appendonly.aof")
32
+ appendonly yes
35
33
 
36
34
  # appendfsync always
37
35
  appendfsync everysec
@@ -2,50 +2,50 @@ module RedisRing
2
2
 
3
3
  class Application
4
4
 
5
- attr_reader :shards, :configuration, :process_manager
5
+ attr_reader :shards, :configuration, :process_manager, :zookeeper_observer, :master, :slave, :zookeeper_connection, :master_rpc, :http_client, :node_provider, :slave_rpc
6
6
 
7
- def initialize(configuration)
8
- @configuration = configuration
7
+ def initialize(config)
8
+ @configuration = config
9
9
  @process_manager = ProcessManager.new
10
- @shards = {}
10
+ @http_client = HttpClient.new
11
+ @master_rpc = MasterRPC.new(http_client)
12
+ @slave_rpc = SlaveRPC.new(http_client)
13
+ @node_provider = NodeProvider.new(slave_rpc)
14
+ @zookeeper_connection = ZookeeperConnection.new(config.host_name,
15
+ config.base_port,
16
+ config.zookeeper_address)
17
+ @master = Master.new(zookeeper_connection, config.ring_size, node_provider)
18
+ @slave = Slave.new(configuration, master_rpc, process_manager)
19
+ @zookeeper_observer = ZookeeperObserver.new(zookeeper_connection, master, slave)
20
+ @web_interface_runner = WebInterfaceRunner.new(config.base_port, master, slave)
11
21
  end
12
22
 
13
23
  def start
14
24
  self.stop
15
25
 
16
- @configuration.ring_size.times do |shard_number|
17
- shard_conf = ShardConfig.new(shard_number, configuration)
18
- @shards[shard_number] = Shard.new(shard_conf)
19
- end
20
-
21
- @shards.each do |shard_no, shard|
22
- @process_manager.start_shard(shard)
23
- end
26
+ @web_thread = @web_interface_runner.run
24
27
 
25
- @process_manager.run
26
- end
28
+ @zookeeper_connection.connect
29
+ @slave.node_id = @zookeeper_connection.current_node
27
30
 
28
- def stop
29
- @process_manager.halt
31
+ @zookeeper_thread = @zookeeper_observer.run
32
+ @pm_thread = @process_manager.run
30
33
 
31
- @shards.each do |shard_no, shard|
32
- @process_manager.stop_shard(shard)
34
+ [:INT, :TERM, :QUIT].each do |sig|
35
+ trap(sig) { self.stop }
33
36
  end
34
-
35
- @shards = {}
36
37
  end
37
38
 
38
- def shards_hash
39
- shards_hash = {}
40
- shards.each do |shard_no, shard|
41
- shards_hash[shard_no] = { :host => shard.host, :port => shard.port, :status => shard.status }
42
- end
43
-
44
- return { :count => configuration.ring_size, :shards => shards_hash }
39
+ def wait
40
+ @pm_thread.join if @pm_thread
41
+ @zookeeper_thread.join if @zookeeper_thread
42
+ @web_thread.join if @web_thread
45
43
  end
46
44
 
47
- class << self
48
- attr_accessor :instance
45
+ def stop
46
+ @process_manager.halt
47
+ @zookeeper_observer.halt
48
+ @web_interface_runner.halt
49
49
  end
50
50
 
51
51
  end
@@ -0,0 +1,45 @@
1
+ module RedisRing
2
+
3
+ module BackgroundThread
4
+
5
+ def before_run
6
+ end
7
+
8
+ def after_halt
9
+ end
10
+
11
+ def do_work
12
+ end
13
+
14
+ def run
15
+ before_run
16
+
17
+ @continue_running = true
18
+
19
+ return Thread.new do
20
+ begin
21
+ while continue_running?
22
+ do_work
23
+ end
24
+ after_halt
25
+ rescue SystemExit
26
+ raise
27
+ rescue => e
28
+ puts "Error caught in #{self.class.name}:"
29
+ puts e
30
+ puts e.backtrace.join("\n")
31
+ end
32
+ end
33
+ end
34
+
35
+ def continue_running?
36
+ @continue_running
37
+ end
38
+
39
+ def halt
40
+ @continue_running = false
41
+ end
42
+
43
+ end
44
+
45
+ end
@@ -42,12 +42,9 @@ USAGE
42
42
  def start(config_file = nil)
43
43
  config = config_file ? Configuration.from_yml_file(config_file) : Configuration.new
44
44
 
45
- Application.instance = Application.new(config)
46
- Application.instance.start
47
-
48
- WebInterface.run!(:port => config.base_port)
49
-
50
- Application.instance.stop
45
+ app = Application.new(config)
46
+ app.start
47
+ app.wait
51
48
  end
52
49
 
53
50
  end
@@ -7,7 +7,7 @@ module RedisRing
7
7
  class Configuration
8
8
 
9
9
  PARAMETERS = [:host_name, :base_port, :ring_size, :redis_path, :redis_config_template_path,
10
- :total_vm_size, :base_directory, :password, :total_max_memory, :vm_page_size]
10
+ :total_vm_size, :base_directory, :password, :total_max_memory, :vm_page_size, :zookeeper_address]
11
11
 
12
12
  attr_reader *PARAMETERS
13
13
 
@@ -50,6 +50,7 @@ module RedisRing
50
50
  self.base_directory ||= "/var/lib/redis"
51
51
  self.total_max_memory ||= 1024 * 1024 * 1024 # 1GB
52
52
  self.vm_page_size ||= 32
53
+ self.zookeeper_address ||= "localhost:2181"
53
54
  end
54
55
 
55
56
  def validate!
@@ -0,0 +1,23 @@
1
+ module RedisRing
2
+
3
+ class HttpClient
4
+
5
+ def get(host, port, path, params = {})
6
+ Net::HTTP.get(uri(host, port, path, params))
7
+ end
8
+
9
+ def post(host, port, path, params = {})
10
+ Net::HTTP.post_form(uri(host, port, path, params), {}).body
11
+ end
12
+
13
+ protected
14
+
15
+ def uri(host, port, path, params)
16
+ params_str = params.map{|k,v| "#{k}=#{v}"}.join("&")
17
+ params_str = "?" + params_str unless params_str.empty?
18
+ URI.parse("http://#{host}:#{port}#{path}#{params_str}")
19
+ end
20
+
21
+ end
22
+
23
+ end
@@ -0,0 +1,154 @@
1
+ module RedisRing
2
+
3
+ class Master
4
+
5
+ attr_reader :zookeeper_connection, :ring_size, :node_provider
6
+
7
+ def initialize(zookeeper_connection, ring_size, node_provider)
8
+ @zookeeper_connection = zookeeper_connection
9
+ @ring_size = ring_size
10
+ @node_provider = node_provider
11
+ @node_ids = []
12
+ @is_master = false
13
+ end
14
+
15
+ def became_master
16
+ return if is_master?
17
+
18
+ puts "BECAME MASTER"
19
+
20
+ @is_master = true
21
+ end
22
+
23
+ def no_longer_is_master
24
+ return unless is_master?
25
+
26
+ puts "LOST MASTER STATUS"
27
+
28
+ @is_master = false
29
+ end
30
+
31
+ def nodes_changed(changed_node_ids)
32
+ return unless is_master?
33
+
34
+ new_nodes = changed_node_ids - node_ids
35
+ removed_nodes = node_ids - changed_node_ids
36
+
37
+ puts "NODES CHANGED"
38
+ puts "NEW: #{new_nodes.join(", ")}" if new_nodes.any?
39
+ puts "REMOVED: #{removed_nodes.join(', ')}" if removed_nodes.any?
40
+
41
+ @node_ids = changed_node_ids
42
+
43
+ reassign_shards
44
+ end
45
+
46
+ def node_joined(node_id)
47
+ puts "NODE JOINED #{node_id}"
48
+
49
+ reassign_shards
50
+ end
51
+
52
+ def node_leaving(node_id)
53
+ puts "NODE LEAVING #{node_id}"
54
+
55
+ node_ids.delete(node_id)
56
+ reassign_shards
57
+ end
58
+
59
+ def is_master?
60
+ return @is_master
61
+ end
62
+
63
+ def reassign_shards
64
+ update_node_statuses
65
+
66
+ running_shards = {}
67
+ best_candidates = {}
68
+ best_candidates_timestamps = Hash.new(0)
69
+
70
+ nodes.each do |node_id, node|
71
+ node.running_shards.dup.each do |shard_no|
72
+ if running_shards.key?(shard_no)
73
+ node.stop_shard(shard_no)
74
+ else
75
+ running_shards[shard_no] = node_id
76
+ end
77
+ end
78
+
79
+ node.available_shards.each do |shard_no, timestamp|
80
+ if timestamp > best_candidates_timestamps[shard_no]
81
+ best_candidates[shard_no] = node_id
82
+ best_candidates_timestamps[shard_no] = timestamp
83
+ end
84
+ end
85
+ end
86
+
87
+ offline_shards = (0...ring_size).to_a - running_shards.keys
88
+ shards_per_node = (1.0 * ring_size / nodes.size).floor
89
+ rest = ring_size - shards_per_node * nodes.size
90
+
91
+ nodes.each do |node_id, node|
92
+ next unless node.joined?
93
+ break if offline_shards.empty?
94
+ count_to_assign = shards_per_node - node.running_shards.size
95
+ count_to_assign += 1 if node_ids.index(node_id) < rest
96
+ count_to_assign.times do
97
+ shard_no = offline_shards.shift
98
+ break unless shard_no
99
+ node.start_shard(shard_no)
100
+ end
101
+ end
102
+
103
+ zookeeper_connection.update_status(status)
104
+ end
105
+
106
+ def status
107
+ {
108
+ :ring_size => ring_size,
109
+ :shards => shards
110
+ }
111
+ end
112
+
113
+ protected
114
+
115
+ attr_reader :node_ids
116
+ attr_accessor :nodes
117
+
118
+ def shards
119
+ running_shards = {}
120
+ nodes.each do |node_id, node|
121
+ node.running_shards.each do |shard_no|
122
+ running_shards[shard_no] = {
123
+ :host => node.host,
124
+ :port => node.port + shard_no + 1,
125
+ :status => :running
126
+ }
127
+ end
128
+ end
129
+ return running_shards
130
+ end
131
+
132
+ def update_node_statuses
133
+ self.nodes ||= {}
134
+
135
+ nodes.each do |node_id, node|
136
+ unless node_ids.include?(node_id)
137
+ nodes.delete(node_id)
138
+ end
139
+ end
140
+
141
+ node_ids.each do |node_id|
142
+ next if nodes.key?(node_id)
143
+ node_data = zookeeper_connection.node_data(node_id)
144
+ nodes[node_id] = node_provider.new(node_data["host"], node_data["port"])
145
+ end
146
+
147
+ nodes.each do |node_id, node|
148
+ node.update_status!
149
+ end
150
+ end
151
+
152
+ end
153
+
154
+ end
@@ -0,0 +1,33 @@
1
+ module RedisRing
2
+
3
+ class MasterRPC
4
+
5
+ attr_reader :http_client
6
+
7
+ def initialize(http_client)
8
+ @http_client = http_client
9
+ end
10
+
11
+ def connection(host, port)
12
+ Connection.new(http_client, host, port)
13
+ end
14
+
15
+ class Connection
16
+
17
+ attr_reader :http_client, :host, :port
18
+
19
+ def initialize(http_client, host, port)
20
+ @http_client = http_client
21
+ @host = host
22
+ @port = port
23
+ end
24
+
25
+ def node_loaded(node_id)
26
+ http_client.post(host, port, "/master/node_joined/#{node_id}")
27
+ end
28
+
29
+ end
30
+
31
+ end
32
+
33
+ end
@@ -0,0 +1,66 @@
1
+ module RedisRing
2
+
3
+ class NodeProvider
4
+
5
+ attr_reader :slave_rpc
6
+
7
+ def initialize(slave_rpc)
8
+ @slave_rpc = slave_rpc
9
+ end
10
+
11
+ def new(host, port)
12
+ Node.new(slave_rpc.connection(host, port), host, port)
13
+ end
14
+
15
+ end
16
+
17
+ class Node
18
+
19
+ attr_reader :slave_rpc, :host, :port
20
+
21
+ def initialize(slave_rpc, host, port)
22
+ @slave_rpc = slave_rpc
23
+ @host = host
24
+ @port = port
25
+ end
26
+
27
+ def update_status!
28
+ status_hash = slave_rpc.status
29
+ @joined = status_hash["joined"]
30
+ @running_shards = status_hash["running_shards"] || []
31
+ @available_shards = keys_to_i(status_hash["available_shards"] || {})
32
+ end
33
+
34
+ def joined?
35
+ @joined
36
+ end
37
+
38
+ def start_shard(shard_number)
39
+ running_shards << shard_number
40
+ slave_rpc.start_shard(shard_number)
41
+ end
42
+
43
+ def stop_shard(shard_number)
44
+ running_shards.delete(shard_number)
45
+ slave_rpc.stop_shard(shard_number)
46
+ end
47
+
48
+ def running_shards
49
+ @running_shards ||= []
50
+ end
51
+
52
+ def available_shards
53
+ @available_shards ||= {}
54
+ end
55
+
56
+ protected
57
+
58
+ def keys_to_i(hash)
59
+ result = {}
60
+ hash.each { |key, val| result[key.to_i] = val }
61
+ return result
62
+ end
63
+
64
+ end
65
+
66
+ end
@@ -4,49 +4,63 @@ module RedisRing
4
4
 
5
5
  class ProcessManager
6
6
 
7
+ include RedisRing::BackgroundThread
8
+
7
9
  def initialize
8
10
  @shards = {}
11
+ @shards_to_stop = []
12
+ @mutex = Mutex.new
9
13
  end
10
14
 
11
- def run
12
- @continue_running = true
13
- Thread.new do
14
- monitor_processes_loop
15
- end
15
+ def do_work
16
+ monitor_processes
17
+ sleep(0.5)
16
18
  end
17
19
 
18
- def halt
19
- @continue_running = false
20
+ def after_halt
21
+ shards.each do |shard_no, shard|
22
+ if shard.alive?
23
+ puts "Stopping shard #{shard_no}"
24
+ shard.stop
25
+ end
26
+ end
20
27
  end
21
28
 
22
29
  def start_shard(shard)
23
- if shards.key?(shard.shard_number)
24
- raise ShardAlreadyStarted.new("Shard: #{shard.shard_number} already started!")
25
- end
26
-
27
- shards[shard.shard_number] = shard
30
+ @mutex.synchronize do
31
+ if shards.key?(shard.shard_number)
32
+ raise ShardAlreadyStarted.new("Shard: #{shard.shard_number} already started!")
33
+ end
28
34
 
29
- shard.start
35
+ shards[shard.shard_number] = shard
36
+ end
30
37
  end
31
38
 
32
39
  def stop_shard(shard)
33
- shards.delete(shard.shard_number)
34
- shard.stop
40
+ @mutex.synchronize do
41
+ shards.delete(shard.shard_number)
42
+ shards_to_stop << shard
43
+ end
35
44
  end
36
45
 
37
46
  protected
38
47
 
39
- attr_reader :shards
48
+ attr_reader :shards, :shards_to_stop
49
+
50
+ def monitor_processes
51
+ @mutex.synchronize do
52
+ shards_to_stop.each do |shard|
53
+ puts "Stopping shard #{shard.shard_number}"
54
+ shard.stop
55
+ end
56
+ @shards_to_stop = []
40
57
 
41
- def monitor_processes_loop
42
- while(@continue_running) do
43
58
  shards.each do |shard_no, shard|
44
59
  unless shard.alive?
45
60
  puts "Restarting shard #{shard_no}"
46
61
  shard.start
47
62
  end
48
63
  end
49
- sleep(1)
50
64
  end
51
65
  end
52
66
 
@@ -5,6 +5,10 @@ module RedisRing
5
5
  attr_reader :shard_number, :configuration
6
6
 
7
7
  def initialize(shard_number, configuration)
8
+ unless shard_number >= 0 && shard_number < configuration.ring_size
9
+ raise ArgumentError.new("shard number #{shard_number} must be between 0 and #{configuration.ring_size - 1}")
10
+ end
11
+
8
12
  @shard_number = shard_number
9
13
  @configuration = configuration
10
14
  end
@@ -74,6 +78,14 @@ module RedisRing
74
78
  file('db_files', "shard-#{shard_number}.aof")
75
79
  end
76
80
 
81
+ def db_mtime
82
+ mtime(db_file_name)
83
+ end
84
+
85
+ def aof_mtime
86
+ mtime(aof_file_name)
87
+ end
88
+
77
89
  def password
78
90
  configuration.password
79
91
  end
@@ -88,6 +100,12 @@ module RedisRing
88
100
  File.join('..', '..', *parts)
89
101
  end
90
102
 
103
+ def mtime(relative_path)
104
+ path = File.expand_path(relative_path, working_directory)
105
+ return nil unless File.exist?(path)
106
+ return File.mtime(path).to_i
107
+ end
108
+
91
109
  end
92
110
 
93
111
  end
@@ -0,0 +1,62 @@
1
+ module RedisRing
2
+
3
+ class Slave
4
+
5
+ attr_accessor :current_master_host, :current_master_port, :node_id
6
+ attr_reader :configuration, :master_rpc, :process_manager
7
+ attr_reader :running_shards
8
+
9
+ def initialize(configuration, master_rpc, process_manager)
10
+ @configuration = configuration
11
+ @master_rpc = master_rpc
12
+ @process_manager = process_manager
13
+ @joined = false
14
+ @running_shards = {}
15
+ end
16
+
17
+ def joined?
18
+ @joined
19
+ end
20
+
21
+ def available_shards
22
+ available_shards = {}
23
+ configuration.ring_size.times do |shard_no|
24
+ shard_conf = ShardConfig.new(shard_no, configuration)
25
+ timestamp = [shard_conf.db_mtime, shard_conf.aof_mtime].compact.max
26
+ available_shards[shard_no] = timestamp if timestamp
27
+ end
28
+ return available_shards
29
+ end
30
+
31
+ def status
32
+ { :joined => joined?, :running_shards => running_shards.keys, :available_shards => available_shards }
33
+ end
34
+
35
+ def join
36
+ puts "JOINING CLUSTER"
37
+ @joined = true
38
+ master_rpc.connection(current_master_host, current_master_port).node_loaded(node_id)
39
+ end
40
+
41
+ def start_shard(shard_number)
42
+ puts "STARTING SHARD #{shard_number}"
43
+ return if running_shards.include?(shard_number)
44
+ shard_conf = ShardConfig.new(shard_number, configuration)
45
+ shard = running_shards[shard_number] = Shard.new(shard_conf)
46
+ process_manager.start_shard(shard)
47
+ end
48
+
49
+ def stop_shard(shard_number)
50
+ puts "STOPPING SHARD #{shard_number}"
51
+ shard = running_shards[shard_number]
52
+ return unless shard
53
+ process_manager.stop_shard(shard)
54
+ running_shards.delete(shard_number)
55
+ end
56
+
57
+ def sync_shard_with(shard_number, host, port)
58
+ end
59
+
60
+ end
61
+
62
+ end