revactor 0.1.3 → 0.1.4

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGES CHANGED
@@ -1,6 +1,10 @@
1
+ 0.1.4:
2
+
3
+ * Fix bungled 0.1.3 release :(
4
+
1
5
  0.1.3:
2
6
 
3
- * Removed case gem usage in Revactor internals
7
+ * Added the Actorize module for easily using objects as Actors
4
8
 
5
9
  * Add a guaranteed blocking idle state for the Actor scheduler, fixing a bug
6
10
  where an idle root Actor would spin on the scheduler when calling receive
@@ -14,80 +14,99 @@ PORT = 4321
14
14
 
15
15
  # Open a listen socket. All traffic on new connections will be run through
16
16
  # the "line" filter, so incoming messages are delimited by newlines.
17
-
18
17
  listener = Actor::TCP.listen(HOST, PORT, :filter => :line)
19
18
  puts "Listening on #{HOST}:#{PORT}"
20
19
 
21
- # Spawn the server
22
- server = Actor.spawn do
23
- clients = {}
20
+ # The ClientConenction class handles all network interaction with clients
21
+ # This includes the initial handshake (getting a nickname), processing
22
+ # incoming messages, and writing out enqueued messages
23
+ class ClientConnection
24
+ # Add .spawn and .spawn_link methods to the singleton
25
+ extend Actorize
24
26
 
25
- # A proc to broadcast a message to all connected clients. If the server
26
- # were encapsulated into an object this could be a method
27
- broadcast = proc do |msg|
28
- clients.keys.each { |client| client << T[:write, msg] }
27
+ def initialize(dispatcher, sock)
28
+ @dispatcher, @sock = dispatcher, sock
29
+ puts "#{sock.remote_addr}:#{sock.remote_port} connected"
30
+
31
+ handshake
32
+ message_loop
33
+ rescue EOFError
34
+ puts "#{sock.remote_addr}:#{sock.remote_port} disconnected"
35
+ @dispatcher << T[:disconnected, Actor.current]
29
36
  end
30
37
 
31
- # Server's main loop. The server handles incoming messages from the
32
- # client managers and dispatches them to other client managers.
33
- loop do
34
- Actor.receive do |filter|
35
- filter.when(T[:register]) do |_, client, nickname|
36
- clients[client] = nickname
37
- broadcast.call "*** #{nickname} joined"
38
- client << T[:write, "*** Users: " + clients.values.join(', ')]
39
- end
40
-
41
- filter.when(T[:say]) do |_, client, msg|
42
- nickname = clients[client]
43
- broadcast.call "<#{nickname}> #{msg}"
44
- end
45
-
46
- filter.when(T[:disconnected]) do |_, client|
47
- nickname = clients.delete client
48
- broadcast.call "*** #{nickname} left" if nickname
49
- end
50
- end
38
+ def handshake
39
+ @sock.write "Please enter a nickname:"
40
+ nickname = @sock.read
41
+ @dispatcher << T[:register, Actor.current, nickname]
51
42
  end
52
- end
53
-
54
- # The main loop handles incoming connections
55
- loop do
56
- # Spawn a new actor for each incoming connection
57
- Actor.spawn(listener.accept) do |sock|
58
- puts "#{sock.remote_addr}:#{sock.remote_port} connected"
59
-
60
- # Connection handshaking
61
- sock.write "Please enter a nickname:"
62
- nickname = sock.read
63
43
 
64
- server << T[:register, Actor.current, nickname]
65
-
44
+ def message_loop
66
45
  # Flip the socket into asynchronous "active" mode
67
46
  # This means the Actor can receive messages from
68
47
  # the socket alongside other events.
69
- sock.controller = Actor.current
70
- sock.active = :once
71
-
48
+ @sock.controller = Actor.current
49
+ @sock.active = :once
50
+
72
51
  # Main message loop
73
52
  loop do
74
53
  Actor.receive do |filter|
75
- filter.when(T[:tcp, sock]) do |_, _, message|
76
- server << T[:say, Actor.current, message]
77
- sock.active = :once
54
+ # Handle incoming TCP traffic. The line filter ensures that all
55
+ # incoming traffic is filtered down to CRLF-delimited lines of text
56
+ filter.when(T[:tcp, @sock]) do |_, _, message|
57
+ @dispatcher << T[:line, Actor.current, message]
58
+ @sock.active = :once
78
59
  end
79
60
 
61
+ # Write a message to the client's socket
80
62
  filter.when(T[:write]) do |_, message|
81
- sock.write message
63
+ @sock.write message
82
64
  end
83
-
84
- filter.when(T[:tcp_closed, sock]) do
65
+
66
+ # Indicates the client's connection has closed
67
+ filter.when(T[:tcp_closed, @sock]) do
85
68
  raise EOFError
86
69
  end
87
70
  end
88
71
  end
89
- rescue EOFError
90
- puts "#{sock.remote_addr}:#{sock.remote_port} disconnected"
91
- server << T[:disconnected, Actor.current]
92
72
  end
93
73
  end
74
+
75
+ class Dispatcher
76
+ extend Actorize
77
+
78
+ def initialize
79
+ @clients = {}
80
+ run
81
+ end
82
+
83
+ def run
84
+ loop do
85
+ Actor.receive do |filter|
86
+ filter.when(T[:register]) do |_, client, nickname|
87
+ @clients[client] = nickname
88
+ broadcast "*** #{nickname} joined"
89
+ client << T[:write, "*** Users: " + @clients.values.join(', ')]
90
+ end
91
+
92
+ filter.when(T[:line]) do |_, client, msg|
93
+ nickname = @clients[client]
94
+ broadcast "<#{nickname}> #{msg}"
95
+ end
96
+
97
+ filter.when(T[:disconnected]) do |_, client|
98
+ nickname = @clients.delete client
99
+ broadcast "*** #{nickname} left" if nickname
100
+ end
101
+ end
102
+ end
103
+ end
104
+
105
+ # Broadcast a message to all connected clients
106
+ def broadcast(message)
107
+ @clients.keys.each { |client| client << T[:write, message] }
108
+ end
109
+ end
110
+
111
+ dispatcher = Dispatcher.spawn
112
+ loop { ClientConnection.spawn(dispatcher, listener.accept) }
@@ -5,6 +5,7 @@
5
5
  #++
6
6
 
7
7
  require 'rev'
8
+ require 'case'
8
9
 
9
10
  # This is mostly in hopes of a bright future with Rubinius
10
11
  # The recommended container for all datagrams sent between
@@ -26,13 +27,13 @@ end
26
27
  T = Tuple unless defined? T
27
28
 
28
29
  module Revactor
29
- Revactor::VERSION = '0.1.3' unless defined? Revactor::VERSION
30
+ Revactor::VERSION = '0.1.4' unless defined? Revactor::VERSION
30
31
  def self.version() VERSION end
31
32
  end
32
33
 
33
34
  %w{
34
- actor scheduler mailbox delegator tcp http_client
35
- filters/line filters/packet
35
+ actor scheduler mailbox tcp http_client
36
+ filters/line filters/packet actorize
36
37
  }.each do |file|
37
38
  require File.dirname(__FILE__) + '/revactor/' + file
38
39
  end
@@ -41,6 +42,5 @@ end
41
42
  class Actor
42
43
  Actor::TCP = Revactor::TCP unless defined? Actor::TCP
43
44
  Actor::Filter = Revactor::Filter unless defined? Actor::Filter
44
- Actor::Delegator = Revactor::Delegator unless defined? Actor::Delegator
45
45
  Actor::HttpClient = Revactor::HttpClient unless defined? Actor::HttpClient
46
46
  end
@@ -4,7 +4,6 @@
4
4
  # See file LICENSE for details
5
5
  #++
6
6
 
7
- require File.dirname(__FILE__) + '/../revactor'
8
7
  require 'thread'
9
8
  require 'fiber'
10
9
 
@@ -0,0 +1,76 @@
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
+ # Any class can be "actorized" by extending it with the Actorize module:
8
+ #
9
+ # class MyClass
10
+ # extend Actorize
11
+ # end
12
+ #
13
+ # This will create MyClass.spawn and MyClass.spawn_link instance methods.
14
+ # Now you can create instances of your class which run inside Actors:
15
+ #
16
+ # actor = MyClass.spawn
17
+ #
18
+ # Actorize defines method_missing for the Actor object, and will delegate
19
+ # any method calls to the Actor to MyClass's call method. This method
20
+ # should be defined with the following signature:
21
+ #
22
+ # class MyClass
23
+ # extend Actorize
24
+ #
25
+ # def self.call(actor, meth, *args)
26
+ # ...
27
+ # end
28
+ # end
29
+ #
30
+ # The call method receives the actor the call was made on, the method that
31
+ # was invoked, and the arguments that were passed. You can then write a
32
+ # simple RPC mechanism to send a message to the actor and receive a response:
33
+ #
34
+ # actor << [:call, Actor.current, meth, *args]
35
+ # Actor.receive do |filter|
36
+ # filter.when(T[:call_reply, actor]) { |_, _, response| response }
37
+ # end
38
+ #
39
+ # Using this approach, you can mix the synchronous approach of Objects with
40
+ # the asynchronous approach of Actors, and effectively duck type Actors
41
+ # to Objects.
42
+ #
43
+ module Actorize
44
+ def spawn(*args)
45
+ _actorize Actor.spawn(*args, &method(:new))
46
+ end
47
+
48
+ def spawn_link(*args)
49
+ _actorize Actor.spawn_link(*args, &method(:new))
50
+ end
51
+
52
+ #######
53
+ private
54
+ #######
55
+
56
+ def _actorize(actor)
57
+ actor.extend InstanceMethods
58
+ actor.instance_variable_set(:@_class, self)
59
+ actor
60
+ end
61
+
62
+ module InstanceMethods
63
+ def method_missing(*args, &block)
64
+ return super unless @_class.respond_to?(:call)
65
+ @_class.call(self, *args, &block)
66
+ end
67
+
68
+ def remote_class
69
+ @_class
70
+ end
71
+
72
+ def inspect
73
+ "#<#{self.class}(#{remote_class}):0x#{object_id.to_s(16)}>"
74
+ end
75
+ end
76
+ end
@@ -4,9 +4,7 @@
4
4
  # See file LICENSE for details
5
5
  #++
6
6
 
7
- require File.dirname(__FILE__) + '/../revactor'
8
7
  require 'uri'
9
-
10
8
 
11
9
  module Revactor
12
10
  # Thrown for all HTTP-specific errors
@@ -4,8 +4,6 @@
4
4
  # See file LICENSE for details
5
5
  #++
6
6
 
7
- require File.dirname(__FILE__) + '/../revactor'
8
-
9
7
  class Actor
10
8
  # Actor mailbox. For purposes of efficiency the mailbox also handles
11
9
  # suspending and resuming an actor when no messages match its filter set.
@@ -78,7 +76,7 @@ class Actor
78
76
  end
79
77
 
80
78
  # Otherwise we matched a message, so process it with the action
81
- action.(@queue.delete_at matched_index)
79
+ action.call @queue.delete_at(matched_index)
82
80
  end
83
81
 
84
82
  # Is the mailbox empty?
@@ -1,6 +1,6 @@
1
- require File.dirname(__FILE__) + '/../revactor'
2
1
  require 'mongrel'
3
-
2
+ require File.dirname(__FILE__) + '/../revactor'
3
+
4
4
  class Revactor::TCP::Socket
5
5
  # Monkeypatched readpartial routine inserted whenever Revactor's mongrel.rb
6
6
  # is loaded. The value passed to this method is ignored, so it is not
@@ -5,7 +5,6 @@
5
5
  #++
6
6
 
7
7
  require 'thread'
8
- require File.dirname(__FILE__) + '/../revactor'
9
8
 
10
9
  class Actor
11
10
  # The Actor Scheduler maintains a run queue of actors with outstanding
@@ -4,8 +4,6 @@
4
4
  # See file LICENSE for details
5
5
  #++
6
6
 
7
- require File.dirname(__FILE__) + '/../revactor'
8
-
9
7
  module Revactor
10
8
  # The TCP module holds all Revactor functionality related to the
11
9
  # Transmission Control Protocol, including drop-in replacements
@@ -346,8 +344,8 @@ module Revactor
346
344
  def initialize(host, port, options = {})
347
345
  super(host, port)
348
346
  opts = {
349
- active: false,
350
- controller: Actor.current
347
+ :active => false,
348
+ :controller => Actor.current
351
349
  }.merge(options)
352
350
 
353
351
  @active, @controller = opts[:active], opts[:controller]
@@ -2,10 +2,10 @@ require 'rubygems'
2
2
 
3
3
  GEMSPEC = Gem::Specification.new do |s|
4
4
  s.name = "revactor"
5
- s.version = "0.1.3"
5
+ s.version = "0.1.4"
6
6
  s.authors = "Tony Arcieri"
7
7
  s.email = "tony@medioh.com"
8
- s.date = "2008-1-29"
8
+ s.date = "2008-3-28"
9
9
  s.summary = "Revactor is an Actor implementation for writing high performance concurrent programs"
10
10
  s.platform = Gem::Platform::RUBY
11
11
  s.required_ruby_version = '>= 1.9.0'
@@ -15,6 +15,7 @@ GEMSPEC = Gem::Specification.new do |s|
15
15
 
16
16
  # Dependencies
17
17
  s.add_dependency("rev", ">= 0.2.0")
18
+ s.add_dependency("case", ">= 0.4")
18
19
 
19
20
  # RubyForge info
20
21
  s.homepage = "http://revactor.org"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: revactor
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3
4
+ version: 0.1.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tony Arcieri
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2008-01-29 00:00:00 -07:00
12
+ date: 2008-03-28 00:00:00 -06:00
13
13
  default_executable:
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
@@ -21,6 +21,15 @@ dependencies:
21
21
  - !ruby/object:Gem::Version
22
22
  version: 0.2.0
23
23
  version:
24
+ - !ruby/object:Gem::Dependency
25
+ name: case
26
+ version_requirement:
27
+ version_requirements: !ruby/object:Gem::Requirement
28
+ requirements:
29
+ - - ">="
30
+ - !ruby/object:Gem::Version
31
+ version: "0.4"
32
+ version:
24
33
  description:
25
34
  email: tony@medioh.com
26
35
  executables: []
@@ -34,7 +43,7 @@ extra_rdoc_files:
34
43
  files:
35
44
  - lib/revactor
36
45
  - lib/revactor/actor.rb
37
- - lib/revactor/delegator.rb
46
+ - lib/revactor/actorize.rb
38
47
  - lib/revactor/filters
39
48
  - lib/revactor/filters/line.rb
40
49
  - lib/revactor/filters/packet.rb
@@ -1,70 +0,0 @@
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
- # A Delegator whose delegate runs in a separate Actor. This allows
10
- # an easy method for constructing synchronous calls to a separate Actor
11
- # which cause the current Actor to block until they complete.
12
- class Revactor::Delegator
13
- # Create a new delegator for the given object
14
- def initialize(obj)
15
- @obj = obj
16
- @running = true
17
- @actor = Actor.spawn(&method(:start))
18
- end
19
-
20
- # Stop the delegator Actor
21
- def stop
22
- @actor << :stop
23
- nil
24
- end
25
-
26
- # Send a message to the Actor delegate
27
- def send(meth, *args, &block)
28
- @actor << T[:call, Actor.current, meth, args, block]
29
- Actor.receive do |filter|
30
- filter.when(Case[:call_reply, @actor, Object]) { |_, _, reply| reply }
31
- filter.when(Case[:call_error, @actor, Object]) { |_, _, ex| raise ex }
32
- end
33
- end
34
-
35
- alias_method :method_missing, :send
36
-
37
- #########
38
- protected
39
- #########
40
-
41
- # Start the server
42
- def start
43
- loop do
44
- Actor.receive do |filter|
45
- filter.when(:stop) { |_| return }
46
- filter.when(Object) { |message| handle_message(message) }
47
- end
48
- end
49
- end
50
-
51
- # Dispatch the incoming message to the appropriate handler
52
- def handle_message(message)
53
- case message.first
54
- when :call then handle_call(message)
55
- else @obj.__send__(:on_message, message) if @obj.respond_to? :on_message
56
- end
57
- end
58
-
59
- # Wrapper for calling the provided object's handle_call method
60
- def handle_call(message)
61
- _, from, meth, args, block = message
62
-
63
- begin
64
- result = @obj.__send__(meth, *args, &block)
65
- from << T[:call_reply, Actor.current, result]
66
- rescue => ex
67
- from << T[:call_error, Actor.current, ex]
68
- end
69
- end
70
- end