thespian 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,52 @@
1
+ begin
2
+ require "fiber"
3
+ rescue LoadError
4
+ raise "Thespian requires Ruby >= 1.9 to run in fibered mode"
5
+ end
6
+
7
+ require "strand"
8
+
9
+ module Thespian
10
+ module Strategy
11
+ class Fiber #:nodoc:
12
+
13
+ include Interface
14
+
15
+ def initialize(&block)
16
+ @block = block
17
+ @mailbox = []
18
+ @mailbox_cond = Strand::ConditionVariable.new
19
+ end
20
+
21
+ def start
22
+ @strand = Strand.new{ @block.call }
23
+ self
24
+ end
25
+
26
+ def receive
27
+ @mailbox_cond.wait while @mailbox.empty?
28
+ @mailbox.shift
29
+ end
30
+
31
+ def <<(message)
32
+ @mailbox << message
33
+ @mailbox_cond.signal
34
+ self
35
+ end
36
+
37
+ def mailbox_size
38
+ @mailbox.size
39
+ end
40
+
41
+ def messages
42
+ @mailbox.dup
43
+ end
44
+
45
+ def stop
46
+ self << Stop.new
47
+ @strand.join
48
+ end
49
+
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,35 @@
1
+ module Thespian
2
+ module Strategy #:nodoc:
3
+
4
+ autoload :Thread, "thespian/strategies/thread"
5
+ autoload :Fiber, "thespian/strategies/fiber"
6
+
7
+ module Interface #:nodoc:
8
+
9
+ def start
10
+ raise "not implemented"
11
+ end
12
+
13
+ def receive
14
+ raise "not implemented"
15
+ end
16
+
17
+ def <<(message)
18
+ raise "not implemented"
19
+ end
20
+
21
+ def mailbox_size
22
+ raise "not implemented"
23
+ end
24
+
25
+ def stop
26
+ raise "not implemented"
27
+ end
28
+
29
+ def salvage_mailbox
30
+ raise "not implemented"
31
+ end
32
+
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,73 @@
1
+ require "thread"
2
+ require "monitor"
3
+ require "json"
4
+
5
+ module Thespian
6
+ module Strategy
7
+ class Process #:nodoc:
8
+
9
+ include Interface
10
+
11
+ def initialize(&block)
12
+ @block = block
13
+ @mailbox = []
14
+ @mailbox_lock = Monitor.new
15
+ @mailbox_cond = @mailbox_lock.new_cond
16
+ @p_read, @p_write = IO.pipe
17
+ @c_read, @c_write = IO.pipe
18
+ end
19
+
20
+ def start
21
+ if fork
22
+ self
23
+ else
24
+ ::Thread.new{ puts "in thread"; command_loop }
25
+ puts "calling block"
26
+ @block.call
27
+ end
28
+ end
29
+
30
+ def receive
31
+ @mailbox_lock.synchronize do
32
+ @mailbox_cond.wait_while{ @mailbox.empty? }
33
+ @mailbox.shift
34
+ end
35
+ end
36
+
37
+ def <<(message)
38
+ @c_write.puts(JSON.dump({ "cmd" => "message", "payload" => Marshal.dump(message) }))
39
+ @c_write.flush
40
+ puts "written"
41
+ end
42
+
43
+ def mailbox_size
44
+ @c_write.puts(JSON.dump({ "cmd" => "mailbox_size" }))
45
+ @c_write.flush
46
+ @p_read.readline.chomp.to_i
47
+ end
48
+
49
+ def command_loop
50
+ puts "in command loop"
51
+ while line = @c_read.readline
52
+ puts "!!! #{line}"
53
+ hash = JSON.parse(line)
54
+ send("cmd_%s" % hash["cmd"], hash)
55
+ end
56
+ end
57
+
58
+ def cmd_message(hash)
59
+ message = Marshal.load(hash["payload"])
60
+ @mailbox_lock.synchronize do
61
+ @mailbox << message
62
+ @mailbox_cond.signal
63
+ end
64
+ end
65
+
66
+ def cmd_mailbox_size(hash)
67
+ @p_write.puts(@mailbox.size)
68
+ @p_write.flush
69
+ end
70
+
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,74 @@
1
+ require "thread"
2
+ require "monitor"
3
+
4
+ module Thespian
5
+ module Strategy
6
+ class Thread #:nodoc:
7
+
8
+ include Interface
9
+
10
+ attr_reader :thread
11
+
12
+ def initialize(&block)
13
+ @block = block
14
+ @mailbox = []
15
+ @mailbox_lock = Monitor.new
16
+ @mailbox_cond = @mailbox_lock.new_cond
17
+ end
18
+
19
+ def start
20
+ # Declare local synchronization vars.
21
+ lock = Monitor.new
22
+ cond = lock.new_cond
23
+ wait = true
24
+
25
+ # Start the thread and have it signal when it's running.
26
+ @thread = ::Thread.new do
27
+ lock.synchronize do
28
+ wait = false
29
+ cond.signal
30
+ end
31
+ @block.call
32
+ end
33
+
34
+ # Block until the thread has signaled that it's running.
35
+ lock.synchronize do
36
+ cond.wait_while{ wait }
37
+ end
38
+ end
39
+
40
+ def receive
41
+ @mailbox_lock.synchronize do
42
+ @mailbox_cond.wait_while{ @mailbox.empty? }
43
+ @mailbox.shift
44
+ end
45
+ end
46
+
47
+ def <<(message)
48
+ @mailbox_lock.synchronize do
49
+ @mailbox << message
50
+ @mailbox_cond.signal
51
+ end
52
+ self
53
+ end
54
+
55
+ def mailbox_size
56
+ @mailbox_lock.synchronize do
57
+ @mailbox.size
58
+ end
59
+ end
60
+
61
+ def messages
62
+ @mailbox_lock.synchronize do
63
+ @mailbox.dup
64
+ end
65
+ end
66
+
67
+ def stop
68
+ self << Stop.new
69
+ @thread.join
70
+ end
71
+
72
+ end
73
+ end
74
+ end
@@ -1,3 +1,3 @@
1
1
  module Thespian
2
- VERSION = "0.0.1"
2
+ VERSION = "0.1.0"
3
3
  end
data/spec/actor_spec.rb CHANGED
@@ -3,8 +3,9 @@ require "spec_helper"
3
3
  module Thespian
4
4
  describe Actor do
5
5
 
6
+ let(:actor){ Actor.new.extend(ActorHelper) }
7
+
6
8
  context "#new" do
7
- let(:actor){ Actor.new }
8
9
 
9
10
  it "returns a new Actor" do
10
11
  actor.should be_a(Actor)
@@ -13,6 +14,26 @@ module Thespian
13
14
  it "that is initialized" do
14
15
  actor.should be_initialized
15
16
  end
17
+
18
+ it "using the Thread strategy" do
19
+ actor.strategy.should be_a(Strategy::Thread)
20
+ end
21
+
22
+ if supports_fibers?
23
+
24
+ it "using the Fiber strategy" do
25
+ actor = Actor.new(:mode => :fiber).extend(ActorHelper)
26
+ actor.strategy.should be_a(Strategy::Fiber)
27
+ end
28
+
29
+ else
30
+
31
+ it "raises an exception if trying to run in fibered mode" do
32
+ expect{ Actor.new(:mode => :fiber) }.to raise_error(RuntimeError)
33
+ end
34
+
35
+ end
36
+
16
37
  end
17
38
 
18
39
  context "#link" do
@@ -36,9 +57,9 @@ module Thespian
36
57
  end
37
58
 
38
59
  context "#start" do
39
- let(:actor){ Actor.new.extend(ActorHelper) }
40
60
 
41
- before(:all) do
61
+ before(:each) do
62
+ stub(actor.strategy).start
42
63
  actor.start
43
64
  end
44
65
 
@@ -46,43 +67,41 @@ module Thespian
46
67
  actor.should be_running
47
68
  end
48
69
 
49
- it "starts a thread" do
50
- actor.thread.should be_alive
70
+ it "calls Strategy#start" do
71
+ actor.strategy.should have_received.start
51
72
  end
52
73
  end
53
74
 
54
75
  context "#receive" do
55
- let(:actor){ Actor.new.extend(ActorHelper) }
56
76
 
57
77
  it "returns the next message from the actor's mailbox" do
58
- actor.mailbox << "hello"
78
+ mock(actor.strategy).receive{ "hello" }
59
79
  actor.receive.should == "hello"
60
80
  end
61
81
 
62
82
  it "raises a DeadActorError if that's what's in the mailbox" do
63
- actor.mailbox << DeadActorError.new(actor, "blah")
83
+ mock(actor.strategy).receive{ DeadActorError.new(actor, "blah") }
64
84
  expect{ actor.receive }.to raise_error(DeadActorError)
65
85
  end
66
86
 
67
87
  it "raises a Stop exception if that's what's in the mailbox" do
68
- actor.mailbox << Stop.new
88
+ mock(actor.strategy).receive{ Stop.new }
69
89
  expect{ actor.receive }.to raise_error(Stop)
70
90
  end
71
91
 
72
92
  it "returns DeadActorError if trap_exit is true and that's what's in the mailbox" do
73
- actor.mailbox << DeadActorError.new(actor, "blah")
93
+ mock(actor.strategy).receive{ DeadActorError.new(actor, "blah") }
74
94
  actor.options(:trap_exit => true)
75
95
  actor.receive.should be_a(DeadActorError)
76
96
  end
77
97
  end
78
98
 
79
99
  context "#<<" do
80
- let(:actor){ Actor.new.extend(ActorHelper) }
81
100
 
82
101
  it "puts an item into the mailbox" do
83
102
  stub(actor).running?{ true }
103
+ mock(actor.strategy).<<("hello")
84
104
  actor << "hello"
85
- actor.mailbox.should include("hello")
86
105
  end
87
106
 
88
107
  it "raises a RuntimeError if the actor isn't alive" do
@@ -93,23 +112,22 @@ module Thespian
93
112
  it "works on a dead actor if strict is false" do
94
113
  actor.should_not be_running
95
114
  actor.options :strict => false
115
+ mock(actor.strategy).<<("blah")
96
116
  actor << "blah"
97
- actor.mailbox.should include("blah")
98
117
  end
99
118
  end
100
119
 
101
120
  context "#stop" do
102
- let(:actor){ Actor.new.extend(ActorHelper) }
103
121
 
104
122
  it "raises an exception if the actor isn't alive" do
105
123
  expect{ actor.stop }.to raise_error(RuntimeError, /not running/i)
106
124
  end
107
125
 
108
126
  it "puts a Stop message in the actor's mailbox" do
109
- mock(actor).running?{ true }.times(2)
110
- mock(actor.thread).join
127
+ stub(actor).check_alive!{ true }
128
+ mock(actor.strategy).<<(is_a(Stop))
129
+ mock(actor.strategy).stop
111
130
  actor.stop
112
- actor.mailbox[0].should be_a(Stop)
113
131
  end
114
132
  end
115
133
 
@@ -121,21 +139,25 @@ module Thespian
121
139
  end
122
140
 
123
141
  it "doesn't include the last message if the actor stopped properly" do
124
- actor = Actor.new.extend(ActorHelper)
125
- actor.mailbox.replace([1, 2, 3, Stop.new, 4, 5])
126
- actor.start
127
- Thread.pass while actor.running?
128
- actor.salvage_mailbox.should == [4, 5]
142
+ mock(actor.strategy).messages{ [2, 3] }
143
+ mock(actor).finished?{ true }
144
+ actor.salvage_mailbox.should == [2, 3]
129
145
  end
130
146
 
131
147
  it "includes the last message if the actor error'ed" do
132
- actor = Actor.new do |message|
133
- raise "oops" if message == 3
134
- end.extend(ActorHelper)
135
- actor.mailbox.replace([1, 2, 3, 4, 5])
136
- actor.start
137
- Thread.pass while actor.running?
138
- actor.salvage_mailbox.should == [3, 4, 5]
148
+ actor.instance_eval{ @last_message = 1 }
149
+ mock(actor.strategy).messages{ [2, 3] }
150
+ mock(actor).finished?{ true }
151
+ actor.salvage_mailbox.should == [1, 2, 3]
152
+ end
153
+
154
+ end
155
+
156
+ context "#mailbox_size" do
157
+
158
+ it "returns how many messages are in the mailbox" do
159
+ mock(actor.strategy).mailbox_size{ 3 }
160
+ actor.mailbox_size.should == 3
139
161
  end
140
162
 
141
163
  end
@@ -0,0 +1,22 @@
1
+ module Thespian
2
+ describe "Thespian when used with classes" do
3
+
4
+ it "processes messages with the block defined by the actor method in the class" do
5
+ klass = Class.new do
6
+ attr_reader :messages
7
+ include Thespian
8
+ actor do |message|
9
+ @messages ||= []
10
+ @messages << message
11
+ end
12
+ end
13
+
14
+ object = klass.new
15
+ object.actor.start
16
+ object.actor << 1 << 2 << 3
17
+ object.actor.stop
18
+ object.messages.should == [1, 2, 3]
19
+ end
20
+
21
+ end
22
+ end
data/spec/spec_helper.rb CHANGED
@@ -1,32 +1,25 @@
1
1
  require "thespian"
2
+ require "rr"
3
+ require "pry"
2
4
 
3
- module ActorHelper
4
-
5
- def linked_actors
6
- @linked_actors
7
- end
8
-
9
- def thread
10
- @thread
11
- end
5
+ # Shared examples
6
+ require "strategies/interface"
12
7
 
13
- def mailbox
14
- @mailbox
15
- end
8
+ module ActorHelper
16
9
 
17
- def mailbox_cond
18
- @mailbox_cond
10
+ def self.extended(object)
11
+ class << object
12
+ public :strategy
13
+ public :receive
14
+ end
19
15
  end
20
16
 
21
- # To get around receive being private
22
- def receive
23
- super
17
+ def linked_actors
18
+ @linked_actors
24
19
  end
25
20
 
26
21
  end
27
22
 
28
- require 'rr'
29
-
30
23
  module RR
31
24
  module Adapters
32
25
  module RSpec2
@@ -64,3 +57,12 @@ RSpec.configure do |config|
64
57
  config.mock_with :rr
65
58
  end
66
59
 
60
+ def supports_fibers?
61
+ begin
62
+ require "fiber"
63
+ rescue LoadError
64
+ false
65
+ else
66
+ true
67
+ end
68
+ end