revactor 0.1.0

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