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 +1 -1
- data/Rakefile +0 -1
- data/dat-tcp.gemspec +3 -3
- data/lib/dat-tcp.rb +130 -155
- data/lib/dat-tcp/version.rb +1 -1
- metadata +20 -35
data/Gemfile
CHANGED
data/Rakefile
CHANGED
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.
|
22
|
+
gem.add_dependency("dat-worker-pool", ["~> 0.3"])
|
22
23
|
|
23
|
-
gem.add_development_dependency('assert',
|
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
|
-
|
10
|
+
class Server
|
12
11
|
|
13
12
|
attr_reader :logger
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
@
|
19
|
-
@
|
20
|
-
@
|
21
|
-
@
|
22
|
-
@shutdown_timeout = config
|
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
|
-
|
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
|
-
|
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
|
-
|
128
|
-
|
129
|
-
|
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
|
65
|
+
def stop_listen
|
66
|
+
@tcp_server.close rescue false
|
67
|
+
@tcp_server = nil
|
133
68
|
end
|
134
69
|
|
135
|
-
def
|
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
|
76
|
+
def pause(wait = false)
|
77
|
+
@signal_writer.write_nonblock('p')
|
78
|
+
wait_for_shutdown if wait
|
139
79
|
end
|
140
80
|
|
141
|
-
def
|
81
|
+
def stop(wait = false)
|
82
|
+
@signal_writer.write_nonblock('s')
|
83
|
+
wait_for_shutdown if wait
|
142
84
|
end
|
143
85
|
|
144
|
-
def
|
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 |
|
150
|
-
|
151
|
-
if
|
152
|
-
|
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
|
-
|
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
|
-
|
164
|
-
|
165
|
-
|
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
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
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
|
-
|
122
|
+
unless @signal.pause?
|
123
|
+
logger.info "Closing TCP server connection"
|
124
|
+
stop_listen
|
125
|
+
end
|
178
126
|
clear_thread
|
179
|
-
|
127
|
+
logger.info "Stopped work loop"
|
180
128
|
end
|
181
129
|
|
182
|
-
def
|
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
|
-
|
189
|
-
|
190
|
-
|
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
|
-
|
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
|
198
|
-
|
146
|
+
def process_signal
|
147
|
+
@signal.send @signal_reader.read_nonblock(1)
|
199
148
|
end
|
200
149
|
|
201
150
|
def shutdown_worker_pool
|
202
|
-
|
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
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
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
|
-
|
228
|
-
|
229
|
-
|
169
|
+
def s; set :stop; end
|
170
|
+
def h; set :halt; end
|
171
|
+
def p; set :pause; end
|
230
172
|
|
231
|
-
|
232
|
-
|
233
|
-
|
173
|
+
def set(value)
|
174
|
+
@mutex.synchronize{ @value = value }
|
175
|
+
end
|
234
176
|
|
235
|
-
|
177
|
+
def listen?
|
178
|
+
@mutex.synchronize{ @value == :listen }
|
179
|
+
end
|
236
180
|
|
237
|
-
def
|
238
|
-
|
181
|
+
def start?
|
182
|
+
@mutex.synchronize{ @value == :start }
|
239
183
|
end
|
240
184
|
|
241
|
-
|
242
|
-
|
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
|
-
|
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
|
-
|
208
|
+
def self.new(ip, port)
|
209
|
+
configure(::TCPServer.new(ip, port))
|
210
|
+
end
|
250
211
|
|
251
|
-
|
252
|
-
|
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 "
|
236
|
+
super "server isn't listening, call `listen` first"
|
261
237
|
end
|
262
|
-
|
263
238
|
end
|
264
239
|
|
265
240
|
end
|
data/lib/dat-tcp/version.rb
CHANGED
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:
|
4
|
+
hash: 11
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 0
|
8
|
-
-
|
8
|
+
- 5
|
9
9
|
- 0
|
10
|
-
version: 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:
|
19
|
+
date: 2014-06-17 00:00:00 Z
|
20
20
|
dependencies:
|
21
21
|
- !ruby/object:Gem::Dependency
|
22
|
-
|
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:
|
27
|
+
hash: 13
|
29
28
|
segments:
|
30
29
|
- 0
|
31
|
-
-
|
32
|
-
version: "0.
|
33
|
-
|
34
|
-
name: dat-worker-pool
|
30
|
+
- 3
|
31
|
+
version: "0.3"
|
32
|
+
version_requirements: *id001
|
35
33
|
type: :runtime
|
36
|
-
|
34
|
+
name: dat-worker-pool
|
37
35
|
prerelease: false
|
38
|
-
|
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:
|
42
|
+
hash: 21
|
44
43
|
segments:
|
45
44
|
- 2
|
46
|
-
-
|
47
|
-
version: "2.
|
48
|
-
|
49
|
-
name: assert
|
45
|
+
- 11
|
46
|
+
version: "2.11"
|
47
|
+
version_requirements: *id002
|
50
48
|
type: :development
|
51
|
-
|
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.
|
97
|
+
rubygems_version: 1.8.29
|
113
98
|
signing_key:
|
114
99
|
specification_version: 3
|
115
100
|
summary: A generic threaded TCP server API
|