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.
- data/MIT-LICENSE +22 -0
- data/Manifest +27 -0
- data/README.markdown +133 -0
- data/Rakefile +14 -0
- data/examples/client/client.rb +17 -0
- data/examples/client/constitution.txt +865 -0
- data/examples/client/pandemic_client.yml +3 -0
- data/examples/server/pandemic_server.yml +3 -0
- data/examples/server/word_count_server.rb +47 -0
- data/lib/pandemic.rb +42 -0
- data/lib/pandemic/client_side/cluster_connection.rb +194 -0
- data/lib/pandemic/client_side/config.rb +34 -0
- data/lib/pandemic/client_side/connection.rb +40 -0
- data/lib/pandemic/client_side/connection_proxy.rb +15 -0
- data/lib/pandemic/client_side/pandemize.rb +17 -0
- data/lib/pandemic/connection_pool.rb +194 -0
- data/lib/pandemic/mutex_counter.rb +50 -0
- data/lib/pandemic/requests_per_second.rb +31 -0
- data/lib/pandemic/server_side/client.rb +123 -0
- data/lib/pandemic/server_side/config.rb +74 -0
- data/lib/pandemic/server_side/handler.rb +31 -0
- data/lib/pandemic/server_side/peer.rb +211 -0
- data/lib/pandemic/server_side/processor.rb +90 -0
- data/lib/pandemic/server_side/request.rb +92 -0
- data/lib/pandemic/server_side/server.rb +285 -0
- data/lib/pandemic/util.rb +15 -0
- data/pandemic.gemspec +37 -0
- data/test/client_test.rb +87 -0
- data/test/connection_pool_test.rb +133 -0
- data/test/functional_test.rb +57 -0
- data/test/handler_test.rb +31 -0
- data/test/mutex_counter_test.rb +73 -0
- data/test/peer_test.rb +48 -0
- data/test/processor_test.rb +33 -0
- data/test/server_test.rb +171 -0
- data/test/test_helper.rb +24 -0
- data/test/util_test.rb +21 -0
- metadata +141 -0
@@ -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
|
data/lib/pandemic.rb
ADDED
@@ -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
|