pandemic 0.5.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. data/MIT-LICENSE +22 -0
  2. data/Manifest +27 -0
  3. data/README.markdown +133 -0
  4. data/Rakefile +14 -0
  5. data/examples/client/client.rb +17 -0
  6. data/examples/client/constitution.txt +865 -0
  7. data/examples/client/pandemic_client.yml +3 -0
  8. data/examples/server/pandemic_server.yml +3 -0
  9. data/examples/server/word_count_server.rb +47 -0
  10. data/lib/pandemic.rb +42 -0
  11. data/lib/pandemic/client_side/cluster_connection.rb +194 -0
  12. data/lib/pandemic/client_side/config.rb +34 -0
  13. data/lib/pandemic/client_side/connection.rb +40 -0
  14. data/lib/pandemic/client_side/connection_proxy.rb +15 -0
  15. data/lib/pandemic/client_side/pandemize.rb +17 -0
  16. data/lib/pandemic/connection_pool.rb +194 -0
  17. data/lib/pandemic/mutex_counter.rb +50 -0
  18. data/lib/pandemic/requests_per_second.rb +31 -0
  19. data/lib/pandemic/server_side/client.rb +123 -0
  20. data/lib/pandemic/server_side/config.rb +74 -0
  21. data/lib/pandemic/server_side/handler.rb +31 -0
  22. data/lib/pandemic/server_side/peer.rb +211 -0
  23. data/lib/pandemic/server_side/processor.rb +90 -0
  24. data/lib/pandemic/server_side/request.rb +92 -0
  25. data/lib/pandemic/server_side/server.rb +285 -0
  26. data/lib/pandemic/util.rb +15 -0
  27. data/pandemic.gemspec +37 -0
  28. data/test/client_test.rb +87 -0
  29. data/test/connection_pool_test.rb +133 -0
  30. data/test/functional_test.rb +57 -0
  31. data/test/handler_test.rb +31 -0
  32. data/test/mutex_counter_test.rb +73 -0
  33. data/test/peer_test.rb +48 -0
  34. data/test/processor_test.rb +33 -0
  35. data/test/server_test.rb +171 -0
  36. data/test/test_helper.rb +24 -0
  37. data/test/util_test.rb +21 -0
  38. metadata +141 -0
@@ -0,0 +1,3 @@
1
+ servers:
2
+ - localhost:4000
3
+ - localhost:4001
@@ -0,0 +1,3 @@
1
+ servers:
2
+ - localhost:4000
3
+ - localhost:4001
@@ -0,0 +1,47 @@
1
+ require 'rubygems'
2
+ require 'pandemic'
3
+ require 'json'
4
+
5
+ class WordCounter < Pandemic::ServerSide::Handler
6
+ def partition(request, servers)
7
+ # select only the alive servers (non-disconnected)
8
+ only_alive = servers.keys.select{|k| servers[k] != :disconnected}
9
+
10
+ mapping = {}
11
+ intervals = (request.body.size / only_alive.size.to_f).floor
12
+
13
+ pos = 0
14
+ only_alive.size.times do |i|
15
+ if i == only_alive.size - 1 # last peer gets the rest
16
+ mapping[only_alive[i]] = request.body[pos..-1]
17
+ else
18
+ next_pos = request.body[(pos + intervals)..-1].index(/ /) + pos + intervals
19
+ mapping[only_alive[i]] = request.body[pos...next_pos]
20
+ pos = next_pos
21
+ end
22
+ end
23
+ mapping
24
+ end
25
+
26
+ def process(text)
27
+ counts = Hash.new(0)
28
+ text.scan(/\w+/) do |word|
29
+ counts[word.strip.downcase] += 1
30
+ end
31
+ counts.to_json
32
+ end
33
+
34
+ def reduce(request)
35
+ total_counts = Hash.new(0)
36
+ request.responses.each do |counts|
37
+ JSON.parse(counts).each do |word, count|
38
+ total_counts[word] += count
39
+ end
40
+ end
41
+ total_counts.to_json
42
+ end
43
+ end
44
+
45
+ server = epidemic!
46
+ server.handler = WordCounter
47
+ server.start.join
@@ -0,0 +1,42 @@
1
+ require 'socket'
2
+ require 'fastthread' if RUBY_VERSION < '1.9'
3
+ require 'thread'
4
+ require 'monitor'
5
+ require 'yaml'
6
+ require 'digest/md5'
7
+ require 'logger'
8
+ require 'optparse'
9
+
10
+ require 'pandemic/util'
11
+ require 'pandemic/connection_pool'
12
+ require 'pandemic/mutex_counter'
13
+ require 'pandemic/requests_per_second'
14
+
15
+ require 'pandemic/server_side/config'
16
+ require 'pandemic/server_side/client'
17
+ require 'pandemic/server_side/server'
18
+ require 'pandemic/server_side/peer'
19
+ require 'pandemic/server_side/request'
20
+ require 'pandemic/server_side/handler'
21
+ require 'pandemic/server_side/processor'
22
+
23
+ require 'pandemic/client_side/config'
24
+ require 'pandemic/client_side/cluster_connection'
25
+ require 'pandemic/client_side/connection'
26
+ require 'pandemic/client_side/connection_proxy'
27
+ require 'pandemic/client_side/pandemize'
28
+
29
+ TCP_NO_DELAY_AVAILABLE =
30
+ RUBY_VERSION < '1.9' ? Socket.constants.include?('TCP_NODELAY') : Socket.constants.include?(:TCP_NODELAY)
31
+
32
+ MONITOR_TIMEOUT_AVAILABLE = (RUBY_VERSION < '1.9')
33
+ def epidemic!(options = {})
34
+ if $pandemic_logger.nil?
35
+ $pandemic_logger = Logger.new(options[:log_file] || "pandemic.log")
36
+ $pandemic_logger.level = options[:log_level] || Logger::INFO
37
+ $pandemic_logger.datetime_format = "%Y-%m-%d %H:%M:%S "
38
+ end
39
+ Pandemic::ServerSide::Server.boot(options[:bind_to])
40
+ end
41
+
42
+ ::Pandemize = Pandemic::ClientSide::Pandemize
@@ -0,0 +1,194 @@
1
+ module Pandemic
2
+ module ClientSide
3
+ class ClusterConnection
4
+ class NotEnoughConnectionsTimeout < StandardError; end
5
+ class NoNodesAvailable < StandardError; end
6
+ class LostConnectionToNode < StandardError; end
7
+ class NodeTimedOut < StandardError; end
8
+ class RequestFailed < StandardError; end
9
+
10
+ include Util
11
+ def initialize
12
+ Config.load
13
+ @connections = []
14
+ @available = []
15
+ @grouped_connections = Hash.new { |hash, key| hash[key] = [] }
16
+ @grouped_available = Hash.new { |hash, key| hash[key] = [] }
17
+ @mutex = Monitor.new
18
+ @connection_proxies = {}
19
+ @queue = @mutex.new_cond # TODO: there should be a queue for each group
20
+
21
+ @response_timeout = Config.response_timeout
22
+ @response_timeout = nil if @response_timeout <= 0
23
+
24
+ Config.servers.each_with_index do |server_addr, key|
25
+ @connection_proxies[key] = ConnectionProxy.new(key, self)
26
+ host, port = host_port(server_addr)
27
+ Config.min_connections_per_server.times do
28
+ add_connection_for_key(key)
29
+ end
30
+ end
31
+
32
+ maintain_minimum_connections!
33
+ end
34
+
35
+
36
+ def [](key)
37
+ @connection_proxies[key % @connection_proxies.size]
38
+ end
39
+
40
+ def request(body, key = nil, options = {})
41
+ key, options = nil, key if key.is_a?(Hash)
42
+ with_connection(key) do |socket|
43
+ begin
44
+ raise LostConnectionToNode if socket.nil?
45
+ flags = []
46
+ if options[:async]
47
+ flags << "a"
48
+ end
49
+ flags = flags.empty? ? "" : " #{flags.join("")}"
50
+
51
+ socket.write("#{body.size}#{flags}\n#{body}")
52
+ socket.flush
53
+
54
+ unless options[:async]
55
+ is_ready = IO.select([socket], nil, nil, @response_timeout)
56
+ raise NodeTimedOut if is_ready.nil?
57
+ response_size = socket.gets
58
+ if response_size && response_size.to_i >= 0
59
+ socket.read(response_size.to_i)
60
+ elsif response_size && response_size.to_i < 0
61
+ raise RequestFailed
62
+ else
63
+ # nil response size
64
+ raise LostConnectionToNode
65
+ end
66
+ end
67
+ rescue Errno::ECONNRESET, Errno::EPIPE
68
+ raise LostConnectionToNode
69
+ end
70
+ end
71
+ end
72
+
73
+ def shutdown
74
+ @connections.each {|c| c.socket.close if c.alive? }
75
+ @maintain_minimum_connections_thread.kill if @maintain_minimum_connections_thread
76
+ end
77
+
78
+ private
79
+ def with_connection(key, &block)
80
+ connection = nil
81
+ begin
82
+ connection = checkout_connection(key)
83
+ block.call(connection.socket)
84
+ rescue LostConnectionToNode
85
+ connection.died!
86
+ raise
87
+ ensure
88
+ checkin_connection(connection) if connection
89
+ end
90
+ end
91
+
92
+ def checkout_connection(key)
93
+ connection = nil
94
+ select_from = key.nil? ? @available : @grouped_available[key]
95
+ all_connections = key.nil? ? @connections : @grouped_connections[key]
96
+ @mutex.synchronize do
97
+ loop do
98
+ if select_from.size > 0
99
+ connection = select_from.pop
100
+ connection.ensure_alive!
101
+ if !connection.alive?
102
+ # it's dead
103
+ delete_connection(connection)
104
+ next
105
+ end
106
+
107
+ if key.nil?
108
+ @grouped_available[key].delete(connection)
109
+ else
110
+ @available.delete(connection)
111
+ end
112
+ break
113
+ elsif (connection = create_connection(key)) && connection.alive?
114
+ @connections << connection
115
+ @grouped_connections[key] << connection
116
+ break
117
+ elsif all_connections.size > 0 && @queue.wait(Config.connection_wait_timeout)
118
+ next
119
+ else
120
+ if all_connections.size > 0
121
+ raise NotEnoughConnectionsTimeout
122
+ else
123
+ raise NoNodesAvailable
124
+ end
125
+ end
126
+ end
127
+ end
128
+ return connection
129
+ end
130
+
131
+ def checkin_connection(connection)
132
+ @mutex.synchronize do
133
+ @available.unshift(connection)
134
+ @grouped_available[connection.key].unshift(connection)
135
+ @queue.signal
136
+ end
137
+ end
138
+
139
+ def create_connection(key)
140
+ if key.nil?
141
+ # find a key where we can add more connections
142
+ min, min_key = nil, nil
143
+ @grouped_connections.each do |key, list|
144
+ if min.nil? || list.size < min
145
+ min_key = key
146
+ min = list.size
147
+ end
148
+ end
149
+ key = min_key
150
+ end
151
+ return nil if @grouped_connections[key].size >= Config.max_connections_per_server
152
+ host, port = host_port(Config.servers[key])
153
+ Connection.new(host, port, key)
154
+ end
155
+
156
+ def add_connection_for_key(key)
157
+ connection = create_connection(key)
158
+ if connection.alive?
159
+ @connections << connection
160
+ @available << connection
161
+ @grouped_connections[key] << connection
162
+ @grouped_available[key] << connection
163
+ end
164
+ end
165
+
166
+ def delete_connection(connection)
167
+ @connections.delete(connection)
168
+ @available.delete(connection)
169
+ @grouped_connections[connection.key].delete(connection)
170
+ @grouped_available[connection.key].delete(connection)
171
+ end
172
+
173
+ def maintain_minimum_connections!
174
+ return if @maintain_minimum_connections_thread
175
+ @maintain_minimum_connections_thread = Thread.new do
176
+ loop do
177
+ sleep 60 #arbitrary
178
+ @mutex.synchronize do
179
+ @grouped_connections.keys.each do |key|
180
+ currently_exist = @grouped_connections[key].size
181
+ if currently_exist < Config.min_connections_per_server
182
+ (Config.min_connections_per_server - currently_exist).times do
183
+ add_connection_for_key(key)
184
+ end
185
+ end
186
+ end
187
+ end
188
+ end
189
+ end
190
+ end
191
+
192
+ end
193
+ end
194
+ end
@@ -0,0 +1,34 @@
1
+ module Pandemic
2
+ module ClientSide
3
+ class Config
4
+ class << self
5
+ @@load_mutex = Mutex.new
6
+ attr_accessor :config_path, :loaded
7
+ attr_accessor :servers, :max_connections_per_server, :min_connections_per_server,
8
+ :connection_wait_timeout, :response_timeout
9
+ def load
10
+ @@load_mutex.synchronize do
11
+ return if self.loaded
12
+ path = config_path
13
+ yaml = YAML.load_file(path)
14
+
15
+ @servers = yaml['servers'] || []
16
+ # this is just so if we copy/paste from server's yml to client's yml, it will still work
17
+ @servers = @servers.values if @servers.is_a?(Hash)
18
+ @servers.sort! # so it's consistent across all clients
19
+
20
+ @max_connections_per_server = (yaml['max_connections_per_server'] || 1).to_i
21
+ @min_connections_per_server = (yaml['min_connections_per_server'] || 1).to_i
22
+ @connection_wait_timeout = (yaml['connection_wait_timeout'] || 1).to_f
23
+ @response_timeout = (yaml['response_timeout'] || 1).to_f
24
+ self.loaded = true
25
+ end
26
+ end
27
+
28
+ def config_path
29
+ @config_path || "pandemic_client.yml"
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,40 @@
1
+ module Pandemic
2
+ module ClientSide
3
+ class Connection
4
+ attr_reader :key, :socket
5
+ def initialize(host, port, key)
6
+ @host, @port, @key = host, port, key
7
+ connect
8
+ end
9
+
10
+ def alive?
11
+ @socket && !@socket.closed?
12
+ end
13
+
14
+ def ensure_alive!
15
+ connect unless self.alive?
16
+ end
17
+
18
+ def died!
19
+ @socket.close if self.alive?
20
+ @socket = nil
21
+ end
22
+
23
+ private
24
+ def connect
25
+ @socket = begin
26
+ connection = TCPSocket.new(@host, @port)
27
+ if connection && !connection.closed?
28
+ connection.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1) if TCP_NO_DELAY_AVAILABLE
29
+ connection.write("CLIENT\n")
30
+ connection
31
+ else
32
+ nil
33
+ end
34
+ rescue Errno::ETIMEDOUT, Errno::ECONNREFUSED
35
+ nil
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,15 @@
1
+ module Pandemic
2
+ module ClientSide
3
+ class ConnectionProxy
4
+ instance_methods.each {|m| undef_method(m) if m !~ /^__/ && m !~ /object_id/ }
5
+
6
+ def initialize(key, cluster)
7
+ @key, @cluster = key, cluster
8
+ end
9
+
10
+ def request(body, options = {})
11
+ @cluster.request(body, @key, options)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,17 @@
1
+ module Pandemic
2
+ module ClientSide
3
+ module Pandemize
4
+ def self.included(klass)
5
+ klass.class_eval do
6
+ @pandemize_connection ||= Pandemic::ClientSide::ClusterConnection.new
7
+ def self.pandemize_connection
8
+ @pandemize_connection
9
+ end
10
+ end
11
+ end
12
+ def pandemic
13
+ self.class.pandemize_connection
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,194 @@
1
+ module Pandemic
2
+ class ConnectionPool
3
+ class TimedOutWaitingForConnectionException < StandardError; end
4
+ class CreateConnectionUndefinedException < StandardError; end
5
+ include Util
6
+ def initialize(options = {})
7
+ @connected = false
8
+ @mutex = Monitor.new
9
+ @queue = @mutex.new_cond
10
+ @available = []
11
+ @connections = []
12
+ @max_connections = options[:max_connections] || 10
13
+ @min_connections = options[:min_connections] || 1
14
+ @connect_at_define = options.include?(:connect_at_define) ? options[:connect_at_define] : true
15
+ @timeout = MONITOR_TIMEOUT_AVAILABLE ? options[:timeout] || 3 : nil
16
+ end
17
+
18
+ def add_connection!
19
+ # bang because we're ignoring the max connections
20
+ @mutex.synchronize do
21
+ conn = create_connection
22
+ @available << conn if conn && !conn.closed?
23
+ end
24
+ end
25
+
26
+ def create_connection(&block)
27
+ if block.nil?
28
+ if @create_connection
29
+ conn = @create_connection.call
30
+ if conn && !conn.closed?
31
+ @connections << conn
32
+ @connected = true
33
+ conn
34
+ end
35
+ else
36
+ raise CreateConnectionUndefinedException.new("You must specify a block to create connections")
37
+ end
38
+ else
39
+ @create_connection = block
40
+ connect if @connect_at_define
41
+ end
42
+ end
43
+
44
+ def destroy_connection(connection = nil, &block)
45
+ if block.nil?
46
+ if @destroy_connection
47
+ @destroy_connection.call(connection)
48
+ else
49
+ if connection && !connection.closed?
50
+ # defaul behavior is this
51
+ connection.close
52
+ end
53
+ end
54
+ @connections.delete(connection)
55
+ @available.delete(connection)
56
+ # this is within the mutex of the caller
57
+ @connected = false if @connections.empty?
58
+ else
59
+ @destroy_connection = block
60
+ end
61
+ end
62
+
63
+ def status_check(connection = nil, &block)
64
+ if block.nil?
65
+ if @status_check
66
+ @status_check.call(connection)
67
+ else
68
+ connection && !connection.closed?
69
+ end
70
+ else
71
+ @status_check = block
72
+ end
73
+ end
74
+
75
+ def connected?
76
+ @connected
77
+ end
78
+
79
+ def connect
80
+ if !connected?
81
+ @min_connections.times { add_connection! }
82
+ grim_reaper
83
+ end
84
+ end
85
+
86
+ def available_count
87
+ @available.size
88
+ end
89
+
90
+ def connections_count
91
+ @connections.size
92
+ end
93
+
94
+ def disconnect
95
+ @mutex.synchronize do
96
+ return if @disconnecting
97
+ @disconnecting = true
98
+ @connected = false # we don't want anyone thinking they can use this connection
99
+ @grim_reaper.kill if @grim_reaper && @grim_reaper.alive?
100
+
101
+ @available.dup.each do |conn|
102
+ destroy_connection(conn)
103
+ end
104
+
105
+ while @connections.size > 0 && @queue.wait
106
+ @available.dup.each do |conn|
107
+ destroy_connection(conn)
108
+ end
109
+ end
110
+ @disconnecting = false
111
+ end
112
+ end
113
+
114
+ def with_connection(&block)
115
+ connection = nil
116
+ begin
117
+ connection = checkout
118
+ block.call(connection)
119
+ ensure
120
+ checkin(connection) if connection
121
+ end
122
+ end
123
+
124
+ private
125
+
126
+ def checkout
127
+ connection = nil
128
+ @mutex.synchronize do
129
+ loop do
130
+ if @available.size > 0
131
+ connection = @available.pop
132
+ break
133
+ elsif @connections.size < @max_connections && (connection = create_connection)
134
+ break
135
+ elsif @queue.wait(@timeout)
136
+ next
137
+ else
138
+ raise TimedOutWaitingForConnectionException
139
+ end
140
+ end
141
+ end
142
+ return connection
143
+ end
144
+
145
+ def checkin(connection)
146
+ @mutex.synchronize do
147
+ @available.unshift(connection)
148
+ @queue.signal
149
+ end
150
+ end
151
+
152
+ def grim_reaper
153
+ @grim_reaper.kill if @grim_reaper && @grim_reaper.alive?
154
+ @grim_reaper = Thread.new do
155
+ usage_history = []
156
+ loop do
157
+ if connected?
158
+ @mutex.synchronize do
159
+ dead = []
160
+
161
+ @connections.each do |conn|
162
+ dead << conn if !status_check(conn)
163
+ end
164
+
165
+ dead.each { |c| destroy_connection(c) }
166
+
167
+ # restore to minimum number of connections if it's too low
168
+ [@min_connections - @connections.size, 0].max.times do
169
+ add_connection!
170
+ end
171
+
172
+ usage_history.push(@available.size)
173
+ if usage_history.size >= 10
174
+ # kill the minimum number of available connections over the last 10 checks
175
+ # or the total connections minux the min connections, whichever is lower.
176
+ # this ensures that you never go below min connections
177
+ to_kill = [usage_history.min, @connections.size - @min_connections].min
178
+ [to_kill, 0].max.times do
179
+ destroy_connection(@connections.last)
180
+ end
181
+ usage_history = []
182
+ end
183
+
184
+ end # end mutex
185
+ sleep 30
186
+ else
187
+ break
188
+ end # end if connected
189
+ end
190
+ end
191
+ end
192
+
193
+ end
194
+ end