thespian 0.0.1 → 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.
- data/.travis.yml +7 -0
- data/CHANGELOG +8 -0
- data/README.rdoc +49 -3
- data/Rakefile +8 -0
- data/examples/linked.rb +36 -30
- data/examples/producer_consumer.rb +25 -17
- data/examples/task_processor.rb +9 -5
- data/lib/thespian.rb +10 -2
- data/lib/thespian/actor.rb +24 -35
- data/lib/thespian/dsl.rb +5 -0
- data/lib/thespian/example.rb +41 -0
- data/lib/thespian/strategies/fiber.rb +52 -0
- data/lib/thespian/strategies/interface.rb +35 -0
- data/lib/thespian/strategies/process.rb +73 -0
- data/lib/thespian/strategies/thread.rb +74 -0
- data/lib/thespian/version.rb +1 -1
- data/spec/actor_spec.rb +51 -29
- data/spec/classes_spec.rb +22 -0
- data/spec/spec_helper.rb +21 -19
- data/spec/strategies/fiber_spec.rb +42 -0
- data/spec/strategies/interface.rb +91 -0
- data/spec/strategies/thread_spec.rb +17 -0
- data/spec/thespian_spec.rb +65 -0
- data/thespian.gemspec +6 -2
- metadata +76 -10
@@ -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
|
data/lib/thespian/version.rb
CHANGED
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(:
|
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 "
|
50
|
-
actor.
|
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.
|
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.
|
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.
|
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.
|
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
|
-
|
110
|
-
mock(actor.
|
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
|
125
|
-
actor.
|
126
|
-
actor.
|
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 =
|
133
|
-
|
134
|
-
|
135
|
-
actor.
|
136
|
-
|
137
|
-
|
138
|
-
|
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
|
-
|
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
|
-
|
14
|
-
@mailbox
|
15
|
-
end
|
8
|
+
module ActorHelper
|
16
9
|
|
17
|
-
def
|
18
|
-
|
10
|
+
def self.extended(object)
|
11
|
+
class << object
|
12
|
+
public :strategy
|
13
|
+
public :receive
|
14
|
+
end
|
19
15
|
end
|
20
16
|
|
21
|
-
|
22
|
-
|
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
|