revactor 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|