dat-tcp 0.1.1 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/dat-tcp.gemspec +8 -10
- data/lib/dat-tcp.rb +196 -106
- data/lib/dat-tcp/version.rb +1 -1
- data/lib/dat-tcp/worker_pool.rb +213 -0
- metadata +17 -15
- data/lib/dat-tcp/workers.rb +0 -99
data/dat-tcp.gemspec
CHANGED
@@ -6,20 +6,18 @@ require 'dat-tcp/version'
|
|
6
6
|
Gem::Specification.new do |gem|
|
7
7
|
gem.name = "dat-tcp"
|
8
8
|
gem.version = DatTCP::VERSION
|
9
|
-
gem.authors = ["Collin Redding"]
|
10
|
-
gem.email = ["collin.redding@me.com"]
|
11
|
-
gem.description = "
|
12
|
-
"
|
13
|
-
|
14
|
-
gem.
|
15
|
-
"servers."
|
16
|
-
gem.homepage = ""
|
9
|
+
gem.authors = ["Collin Redding", "Kelly Redding"]
|
10
|
+
gem.email = ["collin.redding@me.com", "kelly@kellyredding.com"]
|
11
|
+
gem.description = "A generic threaded TCP server API. It is"\
|
12
|
+
" designed for use as a base for application servers."
|
13
|
+
gem.summary = "A generic threaded TCP server API"
|
14
|
+
gem.homepage = "https://github.com/redding/dat-tcp"
|
17
15
|
|
18
16
|
gem.files = `git ls-files -- lib/* Gemfile Rakefile *.gemspec`.split($/)
|
19
17
|
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
20
18
|
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
21
19
|
gem.require_paths = ["lib"]
|
22
20
|
|
23
|
-
gem.add_development_dependency('assert', ['~>0
|
24
|
-
gem.add_development_dependency('assert-mocha', ['~>0
|
21
|
+
gem.add_development_dependency('assert', ['~> 1.0'])
|
22
|
+
gem.add_development_dependency('assert-mocha', ['~> 1.0'])
|
25
23
|
end
|
data/lib/dat-tcp.rb
CHANGED
@@ -1,158 +1,248 @@
|
|
1
|
-
|
2
|
-
# should be mixed in and provides methods for starting and stopping the main
|
3
|
-
# server loop. The `serve` method is intended to be overwritten so users can
|
4
|
-
# define handling connections. It's primary loop is:
|
5
|
-
#
|
6
|
-
# 1. Wait for worker
|
7
|
-
# 1. Accept connection
|
8
|
-
# 2. Process connection by handing off to worker
|
9
|
-
#
|
10
|
-
# This is repeated until the server is stopped.
|
11
|
-
#
|
12
|
-
# Options:
|
13
|
-
# `max_workers` - (integer) The maximum number of workers for processing
|
14
|
-
# connections. More threads causes more concurrency but also
|
15
|
-
# more overhead. This defaults to 4 workers.
|
16
|
-
# `debug` - (boolean) Whether or not to have the server log debug
|
17
|
-
# messages for when the server starts and stops and when a
|
18
|
-
# client connects and disconnects.
|
19
|
-
# `ready_timeout` - (float) The timeout used with `IO.select` waiting for a
|
20
|
-
# connection to occur. This can be set to 0 to not wait at
|
21
|
-
# all. Defaults to 1 (second).
|
22
|
-
#
|
23
|
-
require 'logger'
|
1
|
+
require 'ostruct'
|
24
2
|
require 'socket'
|
25
3
|
require 'thread'
|
26
4
|
|
27
5
|
module DatTCP; end
|
28
6
|
|
29
7
|
require 'dat-tcp/logger'
|
30
|
-
require 'dat-tcp/
|
8
|
+
require 'dat-tcp/worker_pool'
|
31
9
|
require 'dat-tcp/version'
|
32
10
|
|
33
11
|
module DatTCP
|
34
12
|
|
35
13
|
module Server
|
36
|
-
attr_reader :host, :port, :workers, :debug, :logger, :ready_timeout
|
37
|
-
attr_reader :tcp_server, :thread
|
38
14
|
|
39
|
-
|
40
|
-
options ||= {}
|
41
|
-
options[:max_workers] ||= 4
|
15
|
+
attr_reader :logger
|
42
16
|
|
43
|
-
|
44
|
-
|
45
|
-
@
|
46
|
-
@
|
17
|
+
def initialize(config = nil)
|
18
|
+
config = OpenStruct.new(config || {})
|
19
|
+
@backlog_size = config.backlog_size || 1024
|
20
|
+
@debug = config.debug || false
|
21
|
+
@min_workers = config.min_workers || 2
|
22
|
+
@max_workers = config.max_workers || 4
|
23
|
+
@ready_timeout = config.ready_timeout || 1
|
47
24
|
|
48
|
-
@
|
49
|
-
|
25
|
+
@logger = DatTCP::Logger.new(@debug)
|
26
|
+
|
27
|
+
@tcp_server = nil
|
28
|
+
@work_loop_thread = nil
|
29
|
+
@worker_pool = nil
|
30
|
+
set_state :stop
|
50
31
|
end
|
51
32
|
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
33
|
+
# Socket Options:
|
34
|
+
# * SOL_SOCKET - specifies the protocol layer the option applies to.
|
35
|
+
# SOL_SOCKET is basic socket options (as opposed to
|
36
|
+
# something like IPPROTO_TCP for TCP socket options).
|
37
|
+
# * SO_REUSEADDR - indicates that the rules used in validating addresses
|
38
|
+
# supplied in a bind(2) call should allow reuse of local
|
39
|
+
# addresses. This will allow us to re-bind to a port if we
|
40
|
+
# were shutdown and started right away. This will still
|
41
|
+
# throw an "address in use" if a socket is active on the
|
42
|
+
# port.
|
43
|
+
|
44
|
+
def listen(*args)
|
45
|
+
build_server_method = if args.size == 2
|
46
|
+
:new
|
47
|
+
elsif args.size == 1
|
48
|
+
:for_fd
|
56
49
|
else
|
57
|
-
|
50
|
+
raise InvalidListenArgsError.new
|
58
51
|
end
|
52
|
+
set_state :listen
|
53
|
+
run_hook 'on_listen'
|
54
|
+
@tcp_server = TCPServer.send(build_server_method, *args)
|
55
|
+
|
56
|
+
@tcp_server.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, true)
|
57
|
+
run_hook 'configure_tcp_server', @tcp_server
|
58
|
+
|
59
|
+
@tcp_server.listen(@backlog_size)
|
59
60
|
end
|
60
61
|
|
61
|
-
def
|
62
|
-
if self.
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
62
|
+
def run(client_file_descriptors = nil)
|
63
|
+
raise NotListeningError.new if !self.listening?
|
64
|
+
set_state :run
|
65
|
+
run_hook 'on_run'
|
66
|
+
@work_loop_thread = Thread.new{ work_loop(client_file_descriptors) }
|
67
|
+
end
|
68
|
+
|
69
|
+
def pause(wait = true)
|
70
|
+
set_state :pause
|
71
|
+
run_hook 'on_pause'
|
72
|
+
wait_for_shutdown if wait
|
73
|
+
end
|
74
|
+
|
75
|
+
def stop(wait = true)
|
76
|
+
set_state :stop
|
77
|
+
run_hook 'on_stop'
|
78
|
+
wait_for_shutdown if wait
|
79
|
+
end
|
80
|
+
|
81
|
+
def halt(wait = true)
|
82
|
+
set_state :halt
|
83
|
+
run_hook 'on_halt'
|
84
|
+
wait_for_shutdown if wait
|
85
|
+
end
|
86
|
+
|
87
|
+
def stop_listening
|
88
|
+
@tcp_server.close rescue false
|
89
|
+
@tcp_server = nil
|
90
|
+
end
|
91
|
+
|
92
|
+
def ip
|
93
|
+
@tcp_server.addr[2] if self.listening?
|
94
|
+
end
|
95
|
+
|
96
|
+
def port
|
97
|
+
@tcp_server.addr[1] if self.listening?
|
98
|
+
end
|
99
|
+
|
100
|
+
def file_descriptor
|
101
|
+
@tcp_server.fileno if self.listening?
|
102
|
+
end
|
103
|
+
|
104
|
+
def client_file_descriptors
|
105
|
+
@worker_pool ? @worker_pool.connections.map(&:fileno) : []
|
72
106
|
end
|
73
107
|
|
74
|
-
def
|
75
|
-
|
108
|
+
def listening?
|
109
|
+
!!@tcp_server
|
76
110
|
end
|
77
111
|
|
78
112
|
def running?
|
79
|
-
!!@
|
113
|
+
!!@work_loop_thread
|
80
114
|
end
|
81
115
|
|
82
116
|
# This method should be overwritten to handle new connections
|
83
117
|
def serve(socket)
|
84
118
|
end
|
85
119
|
|
86
|
-
|
87
|
-
|
120
|
+
# Hooks
|
121
|
+
|
122
|
+
def on_listen
|
123
|
+
end
|
124
|
+
|
125
|
+
def configure_tcp_server(tcp_server)
|
126
|
+
end
|
127
|
+
|
128
|
+
def on_run
|
129
|
+
end
|
130
|
+
|
131
|
+
def on_pause
|
132
|
+
end
|
133
|
+
|
134
|
+
def on_stop
|
135
|
+
end
|
136
|
+
|
137
|
+
def on_halt
|
88
138
|
end
|
89
139
|
|
90
140
|
def inspect
|
91
141
|
reference = '0x0%x' % (self.object_id << 1)
|
92
|
-
"#<#{self.class}:#{reference}
|
142
|
+
"#<#{self.class}:#{reference}".tap do |inspect_str|
|
143
|
+
inspect_str << " @state=#{@state.inspect}"
|
144
|
+
if self.listening?
|
145
|
+
port, ip = @tcp_server.addr[1, 2]
|
146
|
+
inspect_str << " @ip=#{ip.inspect} @port=#{port.inspect}"
|
147
|
+
end
|
148
|
+
inspect_str << " @work_loop_status=#{@work_loop_thread.status.inspect}" if self.running?
|
149
|
+
inspect_str << ">"
|
150
|
+
end
|
93
151
|
end
|
94
152
|
|
95
153
|
protected
|
96
154
|
|
97
|
-
def
|
98
|
-
|
99
|
-
@
|
100
|
-
|
155
|
+
def work_loop(client_file_descriptors = nil)
|
156
|
+
self.logger.info "Starting work loop..."
|
157
|
+
pool_args = [ @min_workers, @max_workers, @debug ]
|
158
|
+
@worker_pool = DatTCP::WorkerPool.new(*pool_args){|socket| serve(socket) }
|
159
|
+
self.enqueue_file_descriptors(client_file_descriptors || [])
|
160
|
+
while @state.run?
|
161
|
+
@worker_pool.enqueue_connection self.accept_connection
|
101
162
|
end
|
163
|
+
self.logger.info "Stopping work loop..."
|
164
|
+
shutdown_worker_pool if !@state.halt?
|
165
|
+
rescue Exception => exception
|
166
|
+
self.logger.error "Exception occurred, stopping server!"
|
167
|
+
self.logger.error "#{exception.class}: #{exception.message}"
|
168
|
+
self.logger.error exception.backtrace.join("\n")
|
169
|
+
ensure
|
170
|
+
close_connection if !@state.pause?
|
171
|
+
clear_thread
|
172
|
+
self.logger.info "Stopped work loop"
|
102
173
|
end
|
103
174
|
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
def work_loop
|
108
|
-
self.logger.info("Starting...")
|
109
|
-
while !@shutdown
|
110
|
-
connection = self.accept_connection
|
111
|
-
self.workers.process(connection){|client| self.serve(client) } if connection
|
175
|
+
def enqueue_file_descriptors(file_descriptors)
|
176
|
+
file_descriptors.each do |file_descriptor|
|
177
|
+
@worker_pool.enqueue_connection TCPSocket.for_fd(file_descriptor)
|
112
178
|
end
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
# This method is a accept-loop waiting for a new connection. It uses
|
120
|
-
# `IO.select` with a timeout to wait for a socket to be ready. Once the
|
121
|
-
# socket is ready, it calls `accept` and returns the connection. If the
|
122
|
-
# server socket doesn't have a new connection waiting, the loop starts over.
|
123
|
-
# In the case the server has been shutdown, it will also break out of the
|
124
|
-
# loop.
|
125
|
-
#
|
126
|
-
# Notes:
|
127
|
-
# * If the server has been shutdown this will return `nil`.
|
179
|
+
end
|
180
|
+
|
181
|
+
# An accept-loop waiting for new connections. Will wait for a connection
|
182
|
+
# (up to `ready_timeout`) and accept it. `IO.select` with the timeout
|
183
|
+
# allows the server to be responsive to shutdowns.
|
128
184
|
def accept_connection
|
129
|
-
|
130
|
-
|
131
|
-
return @tcp_server.accept
|
132
|
-
elsif @shutdown
|
133
|
-
return
|
134
|
-
end
|
185
|
+
while @state.run?
|
186
|
+
return @tcp_server.accept if self.connection_ready?
|
135
187
|
end
|
136
188
|
end
|
137
189
|
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
190
|
+
def connection_ready?
|
191
|
+
!!IO.select([ @tcp_server ], nil, nil, @ready_timeout)
|
192
|
+
end
|
193
|
+
|
194
|
+
def shutdown_worker_pool
|
195
|
+
self.logger.info "Shutting down worker pool, letting it finish..."
|
196
|
+
@worker_pool.shutdown
|
197
|
+
end
|
198
|
+
|
199
|
+
def close_connection
|
200
|
+
self.logger.info "Closing TCP server connection..."
|
201
|
+
self.stop_listening
|
202
|
+
end
|
203
|
+
|
204
|
+
def clear_thread
|
205
|
+
@work_loop_thread = nil
|
206
|
+
end
|
207
|
+
|
208
|
+
def wait_for_shutdown
|
209
|
+
@work_loop_thread.join if @work_loop_thread
|
210
|
+
end
|
211
|
+
|
212
|
+
def run_hook(method, *args)
|
213
|
+
self.send(method, *args)
|
214
|
+
end
|
215
|
+
|
216
|
+
def set_state(name)
|
217
|
+
@state = State.new(name)
|
218
|
+
end
|
219
|
+
|
220
|
+
class State < String
|
221
|
+
|
222
|
+
def initialize(value)
|
223
|
+
super value.to_s
|
150
224
|
end
|
151
|
-
|
152
|
-
|
153
|
-
|
225
|
+
|
226
|
+
[ :listen, :run, :stop, :halt, :pause ].each do |name|
|
227
|
+
define_method("#{name}?"){ self.to_sym == name }
|
154
228
|
end
|
155
|
-
|
229
|
+
|
230
|
+
end
|
231
|
+
|
232
|
+
end
|
233
|
+
|
234
|
+
class InvalidListenArgsError < ArgumentError
|
235
|
+
|
236
|
+
def initialize
|
237
|
+
super "invalid arguments, must be either a file descriptor or an ip and port"
|
238
|
+
end
|
239
|
+
|
240
|
+
end
|
241
|
+
|
242
|
+
class NotListeningError < RuntimeError
|
243
|
+
|
244
|
+
def initialize
|
245
|
+
super "`listen` must be called before calling `run`"
|
156
246
|
end
|
157
247
|
|
158
248
|
end
|
data/lib/dat-tcp/version.rb
CHANGED
@@ -0,0 +1,213 @@
|
|
1
|
+
require 'thread'
|
2
|
+
|
3
|
+
require 'dat-tcp/logger'
|
4
|
+
|
5
|
+
module DatTCP
|
6
|
+
|
7
|
+
class WorkerPool
|
8
|
+
attr_reader :logger, :spawned
|
9
|
+
|
10
|
+
def initialize(min = 0, max = 1, debug = false, &serve_proc)
|
11
|
+
@min_workers = min
|
12
|
+
@max_workers = max
|
13
|
+
@logger = DatTCP::Logger.new(debug)
|
14
|
+
@serve_proc = serve_proc
|
15
|
+
|
16
|
+
@queue = DatTCP::Queue.new
|
17
|
+
@workers_waiting = DatTCP::WorkersWaiting.new
|
18
|
+
|
19
|
+
@mutex = Mutex.new
|
20
|
+
@workers = []
|
21
|
+
@spawned = 0
|
22
|
+
|
23
|
+
@min_workers.times{ self.spawn_worker }
|
24
|
+
end
|
25
|
+
|
26
|
+
def connections
|
27
|
+
@queue.items
|
28
|
+
end
|
29
|
+
|
30
|
+
def waiting
|
31
|
+
@workers_waiting.count
|
32
|
+
end
|
33
|
+
|
34
|
+
# Check if all workers are busy before adding the connection. When the
|
35
|
+
# connection is added, a worker will stop waiting (if it was idle). Because
|
36
|
+
# of that, we can't reliably check if all workers are busy. We might think
|
37
|
+
# all workers are busy because we just woke up a sleeping worker to serve
|
38
|
+
# this connection. Then we would spawn a worker to do nothing.
|
39
|
+
def enqueue_connection(socket)
|
40
|
+
return if !socket
|
41
|
+
new_worker_needed = all_workers_are_busy?
|
42
|
+
@queue.push socket
|
43
|
+
self.spawn_worker if new_worker_needed && havent_reached_max_workers?
|
44
|
+
end
|
45
|
+
|
46
|
+
# Shutdown each worker and then the queue. Shutting down the queue will
|
47
|
+
# signal any workers waiting on it to wake up, so they can start shutting
|
48
|
+
# down. If a worker is processing a connection, then it will be joined and
|
49
|
+
# allowed to finish.
|
50
|
+
# **NOTE** Any connections that are on the queue are not served.
|
51
|
+
def shutdown
|
52
|
+
@workers.each(&:shutdown)
|
53
|
+
@queue.shutdown
|
54
|
+
|
55
|
+
# use this pattern instead of `each` -- we don't want to call `join` on
|
56
|
+
# every worker (especially if they are shutting down on their own), we
|
57
|
+
# just want to make sure that any who haven't had a chance to finish
|
58
|
+
# get to (this is safe, otherwise you might get a dead thread in the
|
59
|
+
# `each`).
|
60
|
+
@workers.first.join until @workers.empty?
|
61
|
+
end
|
62
|
+
|
63
|
+
# public, because workers need to call it for themselves
|
64
|
+
def despawn_worker(worker)
|
65
|
+
@mutex.synchronize do
|
66
|
+
@spawned -= 1
|
67
|
+
@workers.delete worker
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
protected
|
72
|
+
|
73
|
+
def spawn_worker
|
74
|
+
@mutex.synchronize do
|
75
|
+
worker = DatTCP::Worker.new(self, @queue, @workers_waiting) do |socket|
|
76
|
+
self.serve_socket(socket)
|
77
|
+
end
|
78
|
+
@workers << worker
|
79
|
+
@spawned += 1
|
80
|
+
worker
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def serve_socket(socket)
|
85
|
+
begin
|
86
|
+
@serve_proc.call(socket)
|
87
|
+
rescue Exception => exception
|
88
|
+
self.logger.error "Exception raised while serving connection!"
|
89
|
+
self.logger.error "#{exception.class}: #{exception.message}"
|
90
|
+
self.logger.error exception.backtrace.join("\n")
|
91
|
+
ensure
|
92
|
+
socket.close rescue false
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def all_workers_are_busy?
|
97
|
+
@workers_waiting.count <= 0
|
98
|
+
end
|
99
|
+
|
100
|
+
def havent_reached_max_workers?
|
101
|
+
@mutex.synchronize do
|
102
|
+
@spawned < @max_workers
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
end
|
107
|
+
|
108
|
+
class Queue
|
109
|
+
|
110
|
+
def initialize
|
111
|
+
@items = []
|
112
|
+
@shutdown = false
|
113
|
+
@mutex = Mutex.new
|
114
|
+
@condition_variable = ConditionVariable.new
|
115
|
+
end
|
116
|
+
|
117
|
+
def items
|
118
|
+
@mutex.synchronize{ @items }
|
119
|
+
end
|
120
|
+
|
121
|
+
# Add the connection and wake up the first worker (the `signal`) that's
|
122
|
+
# waiting (because of `wait_for_new_connection`)
|
123
|
+
def push(socket)
|
124
|
+
raise "Unable to add connection while shutting down" if @shutdown
|
125
|
+
@mutex.synchronize do
|
126
|
+
@items << socket
|
127
|
+
@condition_variable.signal
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def pop
|
132
|
+
@mutex.synchronize{ @items.pop }
|
133
|
+
end
|
134
|
+
|
135
|
+
def empty?
|
136
|
+
@mutex.synchronize{ @items.empty? }
|
137
|
+
end
|
138
|
+
|
139
|
+
# wait to be signaled by `push`
|
140
|
+
def wait_for_new_connection
|
141
|
+
@mutex.synchronize{ @condition_variable.wait(@mutex) }
|
142
|
+
end
|
143
|
+
|
144
|
+
# wake up any workers who are idle (because of `wait_for_new_connection`)
|
145
|
+
def shutdown
|
146
|
+
@shutdown = true
|
147
|
+
@mutex.synchronize{ @condition_variable.broadcast }
|
148
|
+
end
|
149
|
+
|
150
|
+
end
|
151
|
+
|
152
|
+
class Worker
|
153
|
+
|
154
|
+
def initialize(pool, queue, workers_waiting, &block)
|
155
|
+
@pool = pool
|
156
|
+
@queue = queue
|
157
|
+
@workers_waiting = workers_waiting
|
158
|
+
@block = block
|
159
|
+
@shutdown = false
|
160
|
+
@thread = Thread.new{ work_loop }
|
161
|
+
end
|
162
|
+
|
163
|
+
def shutdown
|
164
|
+
@shutdown = true
|
165
|
+
end
|
166
|
+
|
167
|
+
def join
|
168
|
+
@thread.join if @thread
|
169
|
+
end
|
170
|
+
|
171
|
+
protected
|
172
|
+
|
173
|
+
def work_loop
|
174
|
+
loop do
|
175
|
+
self.wait_for_work
|
176
|
+
break if @shutdown
|
177
|
+
@block.call @queue.pop
|
178
|
+
end
|
179
|
+
ensure
|
180
|
+
@pool.despawn_worker(self)
|
181
|
+
end
|
182
|
+
|
183
|
+
# Wait for a connection to serve by checking if the queue is empty.
|
184
|
+
def wait_for_work
|
185
|
+
while @queue.empty?
|
186
|
+
return if @shutdown
|
187
|
+
@workers_waiting.increment
|
188
|
+
@queue.wait_for_new_connection
|
189
|
+
@workers_waiting.decrement
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
end
|
194
|
+
|
195
|
+
class WorkersWaiting
|
196
|
+
attr_reader :count
|
197
|
+
|
198
|
+
def initialize
|
199
|
+
@mutex = Mutex.new
|
200
|
+
@count = 0
|
201
|
+
end
|
202
|
+
|
203
|
+
def increment
|
204
|
+
@mutex.synchronize{ @count += 1 }
|
205
|
+
end
|
206
|
+
|
207
|
+
def decrement
|
208
|
+
@mutex.synchronize{ @count -= 1 }
|
209
|
+
end
|
210
|
+
|
211
|
+
end
|
212
|
+
|
213
|
+
end
|
metadata
CHANGED
@@ -1,21 +1,22 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: dat-tcp
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 23
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 0
|
8
|
-
-
|
9
|
-
-
|
10
|
-
version: 0.
|
8
|
+
- 2
|
9
|
+
- 0
|
10
|
+
version: 0.2.0
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Collin Redding
|
14
|
+
- Kelly Redding
|
14
15
|
autorequire:
|
15
16
|
bindir: bin
|
16
17
|
cert_chain: []
|
17
18
|
|
18
|
-
date:
|
19
|
+
date: 2013-02-16 00:00:00 Z
|
19
20
|
dependencies:
|
20
21
|
- !ruby/object:Gem::Dependency
|
21
22
|
prerelease: false
|
@@ -24,11 +25,11 @@ dependencies:
|
|
24
25
|
requirements:
|
25
26
|
- - ~>
|
26
27
|
- !ruby/object:Gem::Version
|
27
|
-
hash:
|
28
|
+
hash: 15
|
28
29
|
segments:
|
30
|
+
- 1
|
29
31
|
- 0
|
30
|
-
|
31
|
-
version: "0.8"
|
32
|
+
version: "1.0"
|
32
33
|
requirement: *id001
|
33
34
|
name: assert
|
34
35
|
type: :development
|
@@ -39,17 +40,18 @@ dependencies:
|
|
39
40
|
requirements:
|
40
41
|
- - ~>
|
41
42
|
- !ruby/object:Gem::Version
|
42
|
-
hash:
|
43
|
+
hash: 15
|
43
44
|
segments:
|
44
|
-
- 0
|
45
45
|
- 1
|
46
|
-
|
46
|
+
- 0
|
47
|
+
version: "1.0"
|
47
48
|
requirement: *id002
|
48
49
|
name: assert-mocha
|
49
50
|
type: :development
|
50
|
-
description:
|
51
|
+
description: A generic threaded TCP server API. It is designed for use as a base for application servers.
|
51
52
|
email:
|
52
53
|
- collin.redding@me.com
|
54
|
+
- kelly@kellyredding.com
|
53
55
|
executables: []
|
54
56
|
|
55
57
|
extensions: []
|
@@ -63,8 +65,8 @@ files:
|
|
63
65
|
- lib/dat-tcp.rb
|
64
66
|
- lib/dat-tcp/logger.rb
|
65
67
|
- lib/dat-tcp/version.rb
|
66
|
-
- lib/dat-tcp/
|
67
|
-
homepage:
|
68
|
+
- lib/dat-tcp/worker_pool.rb
|
69
|
+
homepage: https://github.com/redding/dat-tcp
|
68
70
|
licenses: []
|
69
71
|
|
70
72
|
post_install_message:
|
@@ -96,6 +98,6 @@ rubyforge_project:
|
|
96
98
|
rubygems_version: 1.8.15
|
97
99
|
signing_key:
|
98
100
|
specification_version: 3
|
99
|
-
summary:
|
101
|
+
summary: A generic threaded TCP server API
|
100
102
|
test_files: []
|
101
103
|
|
data/lib/dat-tcp/workers.rb
DELETED
@@ -1,99 +0,0 @@
|
|
1
|
-
# DatTCP's workers is a class for managing the worker threads that are
|
2
|
-
# spun up to handle clients. It manages a list of working threads and provides
|
3
|
-
# external methods for working with them. Working threads are managed by
|
4
|
-
# creating a new one when `process` is called. A client connection and a block
|
5
|
-
# are passed to the worker thread for it to handle the connection. Once it's
|
6
|
-
# done handling the thread, the connection is closed and the thread is removed
|
7
|
-
# from the list. This iignals some of the other methods that the workers class
|
8
|
-
# provides that wait on threads to finish.
|
9
|
-
#
|
10
|
-
require 'thread'
|
11
|
-
|
12
|
-
require 'dat-tcp/logger'
|
13
|
-
|
14
|
-
module DatTCP
|
15
|
-
|
16
|
-
class Workers
|
17
|
-
attr_reader :max, :list, :logger
|
18
|
-
|
19
|
-
def initialize(max = 1, logger = nil)
|
20
|
-
@max = max
|
21
|
-
@logger = logger || DatTCP::Logger::Null.new
|
22
|
-
@list = []
|
23
|
-
|
24
|
-
@mutex = Mutex.new
|
25
|
-
@condition_variable = ConditionVariable.new
|
26
|
-
end
|
27
|
-
|
28
|
-
# This uses the mutex and condition variable to sleep the current thread
|
29
|
-
# until signaled. When a thread closes down, it signals, causing this thread
|
30
|
-
# to wakeup. This will run the loop again, and as long as a worker is
|
31
|
-
# available, the method will return. Otherwise it will sleep again waiting
|
32
|
-
# to be signaled.
|
33
|
-
def wait_for_available
|
34
|
-
@mutex.synchronize do
|
35
|
-
while self.list.size >= self.max
|
36
|
-
@condition_variable.wait(@mutex)
|
37
|
-
end
|
38
|
-
end
|
39
|
-
end
|
40
|
-
|
41
|
-
def process(connecting_socket, &block)
|
42
|
-
self.wait_for_available
|
43
|
-
worker_id = self.list.size + 1
|
44
|
-
@list << Thread.new{ self.serve_client(worker_id, connecting_socket, &block) }
|
45
|
-
end
|
46
|
-
|
47
|
-
# Finish simply sleeps the current thread until signaled. Again, when a
|
48
|
-
# worker thread closes down, it signals. This will cause this to wake up and
|
49
|
-
# continue running the loop. Once the list is empty, the method will return.
|
50
|
-
# Otherwise this will sleep until signaled again. This is a graceful
|
51
|
-
# shutdown, letting the threads finish their processing.
|
52
|
-
def finish
|
53
|
-
@mutex.synchronize do
|
54
|
-
@list.reject!{|thread| !thread.alive? }
|
55
|
-
while !self.list.empty?
|
56
|
-
@condition_variable.wait(@mutex)
|
57
|
-
end
|
58
|
-
end
|
59
|
-
end
|
60
|
-
|
61
|
-
protected
|
62
|
-
|
63
|
-
def log(message, worker_id)
|
64
|
-
self.logger.info("[Worker##{worker_id}] #{message}") if self.logger
|
65
|
-
end
|
66
|
-
|
67
|
-
def serve_client(worker_id, connecting_socket, &block)
|
68
|
-
begin
|
69
|
-
Thread.current["client_address"] = connecting_socket.peeraddr[1, 2].reverse.join(':')
|
70
|
-
self.log("Connecting #{Thread.current["client_address"]}", worker_id)
|
71
|
-
block.call(connecting_socket)
|
72
|
-
rescue Exception => exception
|
73
|
-
self.log("Exception occurred, stopping worker", worker_id)
|
74
|
-
ensure
|
75
|
-
self.disconnect_client(worker_id, connecting_socket, exception)
|
76
|
-
end
|
77
|
-
end
|
78
|
-
|
79
|
-
# Closes the client connection and also shuts the thread down. This is done
|
80
|
-
# by removing the thread from the list. This is wrapped in a mutex
|
81
|
-
# synchronize, to ensure only one thread interacts with list at a time. Also
|
82
|
-
# the condition variable is signaled to trigger the `finish` or
|
83
|
-
# `wait_for_available` methods.
|
84
|
-
def disconnect_client(worker_id, connecting_socket, exception)
|
85
|
-
connecting_socket.close rescue false
|
86
|
-
@mutex.synchronize do
|
87
|
-
@list.delete(Thread.current)
|
88
|
-
@condition_variable.signal
|
89
|
-
end
|
90
|
-
if exception
|
91
|
-
self.log("#{exception.class}: #{exception.message}", worker_id)
|
92
|
-
self.log(exception.backtrace.join("\n"), worker_id)
|
93
|
-
end
|
94
|
-
self.log("Disconnecting #{Thread.current["client_address"]}", worker_id)
|
95
|
-
end
|
96
|
-
|
97
|
-
end
|
98
|
-
|
99
|
-
end
|