dat-tcp 0.4.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile CHANGED
@@ -3,4 +3,4 @@ source "https://rubygems.org"
3
3
  gemspec
4
4
 
5
5
  gem 'rake'
6
- gem 'pry'
6
+ gem 'pry', "~> 0.9.0"
data/Rakefile CHANGED
@@ -1,2 +1 @@
1
1
  require "bundler/gem_tasks"
2
- require "bench/tasks"
data/dat-tcp.gemspec CHANGED
@@ -12,14 +12,14 @@ Gem::Specification.new do |gem|
12
12
  " designed for use as a base for application servers."
13
13
  gem.summary = "A generic threaded TCP server API"
14
14
  gem.homepage = "https://github.com/redding/dat-tcp"
15
+ gem.license = 'MIT'
15
16
 
16
17
  gem.files = `git ls-files -- lib/* Gemfile Rakefile *.gemspec`.split($/)
17
18
  gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
18
19
  gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
19
20
  gem.require_paths = ["lib"]
20
21
 
21
- gem.add_dependency("dat-worker-pool", ["~> 0.1"])
22
+ gem.add_dependency("dat-worker-pool", ["~> 0.3"])
22
23
 
23
- gem.add_development_dependency('assert', ['~> 2.0'])
24
- gem.add_development_dependency('assert-mocha', ['~> 1.0'])
24
+ gem.add_development_dependency('assert', ['~> 2.11'])
25
25
  end
data/lib/dat-tcp.rb CHANGED
@@ -1,5 +1,4 @@
1
1
  require 'dat-worker-pool'
2
- require 'ostruct'
3
2
  require 'socket'
4
3
  require 'thread'
5
4
 
@@ -8,86 +7,27 @@ require 'dat-tcp/logger'
8
7
 
9
8
  module DatTCP
10
9
 
11
- module Server
10
+ class Server
12
11
 
13
12
  attr_reader :logger
14
-
15
- def initialize(config = nil)
16
- config = OpenStruct.new(config || {})
17
- @backlog_size = config.backlog_size || 1024
18
- @debug = config.debug || false
19
- @min_workers = config.min_workers || 2
20
- @max_workers = config.max_workers || 4
21
- @ready_timeout = config.ready_timeout || 1
22
- @shutdown_timeout = config.shutdown_timeout || 15
13
+ private :logger
14
+
15
+ def initialize(config = nil, &serve_proc)
16
+ config ||= {}
17
+ @backlog_size = config[:backlog_size] || 1024
18
+ @debug = config[:debug] || false
19
+ @min_workers = config[:min_workers] || 2
20
+ @max_workers = config[:max_workers] || 4
21
+ @shutdown_timeout = config[:shutdown_timeout] || 15
22
+ @signal_reader, @signal_writer = IO.pipe
23
+ @serve_proc = serve_proc || raise(ArgumentError, "no block given")
23
24
 
24
25
  @logger = DatTCP::Logger.new(@debug)
25
26
 
26
- check_configuration
27
-
28
27
  @tcp_server = nil
29
28
  @work_loop_thread = nil
30
29
  @worker_pool = nil
31
- set_state :stop
32
- end
33
-
34
- # Socket Options:
35
- # * SOL_SOCKET - specifies the protocol layer the option applies to.
36
- # SOL_SOCKET is basic socket options (as opposed to
37
- # something like IPPROTO_TCP for TCP socket options).
38
- # * SO_REUSEADDR - indicates that the rules used in validating addresses
39
- # supplied in a bind(2) call should allow reuse of local
40
- # addresses. This will allow us to re-bind to a port if we
41
- # were shutdown and started right away. This will still
42
- # throw an "address in use" if a socket is active on the
43
- # port.
44
-
45
- def listen(*args)
46
- build_server_method = if args.size == 2
47
- :new
48
- elsif args.size == 1
49
- :for_fd
50
- else
51
- raise InvalidListenArgsError.new
52
- end
53
- set_state :listen
54
- run_hook 'on_listen'
55
- @tcp_server = TCPServer.send(build_server_method, *args)
56
-
57
- @tcp_server.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, true)
58
- run_hook 'configure_tcp_server', @tcp_server
59
-
60
- @tcp_server.listen(@backlog_size)
61
- end
62
-
63
- def run(client_file_descriptors = nil)
64
- raise NotListeningError.new if !self.listening?
65
- set_state :run
66
- run_hook 'on_run'
67
- @work_loop_thread = Thread.new{ work_loop(client_file_descriptors) }
68
- end
69
-
70
- def pause(wait = true)
71
- set_state :pause
72
- run_hook 'on_pause'
73
- wait_for_shutdown if wait
74
- end
75
-
76
- def stop(wait = true)
77
- set_state :stop
78
- run_hook 'on_stop'
79
- wait_for_shutdown if wait
80
- end
81
-
82
- def halt(wait = true)
83
- set_state :halt
84
- run_hook 'on_halt'
85
- wait_for_shutdown if wait
86
- end
87
-
88
- def stop_listening
89
- @tcp_server.close rescue false
90
- @tcp_server = nil
30
+ @signal = Signal.new(:stop)
91
31
  end
92
32
 
93
33
  def ip
@@ -111,103 +51,107 @@ module DatTCP
111
51
  end
112
52
 
113
53
  def running?
114
- !!@work_loop_thread
115
- end
116
-
117
- def serve(socket)
118
- self.serve!(socket)
119
- ensure
120
- socket.close rescue false
121
- end
122
-
123
- # This method should be overwritten to handle new connections
124
- def serve!(socket)
54
+ !!(@work_loop_thread && @work_loop_thread.alive?)
125
55
  end
126
56
 
127
- # Hooks
128
-
129
- def on_listen
57
+ def listen(*args)
58
+ @signal.set :listen
59
+ @tcp_server = TCPServer.build(*args)
60
+ raise ArgumentError, "takes ip and port or file descriptor" if !@tcp_server
61
+ yield @tcp_server if block_given?
62
+ @tcp_server.listen(@backlog_size)
130
63
  end
131
64
 
132
- def configure_tcp_server(tcp_server)
65
+ def stop_listen
66
+ @tcp_server.close rescue false
67
+ @tcp_server = nil
133
68
  end
134
69
 
135
- def on_run
70
+ def start(client_file_descriptors = nil)
71
+ raise NotListeningError.new unless listening?
72
+ @signal.set :start
73
+ @work_loop_thread = Thread.new{ work_loop(client_file_descriptors) }
136
74
  end
137
75
 
138
- def on_pause
76
+ def pause(wait = false)
77
+ @signal_writer.write_nonblock('p')
78
+ wait_for_shutdown if wait
139
79
  end
140
80
 
141
- def on_stop
81
+ def stop(wait = false)
82
+ @signal_writer.write_nonblock('s')
83
+ wait_for_shutdown if wait
142
84
  end
143
85
 
144
- def on_halt
86
+ def halt(wait = false)
87
+ @signal_writer.write_nonblock('h')
88
+ wait_for_shutdown if wait
145
89
  end
146
90
 
147
91
  def inspect
148
92
  reference = '0x0%x' % (self.object_id << 1)
149
- "#<#{self.class}:#{reference}".tap do |inspect_str|
150
- inspect_str << " @state=#{@state.inspect}"
151
- if self.listening?
152
- port, ip = @tcp_server.addr[1, 2]
153
- inspect_str << " @ip=#{ip.inspect} @port=#{port.inspect}"
154
- end
155
- inspect_str << " @work_loop_status=#{@work_loop_thread.status.inspect}" if self.running?
156
- inspect_str << ">"
93
+ "#<#{self.class}:#{reference}".tap do |s|
94
+ s << " @ip=#{ip.inspect} @port=#{port.inspect}"
95
+ s << " @work_loop_status=#{@work_loop_thread.status.inspect}" if running?
96
+ s << ">"
157
97
  end
158
98
  end
159
99
 
160
- protected
100
+ private
101
+
102
+ def serve(socket)
103
+ @serve_proc.call(socket)
104
+ ensure
105
+ socket.close rescue false
106
+ end
161
107
 
162
108
  def work_loop(client_file_descriptors = nil)
163
- self.logger.info "Starting work loop..."
164
- pool_args = [ @min_workers, @max_workers, @debug ]
165
- @worker_pool = DatWorkerPool.new(*pool_args){|socket| serve(socket) }
166
- self.enqueue_file_descriptors(client_file_descriptors || [])
167
- while @state.run?
168
- @worker_pool.add_work self.accept_connection
109
+ logger.info "Starting work loop..."
110
+ @worker_pool = DatWorkerPool.new(@min_workers, @max_workers) do |socket|
111
+ serve(socket)
169
112
  end
170
- self.logger.info "Stopping work loop..."
171
- shutdown_worker_pool if !@state.halt?
172
- rescue Exception => exception
173
- self.logger.error "Exception occurred, stopping server!"
174
- self.logger.error "#{exception.class}: #{exception.message}"
175
- self.logger.error exception.backtrace.join("\n")
113
+ add_client_sockets_from_fds client_file_descriptors
114
+ process_inputs while @signal.start?
115
+ logger.info "Stopping work loop..."
116
+ shutdown_worker_pool unless @signal.halt?
117
+ rescue StandardError => exception
118
+ logger.error "Exception occurred, stopping server!"
119
+ logger.error "#{exception.class}: #{exception.message}"
120
+ logger.error exception.backtrace.join("\n")
176
121
  ensure
177
- close_connection if !@state.pause?
122
+ unless @signal.pause?
123
+ logger.info "Closing TCP server connection"
124
+ stop_listen
125
+ end
178
126
  clear_thread
179
- self.logger.info "Stopped work loop"
127
+ logger.info "Stopped work loop"
180
128
  end
181
129
 
182
- def enqueue_file_descriptors(file_descriptors)
183
- file_descriptors.each do |file_descriptor|
130
+ def add_client_sockets_from_fds(file_descriptors)
131
+ (file_descriptors || []).each do |file_descriptor|
184
132
  @worker_pool.add_work TCPSocket.for_fd(file_descriptor)
185
133
  end
186
134
  end
187
135
 
188
- # An accept-loop waiting for new connections. Will wait for a connection
189
- # (up to `ready_timeout`) and accept it. `IO.select` with the timeout
190
- # allows the server to be responsive to shutdowns.
136
+ def process_inputs
137
+ ready_inputs, _, _ = IO.select([ @tcp_server, @signal_reader ])
138
+ accept_connection if ready_inputs.include?(@tcp_server)
139
+ process_signal if ready_inputs.include?(@signal_reader)
140
+ end
141
+
191
142
  def accept_connection
192
- while @state.run?
193
- return @tcp_server.accept if self.connection_ready?
194
- end
143
+ @worker_pool.add_work @tcp_server.accept
195
144
  end
196
145
 
197
- def connection_ready?
198
- !!IO.select([ @tcp_server ], nil, nil, @ready_timeout)
146
+ def process_signal
147
+ @signal.send @signal_reader.read_nonblock(1)
199
148
  end
200
149
 
201
150
  def shutdown_worker_pool
202
- self.logger.info "Shutting down worker pool, letting it finish..."
151
+ logger.info "Shutting down worker pool"
203
152
  @worker_pool.shutdown(@shutdown_timeout)
204
153
  end
205
154
 
206
- def close_connection
207
- self.logger.info "Closing TCP server connection..."
208
- self.stop_listening
209
- end
210
-
211
155
  def clear_thread
212
156
  @work_loop_thread = nil
213
157
  end
@@ -216,50 +160,81 @@ module DatTCP
216
160
  @work_loop_thread.join if @work_loop_thread
217
161
  end
218
162
 
219
- def check_configuration
220
- if @min_workers > @max_workers
221
- self.logger.warn "The minimum number of workers (#{@min_workers}) " \
222
- "is greater than " \
223
- "the maximum number of workers (#{@max_workers})."
163
+ class Signal
164
+ def initialize(value)
165
+ @value = value
166
+ @mutex = Mutex.new
224
167
  end
225
- end
226
168
 
227
- def run_hook(method, *args)
228
- self.send(method, *args)
229
- end
169
+ def s; set :stop; end
170
+ def h; set :halt; end
171
+ def p; set :pause; end
230
172
 
231
- def set_state(name)
232
- @state = State.new(name)
233
- end
173
+ def set(value)
174
+ @mutex.synchronize{ @value = value }
175
+ end
234
176
 
235
- class State < String
177
+ def listen?
178
+ @mutex.synchronize{ @value == :listen }
179
+ end
236
180
 
237
- def initialize(value)
238
- super value.to_s
181
+ def start?
182
+ @mutex.synchronize{ @value == :start }
239
183
  end
240
184
 
241
- [ :listen, :run, :stop, :halt, :pause ].each do |name|
242
- define_method("#{name}?"){ self.to_sym == name }
185
+ def pause?
186
+ @mutex.synchronize{ @value == :pause }
243
187
  end
244
188
 
189
+ def stop?
190
+ @mutex.synchronize{ @value == :stop }
191
+ end
192
+
193
+ def halt?
194
+ @mutex.synchronize{ @value == :halt }
195
+ end
245
196
  end
246
197
 
247
- end
198
+ module TCPServer
199
+ def self.build(*args)
200
+ case args.size
201
+ when 2
202
+ self.new(*args)
203
+ when 1
204
+ self.for_fd(*args)
205
+ end
206
+ end
248
207
 
249
- class InvalidListenArgsError < ArgumentError
208
+ def self.new(ip, port)
209
+ configure(::TCPServer.new(ip, port))
210
+ end
250
211
 
251
- def initialize
252
- super "invalid arguments, must be either a file descriptor or an ip and port"
212
+ def self.for_fd(file_descriptor)
213
+ configure(::TCPServer.for_fd(file_descriptor))
214
+ end
215
+
216
+ # `setsockopt` values:
217
+ # * SOL_SOCKET - specifies the protocol layer the option applies to.
218
+ # SOL_SOCKET is basic socket options (as opposed to
219
+ # something like IPPROTO_TCP for TCP socket options).
220
+ # * SO_REUSEADDR - indicates that the rules used in validating addresses
221
+ # supplied in a bind(2) call should allow reuse of local
222
+ # addresses. This will allow us to re-bind to a port if
223
+ # we were shutdown and started right away. This will
224
+ # still throw an "address in use" if a socket is active
225
+ # on the port.
226
+ def self.configure(tcp_server)
227
+ tcp_server.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, true)
228
+ tcp_server
229
+ end
253
230
  end
254
231
 
255
232
  end
256
233
 
257
234
  class NotListeningError < RuntimeError
258
-
259
235
  def initialize
260
- super "`listen` must be called before calling `run`"
236
+ super "server isn't listening, call `listen` first"
261
237
  end
262
-
263
238
  end
264
239
 
265
240
  end
@@ -1,3 +1,3 @@
1
1
  module DatTCP
2
- VERSION = "0.4.0"
2
+ VERSION = "0.5.0"
3
3
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dat-tcp
3
3
  version: !ruby/object:Gem::Version
4
- hash: 15
4
+ hash: 11
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
- - 4
8
+ - 5
9
9
  - 0
10
- version: 0.4.0
10
+ version: 0.5.0
11
11
  platform: ruby
12
12
  authors:
13
13
  - Collin Redding
@@ -16,53 +16,38 @@ autorequire:
16
16
  bindir: bin
17
17
  cert_chain: []
18
18
 
19
- date: 2013-07-15 00:00:00 Z
19
+ date: 2014-06-17 00:00:00 Z
20
20
  dependencies:
21
21
  - !ruby/object:Gem::Dependency
22
- prerelease: false
23
- version_requirements: &id001 !ruby/object:Gem::Requirement
22
+ requirement: &id001 !ruby/object:Gem::Requirement
24
23
  none: false
25
24
  requirements:
26
25
  - - ~>
27
26
  - !ruby/object:Gem::Version
28
- hash: 9
27
+ hash: 13
29
28
  segments:
30
29
  - 0
31
- - 1
32
- version: "0.1"
33
- requirement: *id001
34
- name: dat-worker-pool
30
+ - 3
31
+ version: "0.3"
32
+ version_requirements: *id001
35
33
  type: :runtime
36
- - !ruby/object:Gem::Dependency
34
+ name: dat-worker-pool
37
35
  prerelease: false
38
- version_requirements: &id002 !ruby/object:Gem::Requirement
36
+ - !ruby/object:Gem::Dependency
37
+ requirement: &id002 !ruby/object:Gem::Requirement
39
38
  none: false
40
39
  requirements:
41
40
  - - ~>
42
41
  - !ruby/object:Gem::Version
43
- hash: 3
42
+ hash: 21
44
43
  segments:
45
44
  - 2
46
- - 0
47
- version: "2.0"
48
- requirement: *id002
49
- name: assert
45
+ - 11
46
+ version: "2.11"
47
+ version_requirements: *id002
50
48
  type: :development
51
- - !ruby/object:Gem::Dependency
49
+ name: assert
52
50
  prerelease: false
53
- version_requirements: &id003 !ruby/object:Gem::Requirement
54
- none: false
55
- requirements:
56
- - - ~>
57
- - !ruby/object:Gem::Version
58
- hash: 15
59
- segments:
60
- - 1
61
- - 0
62
- version: "1.0"
63
- requirement: *id003
64
- name: assert-mocha
65
- type: :development
66
51
  description: A generic threaded TCP server API. It is designed for use as a base for application servers.
67
52
  email:
68
53
  - collin.redding@me.com
@@ -81,8 +66,8 @@ files:
81
66
  - lib/dat-tcp/logger.rb
82
67
  - lib/dat-tcp/version.rb
83
68
  homepage: https://github.com/redding/dat-tcp
84
- licenses: []
85
-
69
+ licenses:
70
+ - MIT
86
71
  post_install_message:
87
72
  rdoc_options: []
88
73
 
@@ -109,7 +94,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
109
94
  requirements: []
110
95
 
111
96
  rubyforge_project:
112
- rubygems_version: 1.8.15
97
+ rubygems_version: 1.8.29
113
98
  signing_key:
114
99
  specification_version: 3
115
100
  summary: A generic threaded TCP server API