revactor 0.1.0

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