pandemic 0.5.4

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.
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