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.
@@ -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