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,153 @@
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
+
9
+ module Revactor
10
+ # Revactor::Server wraps an Actor's receive loop and issues callbacks to
11
+ # a class which implements Revactor::Behaviors::Server. It eases the
12
+ # creation of standard synchronous "blocking" calls by abstracting away
13
+ # inter-Actor communication and also providing baked-in state management.
14
+ #
15
+ # When used properly, Revactor::Server can implement transactional
16
+ # semantics, ensuring only successful calls mutate the previous state
17
+ # and erroneous/exception-raising ones do not.
18
+ #
19
+ # The design is modeled off Erlang/OTP's gen_server
20
+ class Server
21
+ # How long to wait for a response to a call before timing out
22
+ # This value also borrowed from Erlang. More cargo culting!
23
+ DEFAULT_CALL_TIMEOUT = 5
24
+
25
+ # Create a new server. Accepts the following options:
26
+ #
27
+ # register: Register the Actor in the Actor registry under
28
+ # the given term
29
+ #
30
+ # Any options passed after the options hash are passed to the
31
+ # start method of the given object.
32
+ #
33
+ def initialize(obj, options = {}, *args)
34
+ @obj = obj
35
+ @timeout = nil
36
+ @state = obj.start(*args)
37
+ @actor = Actor.new(&method(:start).to_proc)
38
+
39
+ Actor[options[:register]] = @actor if options[:register]
40
+ end
41
+
42
+ # Call the server with the given message
43
+ def call(message, options = {})
44
+ options[:timeout] ||= DEFAULT_CALL_TIMEOUT
45
+
46
+ @actor << T[:call, Actor.current, message]
47
+ Actor.receive do |filter|
48
+ filter.when(Case[:call_reply, @actor, Object]) { |_, _, reply| reply }
49
+ filter.when(Case[:call_error, @actor, Object]) { |_, _, ex| raise ex }
50
+ filter.after(options[:timeout]) { raise 'timeout' }
51
+ end
52
+ end
53
+
54
+ # Send a cast to the server
55
+ def cast(message)
56
+ @actor << T[:cast, message]
57
+ message
58
+ end
59
+
60
+ #########
61
+ protected
62
+ #########
63
+
64
+ # Start the server
65
+ def start
66
+ @running = true
67
+ while @running do
68
+ Actor.receive do |filter|
69
+ filter.when(Object) { |message| handle_message(message) }
70
+ filter.after(@timeout) { stop(:timeout) } if @timeout
71
+ end
72
+ end
73
+ end
74
+
75
+ # Dispatch the incoming message to the appropriate handler
76
+ def handle_message(message)
77
+ case message.first
78
+ when :call then handle_call(message)
79
+ when :cast then handle_cast(message)
80
+ else handle_info(message)
81
+ end
82
+ end
83
+
84
+ # Wrapper for calling the provided object's handle_call method
85
+ def handle_call(message)
86
+ _, from, body = message
87
+
88
+ begin
89
+ result = @obj.handle_call(body, from, @state)
90
+ case result.first
91
+ when :reply
92
+ _, reply, @state, @timeout = result
93
+ from << T[:call_reply, Actor.current, reply]
94
+ when :noreply
95
+ _, @state, @timeout = result
96
+ when :stop
97
+ _, reason, @state = result
98
+ stop(reason)
99
+ end
100
+ rescue Exception => ex
101
+ log_exception(ex)
102
+ from << T[:call_error, Actor.current, ex]
103
+ end
104
+ end
105
+
106
+ # Wrapper for calling the provided object's handle_cast method
107
+ def handle_cast(message)
108
+ _, body = message
109
+
110
+ begin
111
+ result = @obj.handle_cast(body, @state)
112
+ case result.first
113
+ when :noreply
114
+ _, @state, @timeout = result
115
+ when :stop
116
+ _, reason, @state = result
117
+ stop(reason)
118
+ end
119
+ rescue Exception => e
120
+ log_exception(e)
121
+ end
122
+ end
123
+
124
+ # Wrapper for calling the provided object's handle_info method
125
+ def handle_info(message)
126
+ begin
127
+ result = @obj.handle_info(message, @state)
128
+ case result.first
129
+ when :noreply
130
+ _, @state, @timeout = result
131
+ when :stop
132
+ _, reason, @state = result
133
+ stop(reason)
134
+ end
135
+ rescue Exception => e
136
+ log_exception(e)
137
+ end
138
+ end
139
+
140
+ # Stop the server
141
+ def stop(reason)
142
+ @running = false
143
+ @obj.terminate(reason, @state)
144
+ end
145
+
146
+ # Log an exception
147
+ def log_exception(exception)
148
+ # FIXME this should really go to a logger, not STDERR
149
+ STDERR.puts "Rev::Server exception: #{exception}"
150
+ STDERR.puts exception.backtrace
151
+ end
152
+ end
153
+ end
@@ -0,0 +1,397 @@
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
+
9
+ module Revactor
10
+ # The TCP module holds all Revactor functionality related to the
11
+ # Transmission Control Protocol, including drop-in replacements
12
+ # for Ruby TCP Sockets which can operate concurrently using Actors.
13
+ module TCP
14
+ # Number of seconds to wait for a connection
15
+ CONNECT_TIMEOUT = 10
16
+
17
+ # Raised when a connection to a remote server fails
18
+ class ConnectError < StandardError; end
19
+
20
+ # Raised when hostname resolution for a remote server fails
21
+ class ResolveError < ConnectError; end
22
+
23
+ # Connect to the specified host and port. Host may be a domain name
24
+ # or IP address. Accepts the following options:
25
+ #
26
+ # :active - Controls how data is read from the socket. See the
27
+ # documentation for Revactor::TCP::Socket#active=
28
+ #
29
+ def self.connect(host, port, options = {})
30
+ socket = Socket.connect host, port, options
31
+ socket.attach Rev::Loop.default
32
+
33
+ Actor.receive do |filter|
34
+ filter.when(Case[Object, socket]) do |message|
35
+ case message[0]
36
+ when :tcp_connected
37
+ return socket
38
+ when :tcp_connect_failed
39
+ raise ConnectError, "connection refused"
40
+ when :tcp_resolve_failed
41
+ raise ResolveError, "couldn't resolve #{host}"
42
+ else raise "unexpected message for #{socket.inspect}: #{message.first}"
43
+ end
44
+ end
45
+
46
+ filter.after(CONNECT_TIMEOUT) do
47
+ raise ConnectError, "connection timed out"
48
+ end
49
+ end
50
+ end
51
+
52
+ # Listen on the specified address and port. Accepts the following options:
53
+ #
54
+ # :active - Default active setting for new connections. See the
55
+ # documentation Rev::TCP::Socket#active= for more info
56
+ #
57
+ # :controller - The controlling actor, default Actor.current
58
+ #
59
+ def self.listen(addr, port, options = {})
60
+ Listener.new(addr, port, options).attach(Rev::Loop.default).disable
61
+ end
62
+
63
+ # TCP socket class, returned by Revactor::TCP.connect and
64
+ # Revactor::TCP::Listener#accept
65
+ class Socket < Rev::TCPSocket
66
+ attr_reader :active
67
+ attr_reader :controller
68
+
69
+ class << self
70
+ # Connect to the specified host and port. Host may be a domain name
71
+ # or IP address. Accepts the following options:
72
+ #
73
+ # :active - Controls how data is read from the socket. See the
74
+ # documentation for #active=
75
+ #
76
+ # :controller - The controlling actor, default Actor.current
77
+ #
78
+ # :filter - An symbol/class or array of symbols/classes which implement
79
+ # #encode and #decode methods to transform data sent and
80
+ # received data respectively via Revactor::TCP::Socket.
81
+ #
82
+ def connect(host, port, options = {})
83
+ options[:active] ||= false
84
+ options[:controller] ||= Actor.current
85
+
86
+ super(host, port, options).instance_eval {
87
+ @active, @controller = options[:active], options[:controller]
88
+ @filterset = initialize_filter(*options[:filter])
89
+ self
90
+ }
91
+ end
92
+ end
93
+
94
+ def initialize(socket, options = {})
95
+ super(socket)
96
+
97
+ @active ||= options[:active] || false
98
+ @controller ||= options[:controller] || Actor.current
99
+ @filterset ||= initialize_filter(*options[:filter])
100
+
101
+ @receiver = @controller
102
+ @read_buffer = Rev::Buffer.new
103
+ end
104
+
105
+ # Enable or disable active mode data reception. State can be any
106
+ # of the following:
107
+ #
108
+ # true - All received data is sent to the controlling actor
109
+ # false - Receiving data is disabled
110
+ # :once - A single message will be sent to the controlling actor
111
+ # then active mode will be disabled
112
+ def active=(state)
113
+ unless @receiver == @controller
114
+ raise "cannot change active state during a synchronous call"
115
+ end
116
+
117
+ unless [true, false, :once].include? state
118
+ raise ArgumentError, "must be true, false, or :once"
119
+ end
120
+
121
+ if [true, :once].include?(state)
122
+ unless @read_buffer.empty?
123
+ @receiver << [:tcp, self, @read_buffer.read]
124
+ return if state == :once
125
+ end
126
+
127
+ enable unless enabled?
128
+ end
129
+
130
+ @active = state
131
+ end
132
+
133
+ # Set the controlling actor
134
+ def controller=(controller)
135
+ raise ArgumentError, "controller must be an actor" unless controller.is_a? Actor
136
+
137
+ @receiver = controller if @receiver == @controller
138
+ @controller = controller
139
+ end
140
+
141
+ # Read data from the socket synchronously. If a length is specified
142
+ # then the call blocks until the given length has been read. Otherwise
143
+ # the call blocks until it receives any data.
144
+ def read(length = nil)
145
+ # Only one synchronous call allowed at a time
146
+ raise "already being called synchronously" unless @receiver == @controller
147
+
148
+ unless @read_buffer.empty? or (length and @read_buffer.size < length)
149
+ return @read_buffer.read(length)
150
+ end
151
+
152
+ active = @active
153
+ @active = :once
154
+ @receiver = Actor.current
155
+ enable unless enabled?
156
+
157
+ loop do
158
+ Actor.receive do |filter|
159
+ filter.when(Case[:tcp, self, Object]) do |_, _, data|
160
+ if length.nil?
161
+ @receiver = @controller
162
+ @active = active
163
+ enable if @active
164
+
165
+ return data
166
+ end
167
+
168
+ @read_buffer << data
169
+
170
+ if @read_buffer.size >= length
171
+ @receiver = @controller
172
+ @active = active
173
+ enable if @active
174
+
175
+ return @read_buffer.read(length)
176
+ end
177
+ end
178
+
179
+ filter.when(Case[:tcp_closed, self]) do
180
+ unless @receiver == @controller
181
+ @receiver = @controller
182
+ @receiver << T[:tcp_closed, self]
183
+ end
184
+
185
+ raise EOFError, "connection closed"
186
+ end
187
+ end
188
+ end
189
+ end
190
+
191
+ # Write data to the socket. The call blocks until all data has been written.
192
+ def write(data)
193
+ # Only one synchronous call allowed at a time
194
+ raise "already being called synchronously" unless @receiver == @controller
195
+
196
+ active = @active
197
+ @active = false
198
+ @receiver = Actor.current
199
+ disable if @active
200
+
201
+ super(encode(data))
202
+
203
+ Actor.receive do |filter|
204
+ filter.when(Case[:tcp_write_complete, self]) do
205
+ @receiver = @controller
206
+ @active = active
207
+ enable if @active
208
+
209
+ return data.size
210
+ end
211
+
212
+ filter.when(Case[:tcp_closed, self]) do
213
+ @receiver = @controller
214
+ @active = active
215
+ enable if @active
216
+
217
+ raise EOFError, "connection closed"
218
+ end
219
+ end
220
+ end
221
+
222
+ alias_method :<<, :write
223
+
224
+ #########
225
+ protected
226
+ #########
227
+
228
+ #
229
+ # Filter setup
230
+ #
231
+
232
+ # Initialize filter change
233
+ def initialize_filter(*filterset)
234
+ return filterset if filterset.empty?
235
+
236
+ filterset.map do |filter|
237
+ case filter
238
+ when Array
239
+ name = filter.shift
240
+ case name
241
+ when Class
242
+ name.new(*filter)
243
+ when Symbol
244
+ symbol_to_filter(name).new(*filter)
245
+ else raise ArgumentError, "unrecognized filter type: #{name.class}"
246
+ end
247
+ when Class
248
+ filter.new
249
+ when Symbol
250
+ symbol_to_filter(filter).new
251
+ end
252
+ end
253
+ end
254
+
255
+ # Lookup filters referenced as symbols
256
+ def symbol_to_filter(filter)
257
+ case filter
258
+ when :line then Revactor::Filters::Line
259
+ when :packet then Revactor::Filters::Packet
260
+ else raise ArgumentError, "unrecognized filter type: #{filter}"
261
+ end
262
+ end
263
+
264
+ # Decode data through the filter chain
265
+ def decode(data)
266
+ @filterset.reduce([data]) do |a, filter|
267
+ a.reduce([]) do |a2, d|
268
+ a2 + filter.decode(d)
269
+ end
270
+ end
271
+ end
272
+
273
+ # Encode data through the filter chain
274
+ def encode(message)
275
+ @filterset.reverse.reduce(message) { |m, filter| filter.encode(*m) }
276
+ end
277
+
278
+ #
279
+ # Rev::TCPSocket callback
280
+ #
281
+
282
+ def on_connect
283
+ @receiver << T[:tcp_connected, self]
284
+ end
285
+
286
+ def on_connect_failed
287
+ @receiver << T[:tcp_connect_failed, self]
288
+ end
289
+
290
+ def on_resolve_failed
291
+ @receiver << T[:tcp_resolve_failed, self]
292
+ end
293
+
294
+ def on_close
295
+ @receiver << T[:tcp_closed, self]
296
+ end
297
+
298
+ def on_read(data)
299
+ # Run incoming message through the filter chain
300
+ message = decode(data)
301
+
302
+ if message.is_a?(Array) and not message.empty?
303
+ message.each { |msg| @receiver << T[:tcp, self, msg] }
304
+ elsif message
305
+ @receiver << T[:tcp, self, message]
306
+ else return
307
+ end
308
+
309
+ if @active == :once
310
+ @active = false
311
+ disable
312
+ end
313
+ end
314
+
315
+ def on_write_complete
316
+ @receiver << T[:tcp_write_complete, self]
317
+ end
318
+ end
319
+
320
+ # TCP Listener returned from Revactor::TCP.listen
321
+ class Listener < Rev::TCPListener
322
+ attr_reader :active
323
+ attr_reader :controller
324
+
325
+ # Listen on the specified address and port. Accepts the following options:
326
+ #
327
+ # :active - Default active setting for new connections. See the
328
+ # documentation Rev::TCP::Socket#active= for more info
329
+ #
330
+ # :controller - The controlling actor, default Actor.current
331
+ #
332
+ def initialize(host, port, options = {})
333
+ super(host, port)
334
+ opts = {
335
+ active: false,
336
+ controller: Actor.current
337
+ }.merge(options)
338
+
339
+ @active, @controller = opts[:active], opts[:controller]
340
+ @filterset = options[:filter]
341
+
342
+ @accepting = false
343
+ end
344
+
345
+ # Change the default active setting for newly accepted connections
346
+ def active=(state)
347
+ unless [true, false, :once].include? state
348
+ raise ArgumentError, "must be true, false, or :once"
349
+ end
350
+
351
+ @active = state
352
+ end
353
+
354
+ # Change the default controller for newly accepted connections
355
+ def controller=(controller)
356
+ raise ArgumentError, "controller must be an actor" unless controller.is_a? Actor
357
+ @controller = controller
358
+ end
359
+
360
+ # Accept an incoming connection
361
+ def accept
362
+ raise "another actor is already accepting" if @accepting
363
+
364
+ @accepting = true
365
+ @receiver = Actor.current
366
+ enable
367
+
368
+ Actor.receive do |filter|
369
+ filter.when(Case[:tcp_connection, self, Object]) do |_, _, sock|
370
+ @accepting = false
371
+ return sock
372
+ end
373
+ end
374
+ end
375
+
376
+ #########
377
+ protected
378
+ #########
379
+
380
+ #
381
+ # Rev::TCPListener callbacks
382
+ #
383
+
384
+ def on_connection(socket)
385
+ sock = Socket.new(socket,
386
+ :controller => @controller,
387
+ :active => @active,
388
+ :filter => @filterset
389
+ )
390
+ sock.attach(evloop)
391
+
392
+ @receiver << T[:tcp_connection, self, sock]
393
+ disable
394
+ end
395
+ end
396
+ end
397
+ end
data/lib/revactor.rb ADDED
@@ -0,0 +1,29 @@
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 'rubygems'
8
+ require 'rev'
9
+ require 'case'
10
+
11
+ # This is mostly in hopes of a bright future with Rubinius
12
+ # The recommended container for all datagrams sent between
13
+ # Actors is a Tuple, defined below, and with a 'T' shortcut:
14
+ unless defined? Tuple
15
+ # A Tuple class. Will (eventually) be a subset of Array
16
+ # with fixed size and faster performance, at least that's
17
+ # the hope with Rubinius...
18
+ class Tuple < Array; end
19
+ end
20
+
21
+ # Shortcut Tuple as T
22
+ T = Tuple unless defined? T
23
+
24
+ require File.dirname(__FILE__) + '/revactor/actor'
25
+ require File.dirname(__FILE__) + '/revactor/server'
26
+ require File.dirname(__FILE__) + '/revactor/tcp'
27
+ require File.dirname(__FILE__) + '/revactor/behaviors/server'
28
+ require File.dirname(__FILE__) + '/revactor/filters/line'
29
+ require File.dirname(__FILE__) + '/revactor/filters/packet'
data/revactor.gemspec ADDED
@@ -0,0 +1,28 @@
1
+ require 'rubygems'
2
+
3
+ GEMSPEC = Gem::Specification.new do |s|
4
+ s.name = "revactor"
5
+ s.version = "0.1.0"
6
+ s.authors = "Tony Arcieri"
7
+ s.email = "tony@medioh.com"
8
+ s.date = "2008-1-15"
9
+ s.summary = "Revactor is an Actor implementation for writing high performance concurrent programs"
10
+ s.platform = Gem::Platform::RUBY
11
+ s.required_ruby_version = '>= 1.9.0'
12
+
13
+ # Gem contents
14
+ s.files = Dir.glob("{lib,examples,tools,spec}/**/*") + ['Rakefile', 'revactor.gemspec']
15
+
16
+ # Dependencies
17
+ s.add_dependency("rev", ">= 0.1.3")
18
+ s.add_dependency("case", ">= 0.3")
19
+
20
+ # RubyForge info
21
+ s.homepage = "http://revactor.org"
22
+ s.rubyforge_project = "revactor"
23
+
24
+ # RDoc settings
25
+ s.has_rdoc = true
26
+ s.rdoc_options = %w(--title Revactor --main README --line-numbers)
27
+ s.extra_rdoc_files = ["LICENSE", "README", "CHANGES"]
28
+ end