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.
@@ -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