dat-tcp 0.1.1 → 0.2.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/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
|