dat-tcp 0.7.0 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile CHANGED
@@ -2,5 +2,5 @@ source "https://rubygems.org"
2
2
 
3
3
  gemspec
4
4
 
5
- gem 'rake'
6
- gem 'pry', "~> 0.9.0"
5
+ gem 'rake', "~> 10.4.0"
6
+ gem 'pry', "~> 0.9.0"
data/dat-tcp.gemspec CHANGED
@@ -19,7 +19,8 @@ Gem::Specification.new do |gem|
19
19
  gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
20
20
  gem.require_paths = ["lib"]
21
21
 
22
- gem.add_dependency("dat-worker-pool", ["~> 0.5"])
22
+ gem.add_dependency("dat-worker-pool", ["~> 0.6"])
23
23
 
24
- gem.add_development_dependency('assert', ['~> 2.15'])
24
+ gem.add_development_dependency("assert", ["~> 2.15"])
25
+ gem.add_development_dependency("scmd", ["~> 3.0"])
25
26
  end
data/lib/dat-tcp.rb CHANGED
@@ -3,38 +3,44 @@ require 'socket'
3
3
  require 'thread'
4
4
 
5
5
  require 'dat-tcp/version'
6
- require 'dat-tcp/logger'
6
+ require 'dat-tcp/worker'
7
7
 
8
8
  module DatTCP
9
9
 
10
10
  class Server
11
11
 
12
- attr_reader :worker_start_procs, :worker_shutdown_procs
13
- attr_reader :worker_sleep_procs, :worker_wakeup_procs
14
- attr_reader :logger
15
- private :logger
16
-
17
- def initialize(config = nil, &serve_proc)
18
- 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
- @shutdown_timeout = config[:shutdown_timeout] || 15
12
+ DEFAULT_BACKLOG_SIZE = 1024
13
+ DEFAULT_SHUTDOWN_TIMEOUT = 15
14
+ DEFAULT_NUM_WORKERS = 2
15
+
16
+ SIGNAL = '.'.freeze
17
+
18
+ def initialize(worker_class, options = nil)
19
+ if !worker_class.kind_of?(Class) || !worker_class.include?(DatTCP::Worker)
20
+ raise ArgumentError, "worker class must include `#{DatTCP::Worker}`"
21
+ end
22
+
23
+ options ||= {}
24
+ @backlog_size = options[:backlog_size] || DEFAULT_BACKLOG_SIZE
25
+ @shutdown_timeout = options[:shutdown_timeout] || DEFAULT_SHUTDOWN_TIMEOUT
26
+
24
27
  @signal_reader, @signal_writer = IO.pipe
25
- @serve_proc = serve_proc || raise(ArgumentError, "no block given")
26
28
 
27
- @worker_start_procs = []
28
- @worker_shutdown_procs = []
29
- @worker_sleep_procs = []
30
- @worker_wakeup_procs = []
29
+ @logger_proxy = if options[:logger]
30
+ LoggerProxy.new(options[:logger])
31
+ else
32
+ NullLoggerProxy.new
33
+ end
31
34
 
32
- @logger = DatTCP::Logger.new(@debug)
35
+ @worker_pool = DatWorkerPool.new(worker_class, {
36
+ :num_workers => (options[:num_workers] || DEFAULT_NUM_WORKERS),
37
+ :logger => options[:logger],
38
+ :worker_params => options[:worker_params]
39
+ })
33
40
 
34
- @tcp_server = nil
35
- @work_loop_thread = nil
36
- @worker_pool = nil
37
- @signal = Signal.new(:stop)
41
+ @tcp_server = nil
42
+ @thread = nil
43
+ @state = State.new(:stop)
38
44
  end
39
45
 
40
46
  def ip
@@ -50,7 +56,7 @@ module DatTCP
50
56
  end
51
57
 
52
58
  def client_file_descriptors
53
- @worker_pool ? @worker_pool.work_items.map(&:fileno) : []
59
+ @worker_pool.work_items.map(&:fileno)
54
60
  end
55
61
 
56
62
  def listening?
@@ -58,11 +64,11 @@ module DatTCP
58
64
  end
59
65
 
60
66
  def running?
61
- !!(@work_loop_thread && @work_loop_thread.alive?)
67
+ !!(@thread && @thread.alive?)
62
68
  end
63
69
 
64
70
  def listen(*args)
65
- @signal.set :listen
71
+ @state.set :listen
66
72
  @tcp_server = TCPServer.build(*args)
67
73
  raise ArgumentError, "takes ip and port or file descriptor" if !@tcp_server
68
74
  yield @tcp_server if block_given?
@@ -74,159 +80,104 @@ module DatTCP
74
80
  @tcp_server = nil
75
81
  end
76
82
 
77
- def start(client_file_descriptors = nil)
83
+ def start(passed_client_fds = nil)
78
84
  raise NotListeningError.new unless listening?
79
- @signal.set :start
80
- @work_loop_thread = Thread.new{ work_loop(client_file_descriptors) }
85
+ @state.set :run
86
+ @thread = Thread.new{ work_loop(passed_client_fds) }
81
87
  end
82
88
 
83
89
  def pause(wait = false)
84
- @signal_writer.write_nonblock('p')
90
+ return unless self.running?
91
+ @state.set :pause
92
+ wakeup_thread
85
93
  wait_for_shutdown if wait
86
94
  end
87
95
 
88
96
  def stop(wait = false)
89
- @signal_writer.write_nonblock('s')
97
+ return unless self.running?
98
+ @state.set :stop
99
+ wakeup_thread
90
100
  wait_for_shutdown if wait
91
101
  end
92
102
 
93
103
  def halt(wait = false)
94
- @signal_writer.write_nonblock('h')
104
+ return unless self.running?
105
+ @state.set :halt
106
+ wakeup_thread
95
107
  wait_for_shutdown if wait
96
108
  end
97
109
 
98
- def on_worker_start(&block); @worker_start_procs << block; end
99
- def on_worker_shutdown(&block); @worker_shutdown_procs << block; end
100
- def on_worker_sleep(&block); @worker_sleep_procs << block; end
101
- def on_worker_wakeup(&block); @worker_wakeup_procs << block; end
102
-
103
110
  def inspect
104
111
  reference = '0x0%x' % (self.object_id << 1)
105
112
  "#<#{self.class}:#{reference}".tap do |s|
106
113
  s << " @ip=#{ip.inspect} @port=#{port.inspect}"
107
- s << " @work_loop_status=#{@work_loop_thread.status.inspect}" if running?
108
114
  s << ">"
109
115
  end
110
116
  end
111
117
 
112
118
  private
113
119
 
114
- def serve(socket)
115
- @serve_proc.call(socket)
120
+ def work_loop(passed_client_fds)
121
+ setup(passed_client_fds)
122
+ accept_client_connections while @state.run?
123
+ rescue StandardError => exception
124
+ @state.set :stop
125
+ log{ "An error occurred while running the server, exiting" }
126
+ log{ "#{exception.class}: #{exception.message}" }
127
+ (exception.backtrace || []).each{ |l| log{ l } }
116
128
  ensure
117
- socket.close rescue false
129
+ teardown
118
130
  end
119
131
 
120
- def work_loop(client_file_descriptors = nil)
121
- logger.info "Starting work loop..."
122
- @worker_pool = build_worker_pool
123
- add_client_sockets_from_fds client_file_descriptors
132
+ def setup(passed_client_fds)
124
133
  @worker_pool.start
125
- process_inputs while @signal.start?
126
- logger.info "Stopping work loop..."
127
- shutdown_worker_pool unless @signal.halt?
128
- rescue StandardError => exception
129
- logger.error "Exception occurred, stopping server!"
130
- logger.error "#{exception.class}: #{exception.message}"
131
- logger.error exception.backtrace.join("\n")
132
- ensure
133
- unless @signal.pause?
134
- logger.info "Closing TCP server connection"
135
- stop_listen
134
+ (passed_client_fds || []).each do |fd|
135
+ @worker_pool.push TCPSocket.for_fd(fd)
136
136
  end
137
- clear_thread
138
- logger.info "Stopped work loop"
139
137
  end
140
138
 
141
- def build_worker_pool
142
- wp = DatWorkerPool.new(
143
- @min_workers,
144
- @max_workers
145
- ){ |socket| serve(socket) }
139
+ def accept_client_connections
140
+ ready_inputs, _, _ = IO.select([@tcp_server, @signal_reader])
146
141
 
147
- # add any configured callbacks
148
- self.worker_start_procs.each do |cb|
149
- wp.on_worker_start(&cb)
142
+ if ready_inputs.include?(@tcp_server)
143
+ @worker_pool.push @tcp_server.accept
150
144
  end
151
- self.worker_shutdown_procs.each do |cb|
152
- wp.on_worker_shutdown(&cb)
153
- end
154
- self.worker_sleep_procs.each do |cb|
155
- wp.on_worker_sleep(&cb)
156
- end
157
- self.worker_wakeup_procs.each do |cb|
158
- wp.on_worker_wakeup(&cb)
159
- end
160
-
161
- wp
162
- end
163
145
 
164
- def add_client_sockets_from_fds(file_descriptors)
165
- (file_descriptors || []).each do |file_descriptor|
166
- @worker_pool.add_work TCPSocket.for_fd(file_descriptor)
146
+ if ready_inputs.include?(@signal_reader)
147
+ @signal_reader.read_nonblock(SIGNAL.bytesize)
167
148
  end
168
149
  end
169
150
 
170
- def process_inputs
171
- ready_inputs, _, _ = IO.select([ @tcp_server, @signal_reader ])
172
- accept_connection if ready_inputs.include?(@tcp_server)
173
- process_signal if ready_inputs.include?(@signal_reader)
174
- end
175
-
176
- def accept_connection
177
- @worker_pool.add_work @tcp_server.accept
178
- end
179
-
180
- def process_signal
181
- @signal.send @signal_reader.read_nonblock(1)
182
- end
151
+ def teardown
152
+ unless @state.pause?
153
+ log{ "Stop listening for connections, closing TCP socket" }
154
+ self.stop_listen
155
+ end
183
156
 
184
- def shutdown_worker_pool
185
- logger.info "Shutting down worker pool"
186
- @worker_pool.shutdown(@shutdown_timeout)
157
+ timeout = @state.halt? ? 0 : @shutdown_timeout
158
+ @worker_pool.shutdown(timeout)
159
+ ensure
160
+ @thread = nil
187
161
  end
188
162
 
189
- def clear_thread
190
- @work_loop_thread = nil
163
+ def wakeup_thread
164
+ @signal_writer.write_nonblock(SIGNAL)
191
165
  end
192
166
 
193
167
  def wait_for_shutdown
194
- @work_loop_thread.join if @work_loop_thread
168
+ @thread.join if @thread
195
169
  end
196
170
 
197
- class Signal
198
- def initialize(value)
199
- @value = value
200
- @mutex = Mutex.new
201
- end
202
-
203
- def s; set :stop; end
204
- def h; set :halt; end
205
- def p; set :pause; end
206
-
207
- def set(value)
208
- @mutex.synchronize{ @value = value }
209
- end
210
-
211
- def listen?
212
- @mutex.synchronize{ @value == :listen }
213
- end
214
-
215
- def start?
216
- @mutex.synchronize{ @value == :start }
217
- end
218
-
219
- def pause?
220
- @mutex.synchronize{ @value == :pause }
221
- end
222
-
223
- def stop?
224
- @mutex.synchronize{ @value == :stop }
225
- end
171
+ def log(&message_block)
172
+ @logger_proxy.log(&message_block)
173
+ end
226
174
 
227
- def halt?
228
- @mutex.synchronize{ @value == :halt }
229
- end
175
+ class State < DatWorkerPool::LockedObject
176
+ def listen?; self.value == :listen; end
177
+ def run?; self.value == :run; end
178
+ def pause?; self.value == :pause; end
179
+ def stop?; self.value == :stop; end
180
+ def halt?; self.value == :halt; end
230
181
  end
231
182
 
232
183
  module TCPServer
@@ -263,6 +214,16 @@ module DatTCP
263
214
  end
264
215
  end
265
216
 
217
+ class LoggerProxy < Struct.new(:logger)
218
+ def log(&message_block)
219
+ self.logger.debug("[DTCP] #{message_block.call}")
220
+ end
221
+ end
222
+
223
+ class NullLoggerProxy
224
+ def log(&block); end
225
+ end
226
+
266
227
  end
267
228
 
268
229
  class NotListeningError < RuntimeError
@@ -1,45 +1,49 @@
1
- require 'dat-tcp/logger'
1
+ require 'dat-tcp'
2
+ require 'dat-tcp/worker'
2
3
 
3
4
  module DatTCP
4
5
 
5
6
  class ServerSpy
6
7
 
8
+ attr_reader :worker_class
9
+ attr_reader :options, :backlog_size, :shutdown_timeout
10
+ attr_reader :num_workers, :logger, :worker_params
7
11
  attr_reader :ip, :port, :file_descriptor
8
12
  attr_reader :client_file_descriptors
9
- attr_reader :logger
10
- attr_reader :worker_start_procs, :worker_shutdown_procs
11
- attr_reader :worker_sleep_procs, :worker_wakeup_procs
12
13
  attr_reader :waiting_for_pause, :waiting_for_stop, :waiting_for_halt
13
14
  attr_accessor :listen_called, :start_called
14
15
  attr_accessor :stop_listen_called, :pause_called
15
16
  attr_accessor :stop_called, :halt_called
16
17
 
17
- attr_accessor :serve_proc
18
+ def initialize(worker_class, options = nil)
19
+ @worker_class = worker_class
20
+ if !@worker_class.kind_of?(Class) || !@worker_class.include?(DatTCP::Worker)
21
+ raise ArgumentError, "worker class must include `#{DatTCP::Worker}`"
22
+ end
18
23
 
19
- def initialize
20
- @ip = nil
21
- @port = nil
22
- @file_descriptor = nil
24
+ server_ns = DatTCP::Server
25
+ @options = options || {}
26
+ @backlog_size = @options[:backlog_size] || server_ns::DEFAULT_BACKLOG_SIZE
27
+ @shutdown_timeout = @options[:shutdown_timeout] || server_ns::DEFAULT_SHUTDOWN_TIMEOUT
28
+ @num_workers = (@options[:num_workers] || server_ns::DEFAULT_NUM_WORKERS).to_i
29
+ @logger = @options[:logger]
30
+ @worker_params = @options[:worker_params]
31
+
32
+ @ip = nil
33
+ @port = nil
34
+ @file_descriptor = nil
23
35
  @client_file_descriptors = []
24
- @logger = DatTCP::Logger::Null.new
25
-
26
- @worker_start_procs = []
27
- @worker_shutdown_procs = []
28
- @worker_sleep_procs = []
29
- @worker_wakeup_procs = []
30
36
 
31
37
  @waiting_for_pause = nil
32
- @waiting_for_stop = nil
33
- @waiting_for_halt = nil
38
+ @waiting_for_stop = nil
39
+ @waiting_for_halt = nil
34
40
 
35
- @listen_called = false
41
+ @listen_called = false
36
42
  @stop_listen_called = false
37
- @start_called = false
38
- @pause_called = false
39
- @stop_called = false
40
- @halt_called = false
41
-
42
- @serve_proc = proc{ }
43
+ @start_called = false
44
+ @pause_called = false
45
+ @stop_called = false
46
+ @halt_called = false
43
47
  end
44
48
 
45
49
  def listening?
@@ -64,8 +68,8 @@ module DatTCP
64
68
  @stop_listen_called = true
65
69
  end
66
70
 
67
- def start(client_file_descriptors = nil)
68
- @client_file_descriptors = client_file_descriptors || []
71
+ def start(passed_client_fds = nil)
72
+ @client_file_descriptors = passed_client_fds || []
69
73
  @start_called = true
70
74
  end
71
75
 
@@ -84,11 +88,6 @@ module DatTCP
84
88
  @halt_called = true
85
89
  end
86
90
 
87
- def on_worker_start(&block); @worker_start_procs << block; end
88
- def on_worker_shutdown(&block); @worker_shutdown_procs << block; end
89
- def on_worker_sleep(&block); @worker_sleep_procs << block; end
90
- def on_worker_wakeup(&block); @worker_wakeup_procs << block; end
91
-
92
91
  end
93
92
 
94
93
  end
@@ -1,3 +1,3 @@
1
1
  module DatTCP
2
- VERSION = "0.7.0"
2
+ VERSION = "0.8.0"
3
3
  end
@@ -0,0 +1,26 @@
1
+ require 'dat-worker-pool/worker'
2
+
3
+ module DatTCP
4
+
5
+ module Worker
6
+
7
+ def self.included(klass)
8
+ klass.class_eval do
9
+ include DatWorkerPool::Worker
10
+
11
+ end
12
+ end
13
+
14
+ module TestHelpers
15
+
16
+ def self.included(klass)
17
+ klass.class_eval do
18
+ include DatWorkerPool::Worker::TestHelpers
19
+ end
20
+ end
21
+
22
+ end
23
+
24
+ end
25
+
26
+ 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: 3
4
+ hash: 63
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
- - 7
8
+ - 8
9
9
  - 0
10
- version: 0.7.0
10
+ version: 0.8.0
11
11
  platform: ruby
12
12
  authors:
13
13
  - Collin Redding
@@ -16,7 +16,7 @@ autorequire:
16
16
  bindir: bin
17
17
  cert_chain: []
18
18
 
19
- date: 2015-09-08 00:00:00 Z
19
+ date: 2015-11-30 00:00:00 Z
20
20
  dependencies:
21
21
  - !ruby/object:Gem::Dependency
22
22
  requirement: &id001 !ruby/object:Gem::Requirement
@@ -24,11 +24,11 @@ dependencies:
24
24
  requirements:
25
25
  - - ~>
26
26
  - !ruby/object:Gem::Version
27
- hash: 1
27
+ hash: 7
28
28
  segments:
29
29
  - 0
30
- - 5
31
- version: "0.5"
30
+ - 6
31
+ version: "0.6"
32
32
  type: :runtime
33
33
  name: dat-worker-pool
34
34
  version_requirements: *id001
@@ -48,6 +48,21 @@ dependencies:
48
48
  name: assert
49
49
  version_requirements: *id002
50
50
  prerelease: false
51
+ - !ruby/object:Gem::Dependency
52
+ requirement: &id003 !ruby/object:Gem::Requirement
53
+ none: false
54
+ requirements:
55
+ - - ~>
56
+ - !ruby/object:Gem::Version
57
+ hash: 7
58
+ segments:
59
+ - 3
60
+ - 0
61
+ version: "3.0"
62
+ type: :development
63
+ name: scmd
64
+ version_requirements: *id003
65
+ prerelease: false
51
66
  description: A generic threaded TCP server API. It is designed for use as a base for application servers.
52
67
  email:
53
68
  - collin.redding@me.com
@@ -63,9 +78,9 @@ files:
63
78
  - Rakefile
64
79
  - dat-tcp.gemspec
65
80
  - lib/dat-tcp.rb
66
- - lib/dat-tcp/logger.rb
67
81
  - lib/dat-tcp/server_spy.rb
68
82
  - lib/dat-tcp/version.rb
83
+ - lib/dat-tcp/worker.rb
69
84
  homepage: https://github.com/redding/dat-tcp
70
85
  licenses:
71
86
  - MIT
@@ -1,36 +0,0 @@
1
- # DatTCP's logger module acts as a generator for either a debug or null logger.
2
- # This allows the server and workers to always assume they have some logger
3
- # object and not have to worry about conditionally checking if a logger is
4
- # present. The null logger takes all messages and does nothing with them. When
5
- # debug mode is turned off, this logger is used, which keeps the server from
6
- # logging. The debug logger uses an instance of ruby's standard logger and
7
- # writes to STDOUT.
8
- #
9
- require 'logger'
10
-
11
- module DatTCP::Logger
12
-
13
- def self.new(debug)
14
- !!debug ? DatTCP::Logger::Debug.new : DatTCP::Logger::Null.new
15
- end
16
-
17
- module Debug
18
-
19
- def self.new
20
- ::Logger.new(STDOUT).tap do |logger|
21
- logger.progname = "[#{self.name}]"
22
- logger.datetime_format = "%m/%d/%Y %H:%M:%S%p "
23
- end
24
- end
25
-
26
- end
27
-
28
- class Null
29
-
30
- Logger::Severity.constants.each do |name|
31
- define_method(name.downcase){|*args| } # no-op
32
- end
33
-
34
- end
35
-
36
- end