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 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 = "DatTCP is a generic threaded TCP server implementation. It provides a " \
12
- "simple to use interface for defining a TCP server. It is intended to be " \
13
- "used as a base for application-level servers."
14
- gem.summary = "DatTCP is a generic threaded TCP server for defining application-level " \
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.8'])
24
- gem.add_development_dependency('assert-mocha', ['~>0.1'])
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
- # DatTCP Server module is the main interface for defining a new server. It
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/workers'
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
- def initialize(host, port, options = nil)
40
- options ||= {}
41
- options[:max_workers] ||= 4
15
+ attr_reader :logger
42
16
 
43
- @host, @port = [ host, port ]
44
- @logger = DatTCP::Logger.new(options[:debug])
45
- @workers = DatTCP::Workers.new(options[:max_workers], self.logger)
46
- @ready_timeout = options[:ready_timeout] || 1
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
- @mutex = Mutex.new
49
- @condition_variable = ConditionVariable.new
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
- def start
53
- if !self.running?
54
- @shutdown = false
55
- !!self.start_server_thread
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
- false
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 stop
62
- if self.running?
63
- @shutdown = true
64
- @mutex.synchronize do
65
- while self.thread
66
- @condition_variable.wait(@mutex)
67
- end
68
- end
69
- else
70
- false
71
- end
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 join_thread(limit = nil)
75
- @thread.join(limit) if self.running?
108
+ def listening?
109
+ !!@tcp_server
76
110
  end
77
111
 
78
112
  def running?
79
- !!@thread
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
- def name
87
- "#{self.class}|#{self.host}:#{self.port}"
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} @host=#{self.host.inspect} @port=#{self.port.inspect}>"
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 start_server_thread
98
- @tcp_server = TCPServer.new(self.host, self.port)
99
- @mutex.synchronize do
100
- @thread = Thread.new{ self.work_loop }
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
- # Notes:
105
- # * If the server has been shutdown, then `accept_connection` will return
106
- # `nil` always. This will exit the loop and begin shutting down the server.
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
- rescue Exception => exception
114
- self.logger.info("Exception occurred, stopping server!")
115
- ensure
116
- self.shutdown_server_thread(exception)
117
- end
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
- loop do
130
- if IO.select([ @tcp_server ], nil, nil, self.ready_timeout)
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
- # Notes:
139
- # * Stopping the workers is a graceful shutdown. It will let them each finish
140
- # processing by joining their threads.
141
- def shutdown_server_thread(exception = nil)
142
- self.logger.info("Stopping...")
143
- @tcp_server.close rescue false
144
- self.logger.info(" letting any running workers finish...")
145
- self.workers.finish
146
- self.logger.info("Stopped")
147
- if exception
148
- self.logger.error("#{exception.class}: #{exception.message}")
149
- self.logger.error(exception.backtrace.join("\n"))
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
- @mutex.synchronize do
152
- @thread = nil
153
- @condition_variable.signal
225
+
226
+ [ :listen, :run, :stop, :halt, :pause ].each do |name|
227
+ define_method("#{name}?"){ self.to_sym == name }
154
228
  end
155
- true
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
@@ -1,3 +1,3 @@
1
1
  module DatTCP
2
- VERSION = "0.1.1"
2
+ VERSION = "0.2.0"
3
3
  end
@@ -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: 25
4
+ hash: 23
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
- - 1
9
- - 1
10
- version: 0.1.1
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: 2012-11-14 00:00:00 Z
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: 27
28
+ hash: 15
28
29
  segments:
30
+ - 1
29
31
  - 0
30
- - 8
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: 9
43
+ hash: 15
43
44
  segments:
44
- - 0
45
45
  - 1
46
- version: "0.1"
46
+ - 0
47
+ version: "1.0"
47
48
  requirement: *id002
48
49
  name: assert-mocha
49
50
  type: :development
50
- description: DatTCP is a generic threaded TCP server implementation. It provides a simple to use interface for defining a TCP server. It is intended to be used as a base for application-level servers.
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/workers.rb
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: DatTCP is a generic threaded TCP server for defining application-level servers.
101
+ summary: A generic threaded TCP server API
100
102
  test_files: []
101
103
 
@@ -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