revactor 0.1.0 → 0.1.1

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,70 @@
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 Exception => ex
67
+ from << T[:call_error, Actor.current, ex]
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,159 @@
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
+ class Actor
10
+ # Actor mailbox. For purposes of efficiency the mailbox also handles
11
+ # suspending and resuming an actor when no messages match its filter set.
12
+ class Mailbox
13
+ attr_accessor :timer
14
+ attr_accessor :timed_out
15
+ attr_accessor :timeout_action
16
+
17
+ def initialize
18
+ @timer = nil
19
+ @queue = []
20
+ end
21
+
22
+ # Add a message to the mailbox queue
23
+ def <<(message)
24
+ @queue << message
25
+ end
26
+
27
+ # Attempt to receive a message
28
+ def receive
29
+ raise ArgumentError, "no filter block given" unless block_given?
30
+
31
+ # Clear mailbox processing variables
32
+ action = matched_index = nil
33
+ processed_upto = 0
34
+
35
+ # Clear timeout variables
36
+ @timed_out = false
37
+ @timeout_action = nil
38
+
39
+ # Build the filter
40
+ filter = Filter.new(self)
41
+ yield filter
42
+ raise ArgumentError, "empty filter" if filter.empty?
43
+
44
+ # Process incoming messages
45
+ while action.nil?
46
+ @queue[processed_upto..@queue.size].each_with_index do |message, index|
47
+ unless (action = filter.match message)
48
+ # The filter did not match an action for the current message
49
+ # Keep track of which messages we've ran the filter across so it
50
+ # isn't re-run against messages it already failed to match
51
+ processed_upto += 1
52
+ next
53
+ end
54
+
55
+ # We've found a matching action, so break out of the loop
56
+ matched_index = processed_upto + index
57
+ break
58
+ end
59
+
60
+ # Ignore timeouts if we've matched a message
61
+ if action
62
+ @timed_out = false
63
+ break
64
+ end
65
+
66
+ # Otherwise run the timeout action
67
+ action = @timeout_action if @timed_out
68
+
69
+ # If no matching action is found, reschedule until we get another message
70
+ Actor.reschedule unless action
71
+ end
72
+
73
+ @timeout_action = nil
74
+
75
+ if @timer
76
+ @timer.detach if @timer.attached?
77
+ @timer = nil
78
+ end
79
+
80
+ # If we encountered a timeout, call the action directly
81
+ if @timed_out
82
+ @timed_out = false
83
+ return action.call
84
+ end
85
+
86
+ # Otherwise we matched a message, so process it with the action
87
+ action.(@queue.delete_at matched_index)
88
+ end
89
+
90
+ # Is the mailbox empty?
91
+ def empty?
92
+ @queue.empty?
93
+ end
94
+
95
+ #######
96
+ private
97
+ #######
98
+
99
+ # Timeout class, used to implement receive timeouts
100
+ class Timer < Rev::TimerWatcher
101
+ def initialize(seconds, actor)
102
+ @actor = actor
103
+ super(seconds)
104
+ end
105
+
106
+ def on_timer
107
+ detach
108
+ @actor.mailbox.timed_out = true
109
+ @actor.scheduler << @actor
110
+ end
111
+ end
112
+
113
+ # Mailbox filterset. Takes patterns or procs to match messages with
114
+ # and returns the associated proc when a pattern matches.
115
+ class Filter
116
+ def initialize(mailbox)
117
+ @mailbox = mailbox
118
+ @ruleset = []
119
+ end
120
+
121
+ # Provide a pattern to match against with === and a block to call
122
+ # when the pattern is matched.
123
+ def when(pattern, &action)
124
+ raise ArgumentError, "no block given" unless action
125
+ @ruleset << [pattern, action]
126
+ end
127
+
128
+ # Provide a timeout (in seconds, can be a Float) to wait for matching
129
+ # messages. If the timeout elapses, the given block is called.
130
+ def after(seconds, &action)
131
+ raise ArgumentError, "timeout already specified" if @mailbox.timer
132
+ raise ArgumentError, "must be zero or positive" if seconds < 0
133
+
134
+ # Don't explicitly require an action to be specified for a timeout
135
+ @mailbox.timeout_action = action || proc {}
136
+
137
+ if seconds > 0
138
+ @mailbox.timer = Timer.new(seconds, Actor.current).attach(Rev::Loop.default)
139
+ else
140
+ # No need to actually set a timer if the timeout is zero,
141
+ # just short-circuit waiting for one entirely...
142
+ @mailbox.timed_out = true
143
+ Actor.scheduler << Actor.current
144
+ end
145
+ end
146
+
147
+ # Match a message using the filter
148
+ def match(message)
149
+ _, action = @ruleset.find { |pattern, _| pattern === message }
150
+ action
151
+ end
152
+
153
+ # Is the filterset empty?
154
+ def empty?
155
+ @ruleset.empty? and not @mailbox.timer
156
+ end
157
+ end
158
+ end
159
+ end
@@ -32,12 +32,12 @@ module Mongrel
32
32
 
33
33
  # Runs the thing. It returns the Actor the listener is running in.
34
34
  def run
35
- @acceptor = Actor.new do
35
+ @acceptor = Actor.spawn do
36
36
  begin
37
37
  while true
38
38
  begin
39
39
  client = @socket.accept
40
- actor = Actor.new client, &method(:process_client)
40
+ actor = Actor.spawn client, &method(:process_client)
41
41
  actor[:started_on] = Time.now
42
42
  rescue StopServer
43
43
  break
@@ -59,4 +59,4 @@ module Mongrel
59
59
  return @acceptor
60
60
  end
61
61
  end
62
- end
62
+ end
@@ -0,0 +1,65 @@
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
+ class Actor
10
+ # The Actor Scheduler maintains a run queue of actors with outstanding
11
+ # messages who have not yet processed their mailbox. If all actors have
12
+ # processed their mailboxes then the scheduler waits for any outstanding
13
+ # Rev events. If there are no active Rev watchers then the scheduler exits.
14
+ class Scheduler
15
+ def initialize
16
+ @queue = []
17
+ @running = false
18
+ end
19
+
20
+ # Schedule an Actor to be executed, and run the scheduler if it isn't
21
+ # currently running
22
+ def <<(actor)
23
+ raise ArgumentError, "must be an Actor" unless actor.is_a? Actor
24
+
25
+ @queue << actor unless @queue.last == actor
26
+
27
+ unless @running
28
+ # Persist the fiber the scheduler runs in
29
+ @fiber ||= Fiber.new do
30
+ loop { run; Fiber.yield }
31
+ end
32
+
33
+ # Resume the scheduler
34
+ @fiber.resume
35
+ end
36
+ end
37
+
38
+ # Run the scheduler
39
+ def run
40
+ return if @running
41
+
42
+ @running = true
43
+ default_loop = Rev::Loop.default
44
+
45
+ until @queue.empty? and not default_loop.has_active_watchers?
46
+ @queue.each do |actor|
47
+ begin
48
+ actor.fiber.resume
49
+ rescue FiberError # Fiber may have died since being scheduled
50
+ end
51
+ end
52
+
53
+ @queue.clear
54
+ default_loop.run_once if default_loop.has_active_watchers?
55
+ end
56
+
57
+ @running = false
58
+ end
59
+
60
+ # Is the scheduler running?
61
+ def running?
62
+ @running
63
+ end
64
+ end
65
+ end
@@ -31,15 +31,15 @@ module Revactor
31
31
  socket.attach Rev::Loop.default
32
32
 
33
33
  Actor.receive do |filter|
34
- filter.when(Case[Object, socket]) do |message|
35
- case message[0]
34
+ filter.when(Case[Object, socket]) do |message, _|
35
+ case message
36
36
  when :tcp_connected
37
37
  return socket
38
38
  when :tcp_connect_failed
39
39
  raise ConnectError, "connection refused"
40
40
  when :tcp_resolve_failed
41
41
  raise ResolveError, "couldn't resolve #{host}"
42
- else raise "unexpected message for #{socket.inspect}: #{message.first}"
42
+ else raise "unexpected message for #{socket.inspect}: #{message}"
43
43
  end
44
44
  end
45
45
 
@@ -102,6 +102,10 @@ module Revactor
102
102
  @read_buffer = Rev::Buffer.new
103
103
  end
104
104
 
105
+ def inspect
106
+ "#<#{self.class}:0x#{object_id.to_s(16)} #{@remote_host}:#{@remote_port}>"
107
+ end
108
+
105
109
  # Enable or disable active mode data reception. State can be any
106
110
  # of the following:
107
111
  #
@@ -255,8 +259,8 @@ module Revactor
255
259
  # Lookup filters referenced as symbols
256
260
  def symbol_to_filter(filter)
257
261
  case filter
258
- when :line then Revactor::Filters::Line
259
- when :packet then Revactor::Filters::Packet
262
+ when :line then Revactor::Filter::Line
263
+ when :packet then Revactor::Filter::Packet
260
264
  else raise ArgumentError, "unrecognized filter type: #{filter}"
261
265
  end
262
266
  end
@@ -342,6 +346,10 @@ module Revactor
342
346
  @accepting = false
343
347
  end
344
348
 
349
+ def inspect
350
+ "#<#{self.class}:0x#{object_id.to_s(16)}>"
351
+ end
352
+
345
353
  # Change the default active setting for newly accepted connections
346
354
  def active=(state)
347
355
  unless [true, false, :once].include? state
@@ -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.0"
5
+ s.version = "0.1.1"
6
6
  s.authors = "Tony Arcieri"
7
7
  s.email = "tony@medioh.com"
8
- s.date = "2008-1-15"
8
+ s.date = "2008-1-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'
@@ -14,8 +14,8 @@ GEMSPEC = Gem::Specification.new do |s|
14
14
  s.files = Dir.glob("{lib,examples,tools,spec}/**/*") + ['Rakefile', 'revactor.gemspec']
15
15
 
16
16
  # Dependencies
17
- s.add_dependency("rev", ">= 0.1.3")
18
- s.add_dependency("case", ">= 0.3")
17
+ s.add_dependency("rev", ">= 0.1.4")
18
+ s.add_dependency("case", ">= 0.4")
19
19
 
20
20
  # RubyForge info
21
21
  s.homepage = "http://revactor.org"
@@ -8,52 +8,33 @@ require File.dirname(__FILE__) + '/../lib/revactor/actor'
8
8
 
9
9
  describe Actor do
10
10
  describe "creation" do
11
- it "creates a base Actor with Actor.start" do
12
- Actor.start do
13
- Fiber.current.should be_an_instance_of(Actor)
14
- end
15
- end
16
-
17
- it "allows creation of new Actors with Actor.new" do
18
- Actor.new do
19
- Fiber.current.should be_an_instance_of(Actor)
20
- end
11
+ it "lazily creates Actor.current" do
12
+ Actor.current.should be_an_instance_of(Actor)
21
13
  end
22
14
 
23
15
  it "allows creation of new Actors with Actor.spawn" do
16
+ root = Actor.current
17
+
24
18
  Actor.spawn do
25
- Fiber.current.should be_an_instance_of(Actor)
19
+ Actor.current.should be_an_instance_of(Actor)
20
+ Actor.current.should_not eql(root)
26
21
  end
27
22
  end
28
23
 
29
24
  it "allows arguments to be passed when an Actor is created" do
30
- [:new, :spawn].each do |meth|
31
- Actor.send(meth, 1, 2, 3) do |foo, bar, baz|
32
- [foo, bar, baz].should == [1, 2, 3]
33
- end
25
+ Actor.spawn(1, 2, 3) do |foo, bar, baz|
26
+ [foo, bar, baz].should == [1, 2, 3]
34
27
  end
35
28
  end
36
29
  end
37
30
 
38
- describe "current" do
39
- it "allows the current Actor to be retrieved" do
40
- Actor.new do
41
- Actor.current.should be_an_instance_of(Actor)
42
- end
43
- end
44
-
45
- it "disallows retrieving the current Actor unless the Actor environment is started" do
46
- proc { Actor.current }.should raise_error(ActorError)
47
- end
48
- end
49
-
50
31
  describe "receive" do
51
32
  before :each do
52
33
  @actor_run = false
53
34
  end
54
35
 
55
36
  it "returns the value of the matching filter action" do
56
- actor = Actor.new do
37
+ actor = Actor.spawn do
57
38
  Actor.receive do |filter|
58
39
  filter.when(:foo) { |message| :bar }
59
40
  end.should == :bar
@@ -67,7 +48,7 @@ describe Actor do
67
48
  end
68
49
 
69
50
  it "filters messages with ===" do
70
- actor = Actor.new do
51
+ actor = Actor.spawn do
71
52
  results = []
72
53
  3.times do
73
54
  results << Actor.receive do |filter|
@@ -85,7 +66,7 @@ describe Actor do
85
66
  end
86
67
 
87
68
  it "times out if a message isn't received after the specifed interval" do
88
- actor = Actor.new do
69
+ actor = Actor.spawn do
89
70
  Actor.receive do |filter|
90
71
  filter.when(:foo) { :wrong }
91
72
  filter.after(0.01) { :right }
@@ -96,7 +77,7 @@ describe Actor do
96
77
  end
97
78
 
98
79
  it "matches any message with Object" do
99
- actor = Actor.new do
80
+ actor = Actor.spawn do
100
81
  result = []
101
82
  3.times do
102
83
  result << Actor.receive do |filter|
@@ -114,7 +95,7 @@ describe Actor do
114
95
  end
115
96
 
116
97
  it "detects dead actors" do
117
- actor = Actor.new do
98
+ actor = Actor.spawn do
118
99
  Actor.receive do |filter|
119
100
  filter.when(Object) {}
120
101
  end