dat-tcp 0.4.0 → 0.5.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/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