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