revactor 0.1.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/CHANGES +3 -0
- data/LICENSE +58 -0
- data/README +338 -0
- data/Rakefile +48 -0
- data/examples/echo_server.rb +39 -0
- data/examples/google.rb +24 -0
- data/examples/mongrel.rb +14 -0
- data/lib/revactor/actor.rb +316 -0
- data/lib/revactor/behaviors/server.rb +87 -0
- data/lib/revactor/filters/line.rb +53 -0
- data/lib/revactor/filters/packet.rb +59 -0
- data/lib/revactor/mongrel.rb +62 -0
- data/lib/revactor/server.rb +153 -0
- data/lib/revactor/tcp.rb +397 -0
- data/lib/revactor.rb +29 -0
- data/revactor.gemspec +28 -0
- data/spec/actor_spec.rb +127 -0
- data/spec/line_filter_spec.rb +36 -0
- data/spec/packet_filter_spec.rb +59 -0
- data/spec/tcp_spec.rb +84 -0
- data/tools/messaging_throughput.rb +33 -0
- metadata +92 -0
@@ -0,0 +1,316 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright (C)2007 Tony Arcieri
|
3
|
+
# You can redistribute this under the terms of the Ruby license
|
4
|
+
# See file LICENSE for details
|
5
|
+
#++
|
6
|
+
|
7
|
+
require File.dirname(__FILE__) + '/../revactor'
|
8
|
+
require 'fiber'
|
9
|
+
|
10
|
+
# Raised whenever any Actor-specific problems occur
|
11
|
+
class ActorError < StandardError; end
|
12
|
+
|
13
|
+
# Actors are lightweight concurrency primitives which communiucate via message
|
14
|
+
# passing. Each actor has a mailbox which it scans for matching messages.
|
15
|
+
# An actor sleeps until it receives a message, at which time it scans messages
|
16
|
+
# against its filter set and then executes appropriate callbacks.
|
17
|
+
#
|
18
|
+
# The Actor class is definined in the global scope in hopes of being generally
|
19
|
+
# useful for Ruby 1.9 users while also attempting to be as compatible as
|
20
|
+
# possible with the Omnibus and Rubinius Actor implementations. In this way it
|
21
|
+
# should be possible to run programs written using Revactor to on top of other
|
22
|
+
# Actor implementations.
|
23
|
+
#
|
24
|
+
class Actor < Fiber
|
25
|
+
include Enumerable
|
26
|
+
@@registered = {}
|
27
|
+
|
28
|
+
class << self
|
29
|
+
include Enumerable
|
30
|
+
|
31
|
+
# Create a new Actor with the given block and arguments
|
32
|
+
def new(*args, &block)
|
33
|
+
raise ArgumentError, "no block given" unless block
|
34
|
+
actor = super() do
|
35
|
+
block.call(*args)
|
36
|
+
Actor.current.instance_eval { @_dead = true }
|
37
|
+
end
|
38
|
+
|
39
|
+
# For whatever reason #initialize is never called in subclasses of Fiber
|
40
|
+
actor.instance_eval do
|
41
|
+
@_dead = false
|
42
|
+
@_mailbox = Mailbox.new
|
43
|
+
@_dictionary = {}
|
44
|
+
end
|
45
|
+
|
46
|
+
Scheduler << actor
|
47
|
+
actor
|
48
|
+
end
|
49
|
+
|
50
|
+
alias_method :spawn, :new
|
51
|
+
|
52
|
+
# This will be defined differently in the future, but now the two are the same
|
53
|
+
alias_method :start, :new
|
54
|
+
|
55
|
+
# Obtain a handle to the current Actor
|
56
|
+
def current
|
57
|
+
actor = super
|
58
|
+
raise ActorError, "current fiber is not an actor" unless actor.is_a? Actor
|
59
|
+
|
60
|
+
actor
|
61
|
+
end
|
62
|
+
|
63
|
+
# Wait for messages matching a given filter. The filter object is yielded
|
64
|
+
# to be block passed to receive. You can then invoke the when argument
|
65
|
+
# which takes a parameter and a block. Messages are compared (using ===)
|
66
|
+
# against the parameter. The Case gem includes several tools for matching
|
67
|
+
# messages using ===
|
68
|
+
#
|
69
|
+
# The first filter to match a message in the mailbox is executed. If no
|
70
|
+
# filters match then the actor sleeps.
|
71
|
+
def receive(&filter)
|
72
|
+
unless current.is_a?(Actor)
|
73
|
+
raise ActorError, "receive must be called in the context of an Actor"
|
74
|
+
end
|
75
|
+
|
76
|
+
current.__send__(:_mailbox).receive(&filter)
|
77
|
+
end
|
78
|
+
|
79
|
+
# Look up an actor in the global dictionary
|
80
|
+
def [](key)
|
81
|
+
@@registered[key]
|
82
|
+
end
|
83
|
+
|
84
|
+
# Register this actor in the global dictionary
|
85
|
+
def []=(key, actor)
|
86
|
+
unless actor.is_a?(Actor)
|
87
|
+
raise ArgumentError, "only actors may be registered"
|
88
|
+
end
|
89
|
+
|
90
|
+
@@registered[key] = actor
|
91
|
+
end
|
92
|
+
|
93
|
+
# Delete an actor from the global dictionary
|
94
|
+
def delete(key, &block)
|
95
|
+
@@registered.delete(key, &block)
|
96
|
+
end
|
97
|
+
|
98
|
+
# Iterate over the actors in the global dictionary
|
99
|
+
def each(&block)
|
100
|
+
@@registered.each(&block)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
# Look up value in the actor's dictionary
|
105
|
+
def [](key)
|
106
|
+
@_dictionary[key]
|
107
|
+
end
|
108
|
+
|
109
|
+
# Store a value in the actor's dictionary
|
110
|
+
def []=(key, value)
|
111
|
+
@_dictionary[key] = value
|
112
|
+
end
|
113
|
+
|
114
|
+
# Delete a value from the actor's dictionary
|
115
|
+
def delete(key, &block)
|
116
|
+
@_dictionary.delete(key, &block)
|
117
|
+
end
|
118
|
+
|
119
|
+
# Iterate over values in the actor's dictionary
|
120
|
+
def each(&block)
|
121
|
+
@_dictionary.each(&block)
|
122
|
+
end
|
123
|
+
|
124
|
+
# Is the current actor dead?
|
125
|
+
def dead?; @_dead; end
|
126
|
+
|
127
|
+
# Send a message to an actor
|
128
|
+
def <<(message)
|
129
|
+
# Erlang discards messages sent to dead actors, and if Erlang does it,
|
130
|
+
# it must be the right thing to do, right? Hooray for the Erlang
|
131
|
+
# cargo cult! I think they do this because dealing with errors raised
|
132
|
+
# from dead actors complicates overall error handling too much to be worth it.
|
133
|
+
return message if dead?
|
134
|
+
|
135
|
+
@_mailbox << message
|
136
|
+
Scheduler << self
|
137
|
+
message
|
138
|
+
end
|
139
|
+
|
140
|
+
alias_method :send, :<<
|
141
|
+
|
142
|
+
#########
|
143
|
+
protected
|
144
|
+
#########
|
145
|
+
|
146
|
+
attr_reader :_mailbox
|
147
|
+
|
148
|
+
# The Actor Scheduler maintains a run queue of actors with outstanding
|
149
|
+
# messages who have not yet processed their mailbox. If all actors have
|
150
|
+
# processed their mailboxes then the scheduler waits for any outstanding
|
151
|
+
# Rev events. If there are no active Rev watchers then the scheduler exits.
|
152
|
+
class Scheduler
|
153
|
+
@@queue = []
|
154
|
+
@@running = false
|
155
|
+
|
156
|
+
class << self
|
157
|
+
# Schedule an Actor to be executed, and run the scheduler if it isn't
|
158
|
+
# currently running
|
159
|
+
def <<(actor)
|
160
|
+
@@queue << actor
|
161
|
+
run unless @@running
|
162
|
+
end
|
163
|
+
|
164
|
+
# Run the scheduler
|
165
|
+
def run
|
166
|
+
return if @@running
|
167
|
+
@@running = true
|
168
|
+
default_loop = Rev::Loop.default
|
169
|
+
|
170
|
+
until @@queue.empty? and default_loop.watchers.empty?
|
171
|
+
@@queue.each do |actor|
|
172
|
+
begin
|
173
|
+
actor.resume
|
174
|
+
rescue FiberError # Fiber may have died since being scheduled
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
@@queue.clear
|
179
|
+
|
180
|
+
default_loop.run_once unless default_loop.watchers.empty?
|
181
|
+
end
|
182
|
+
|
183
|
+
@@running = false
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
# Actor mailbox. For purposes of efficiency the mailbox also handles
|
189
|
+
# suspending and resuming an actor when no messages match its filter set.
|
190
|
+
class Mailbox
|
191
|
+
attr_accessor :timer
|
192
|
+
attr_accessor :timed_out
|
193
|
+
attr_accessor :timeout_action
|
194
|
+
|
195
|
+
def initialize
|
196
|
+
@timer = nil
|
197
|
+
@queue = []
|
198
|
+
end
|
199
|
+
|
200
|
+
# Add a message to the mailbox queue
|
201
|
+
def <<(message)
|
202
|
+
@queue << message
|
203
|
+
end
|
204
|
+
|
205
|
+
# Attempt to receive a message
|
206
|
+
def receive
|
207
|
+
raise ArgumentError, "no filter block given" unless block_given?
|
208
|
+
|
209
|
+
# Clear mailbox processing variables
|
210
|
+
action = matched_index = nil
|
211
|
+
processed_upto = 0
|
212
|
+
|
213
|
+
# Clear timeout variables
|
214
|
+
@timed_out = false
|
215
|
+
@timeout_action = nil
|
216
|
+
|
217
|
+
# Build the filter
|
218
|
+
filter = Filter.new(self)
|
219
|
+
yield filter
|
220
|
+
raise ArgumentError, "empty filter" if filter.empty?
|
221
|
+
|
222
|
+
# Process incoming messages
|
223
|
+
while action.nil?
|
224
|
+
@queue[processed_upto..@queue.size].each_with_index do |message, index|
|
225
|
+
unless (action = filter.match message)
|
226
|
+
# The filter did not match an action for the current message
|
227
|
+
# Keep track of which messages we've ran the filter across so it doesn't
|
228
|
+
# get run against messages it already failed to match
|
229
|
+
processed_upto += 1
|
230
|
+
next
|
231
|
+
end
|
232
|
+
|
233
|
+
# We've found a matching action, so break out of the loop
|
234
|
+
matched_index = processed_upto + index
|
235
|
+
break
|
236
|
+
end
|
237
|
+
|
238
|
+
# If we've timed out, run the timeout action unless another has been found
|
239
|
+
action ||= @timeout_action if @timed_out
|
240
|
+
|
241
|
+
# If we didn't find a matching action, yield until we get another message
|
242
|
+
Actor.yield unless action
|
243
|
+
end
|
244
|
+
|
245
|
+
if @timer
|
246
|
+
@timer.detach if @timer.attached?
|
247
|
+
@timer = nil
|
248
|
+
end
|
249
|
+
|
250
|
+
# If we encountered a timeout, call the action directly
|
251
|
+
return action.call if @timed_out
|
252
|
+
|
253
|
+
# Otherwise we matched a message, so process it with the action
|
254
|
+
return action.(@queue.delete_at matched_index)
|
255
|
+
end
|
256
|
+
|
257
|
+
# Timeout class, used to implement receive timeouts
|
258
|
+
class Timer < Rev::TimerWatcher
|
259
|
+
def initialize(timeout, actor)
|
260
|
+
@actor = actor
|
261
|
+
super(timeout)
|
262
|
+
end
|
263
|
+
|
264
|
+
def on_timer
|
265
|
+
@actor.instance_eval { @_mailbox.timed_out = true }
|
266
|
+
Scheduler << @actor
|
267
|
+
end
|
268
|
+
end
|
269
|
+
|
270
|
+
# Mailbox filterset. Takes patterns or procs to match messages with
|
271
|
+
# and returns the associated proc when a pattern matches.
|
272
|
+
class Filter
|
273
|
+
def initialize(mailbox)
|
274
|
+
@mailbox = mailbox
|
275
|
+
@ruleset = []
|
276
|
+
end
|
277
|
+
|
278
|
+
# Provide a pattern to match against with === and a block to call
|
279
|
+
# when the pattern is matched.
|
280
|
+
def when(pattern, &action)
|
281
|
+
raise ArgumentError, "no block given" unless action
|
282
|
+
@ruleset << [pattern, action]
|
283
|
+
end
|
284
|
+
|
285
|
+
# Provide a timeout (in seconds, can be a Float) to wait for matching
|
286
|
+
# messages. If the timeout elapses, the given block is called.
|
287
|
+
def after(timeout, &action)
|
288
|
+
raise ArgumentError, "timeout already specified" if @mailbox.timer
|
289
|
+
raise ArgumentError, "must be zero or positive" if timeout < 0
|
290
|
+
|
291
|
+
# Don't explicitly require an action to be specified for a timeout
|
292
|
+
@mailbox.timeout_action = action || proc {}
|
293
|
+
|
294
|
+
if timeout > 0
|
295
|
+
@mailbox.timer = Timer.new(timeout, Actor.current).attach(Rev::Loop.default)
|
296
|
+
else
|
297
|
+
# No need to actually set a timer if the timeout is zero,
|
298
|
+
# just short-circuit waiting for one entirely...
|
299
|
+
@timed_out = true
|
300
|
+
Scheduler << self
|
301
|
+
end
|
302
|
+
end
|
303
|
+
|
304
|
+
# Match a message using the filter
|
305
|
+
def match(message)
|
306
|
+
_, action = @ruleset.find { |pattern, _| pattern === message }
|
307
|
+
action
|
308
|
+
end
|
309
|
+
|
310
|
+
# Is the filterset empty?
|
311
|
+
def empty?
|
312
|
+
@ruleset.empty?
|
313
|
+
end
|
314
|
+
end
|
315
|
+
end
|
316
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright (C)2007 Tony Arcieri
|
3
|
+
# You can redistribute this under the terms of the Ruby license
|
4
|
+
# See file LICENSE for details
|
5
|
+
#++
|
6
|
+
|
7
|
+
module Revactor
|
8
|
+
module Behavior
|
9
|
+
# The Server behavior provides a callback-driven class which eases the
|
10
|
+
# creation of standard synchronous "blocking" calls by abstracting away
|
11
|
+
# inter-Actor communication and also providing baked-in state management.
|
12
|
+
#
|
13
|
+
# This behavior module provides the base set of callbacks necessary
|
14
|
+
# to implement the behavior. It also provides descriptions for what
|
15
|
+
# certain callback methods should do.
|
16
|
+
#
|
17
|
+
# When used properly, the server behavior can implement transactional
|
18
|
+
# semantics, ensuring only successful calls mutate the previous state
|
19
|
+
# and erroneous/exception-raising ones do not.
|
20
|
+
#
|
21
|
+
# The design is modeled off Erlang/OTP's gen_server
|
22
|
+
module Server
|
23
|
+
# Initialize the server state. Can return:
|
24
|
+
#
|
25
|
+
# start(*args)
|
26
|
+
# -> [:ok, state]
|
27
|
+
# -> [:ok, state, timeout]
|
28
|
+
# -> [:stop, reason]
|
29
|
+
#
|
30
|
+
# The state variable allows you to provide a set of state whose mutation
|
31
|
+
# can be controlled a lot more closely than is possible with standard
|
32
|
+
# object oriented behavior. The latest version of state is passed
|
33
|
+
# to all Revactor::Server callbacks and is only mutated upon a
|
34
|
+
# successful return (unless an exception was raised)
|
35
|
+
#
|
36
|
+
def start(*args)
|
37
|
+
return :ok
|
38
|
+
end
|
39
|
+
|
40
|
+
# Handle any calls made to a Reactor::Server object, which are captured
|
41
|
+
# via method_missing and dispatched here. Calls provide synchronous
|
42
|
+
# behavior: the callee will block until this method completss and a
|
43
|
+
# reply is sent back to them. Can return:
|
44
|
+
#
|
45
|
+
# handle_call(message, from, state)
|
46
|
+
# -> [:reply, reply, new_state]
|
47
|
+
# -> [:reply, reply, new_state, timeout]
|
48
|
+
# -> [:noreply, new_state]
|
49
|
+
# -> [:noreply, new_state, timeout]
|
50
|
+
# -> [:stop, reason, reply, new_state]
|
51
|
+
#
|
52
|
+
def handle_call(message, from, state)
|
53
|
+
return :reply, :ok, state
|
54
|
+
end
|
55
|
+
|
56
|
+
# Handle calls without return values
|
57
|
+
#
|
58
|
+
# handle_cast(message, state)
|
59
|
+
# -> [:noreply, new_state]
|
60
|
+
# -> [:noreply, new_state, timeout]
|
61
|
+
# -> [:stop, reason, new_state]
|
62
|
+
#
|
63
|
+
def handle_cast(message, state)
|
64
|
+
return :noreply, state
|
65
|
+
end
|
66
|
+
|
67
|
+
# Handle any spontaneous messages to the server which are not calls
|
68
|
+
# or casts made from Rev::Server. Can return:
|
69
|
+
#
|
70
|
+
# handle_info(info, state)
|
71
|
+
# -> [:noreply, new_state]
|
72
|
+
# -> [:noreply, new_state, timeout]
|
73
|
+
# -> [:stop, reason, new_state]
|
74
|
+
#
|
75
|
+
def handle_info(info, state)
|
76
|
+
return :noreply, state
|
77
|
+
end
|
78
|
+
|
79
|
+
# Method called when the server is about to terminate, for example when
|
80
|
+
# any of the handle_* routines above return :stop. The return value of
|
81
|
+
# terminate is discarded.
|
82
|
+
#
|
83
|
+
def terminate(reason, state)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright (C)2007 Tony Arcieri
|
3
|
+
# You can redistribute this under the terms of the Ruby license
|
4
|
+
# See file LICENSE for details
|
5
|
+
#++
|
6
|
+
|
7
|
+
module Revactor
|
8
|
+
module Filter
|
9
|
+
# A filter for line based protocols which are framed using LF or CRLF
|
10
|
+
# encoding, such as IRC. Both LF and CRLF are supported and no
|
11
|
+
# validation is done on bare LFs for CRLF encoding. The output
|
12
|
+
# is chomped and delivered without any newline.
|
13
|
+
class Line
|
14
|
+
MAX_LENGTH = 1048576 # Maximum length of a single line
|
15
|
+
|
16
|
+
# Create a new Line filter. Accepts the following options:
|
17
|
+
#
|
18
|
+
# delimiter: A character to use as a delimiter. Defaults to "\n"
|
19
|
+
# Character sequences are not supported.
|
20
|
+
#
|
21
|
+
# maxlength: Maximum length of a line
|
22
|
+
#
|
23
|
+
def initialize(options = {})
|
24
|
+
@input = ''
|
25
|
+
@delimiter = options[:delimiter] || "\n"
|
26
|
+
@size_limit = options[:maxlength] || MAX_LENGTH
|
27
|
+
end
|
28
|
+
|
29
|
+
# Callback for processing incoming lines
|
30
|
+
def decode(data)
|
31
|
+
lines = data.split @delimiter, -1
|
32
|
+
|
33
|
+
if @size_limit and @input.size + lines.first.size > @size_limit
|
34
|
+
raise 'input buffer full'
|
35
|
+
end
|
36
|
+
|
37
|
+
@input << lines.shift
|
38
|
+
return [] if lines.empty?
|
39
|
+
|
40
|
+
lines.unshift @input
|
41
|
+
@input = lines.pop
|
42
|
+
|
43
|
+
lines.map(&:chomp)
|
44
|
+
end
|
45
|
+
|
46
|
+
# Encode lines using the current delimiter
|
47
|
+
def encode(*data)
|
48
|
+
data.reduce("") { |str, d| str << d << @delimiter }
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
@@ -0,0 +1,59 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright (C)2007 Tony Arcieri
|
3
|
+
# You can redistribute this under the terms of the Ruby license
|
4
|
+
# See file LICENSE for details
|
5
|
+
#++
|
6
|
+
|
7
|
+
# Use buffering from Rev
|
8
|
+
require 'rubygems'
|
9
|
+
require 'rev'
|
10
|
+
|
11
|
+
module Revactor
|
12
|
+
module Filter
|
13
|
+
# A filter for "packet" protocols which are framed using a fix-sized
|
14
|
+
# length prefix followed by a message body, such as DRb. Either 16-bit
|
15
|
+
# or 32-bit prefixes are supported.
|
16
|
+
class Packet
|
17
|
+
def initialize(size = 2)
|
18
|
+
unless size == 2 or size == 4
|
19
|
+
raise ArgumentError, 'only 2 or 4 byte prefixes are supported'
|
20
|
+
end
|
21
|
+
|
22
|
+
@prefix_size = size
|
23
|
+
@data_size = 0
|
24
|
+
|
25
|
+
@mode = :prefix
|
26
|
+
@buffer = Rev::Buffer.new
|
27
|
+
end
|
28
|
+
|
29
|
+
# Callback for processing incoming frames
|
30
|
+
def decode(data)
|
31
|
+
received = []
|
32
|
+
@buffer << data
|
33
|
+
|
34
|
+
begin
|
35
|
+
if @mode == :prefix
|
36
|
+
break if @buffer.size < @prefix_size
|
37
|
+
prefix = @buffer.read @prefix_size
|
38
|
+
@data_size = prefix.unpack(@prefix_size == 2 ? 'n' : 'N').first
|
39
|
+
@mode = :data
|
40
|
+
end
|
41
|
+
|
42
|
+
break if @buffer.size < @data_size
|
43
|
+
received << @buffer.read(@data_size)
|
44
|
+
@mode = :prefix
|
45
|
+
end until @buffer.empty?
|
46
|
+
|
47
|
+
received
|
48
|
+
end
|
49
|
+
|
50
|
+
# Send a packet with a specified size prefix
|
51
|
+
def encode(*data)
|
52
|
+
data.reduce('') do |s, d|
|
53
|
+
raise ArgumentError, 'packet too long for prefix length' if d.size >= 256 ** @prefix_size
|
54
|
+
s << [d.size].pack(@prefix_size == 2 ? 'n' : 'N') << d
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../revactor'
|
2
|
+
require 'rubygems'
|
3
|
+
require 'mongrel'
|
4
|
+
|
5
|
+
class Revactor::TCP::Socket
|
6
|
+
# Monkeypatched readpartial routine inserted whenever Revactor's mongrel.rb
|
7
|
+
# is loaded. The value passed to this method is ignored, so it is not
|
8
|
+
# fully compatible with Socket's readpartial method.
|
9
|
+
#
|
10
|
+
# Mongrel doesn't really care if we read more than Const::CHUNK_SIZE
|
11
|
+
# and readpartial doesn't really make sense in Revactor's API since
|
12
|
+
# read accomplishes the same functionality. So, in this implementation
|
13
|
+
# readpartial just calls read and returns whatever is available.
|
14
|
+
def readpartial(value = nil)
|
15
|
+
read
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
module Mongrel
|
20
|
+
# Mongrel's HttpServer, monkeypatched to run on top of Revactor and using
|
21
|
+
# Actors for concurrency.
|
22
|
+
class HttpServer
|
23
|
+
def initialize(host, port, num_processors=950, throttle=0, timeout=60)
|
24
|
+
@socket = Revactor::TCP.listen(host, port)
|
25
|
+
@classifier = URIClassifier.new
|
26
|
+
@host = host
|
27
|
+
@port = port
|
28
|
+
@throttle = throttle
|
29
|
+
@num_processors = num_processors
|
30
|
+
@timeout = timeout
|
31
|
+
end
|
32
|
+
|
33
|
+
# Runs the thing. It returns the Actor the listener is running in.
|
34
|
+
def run
|
35
|
+
@acceptor = Actor.new do
|
36
|
+
begin
|
37
|
+
while true
|
38
|
+
begin
|
39
|
+
client = @socket.accept
|
40
|
+
actor = Actor.new client, &method(:process_client)
|
41
|
+
actor[:started_on] = Time.now
|
42
|
+
rescue StopServer
|
43
|
+
break
|
44
|
+
rescue Errno::ECONNABORTED
|
45
|
+
# client closed the socket even before accept
|
46
|
+
client.close rescue nil
|
47
|
+
rescue Object => e
|
48
|
+
STDERR.puts "#{Time.now}: Unhandled listen loop exception #{e.inspect}."
|
49
|
+
STDERR.puts e.backtrace.join("\n")
|
50
|
+
end
|
51
|
+
end
|
52
|
+
graceful_shutdown
|
53
|
+
ensure
|
54
|
+
@socket.close
|
55
|
+
# STDERR.puts "#{Time.now}: Closed socket."
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
return @acceptor
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|