theatre 0.8.0
Sign up to get free protection for your applications and to get access to all the features.
- data/MIT-LICENSE +22 -0
- data/README.markdown +64 -0
- data/Rakefile +67 -0
- data/TODO.markdown +18 -0
- data/algorithms.markdown +43 -0
- data/benchmark/growing_usage.rb +46 -0
- data/lib/theatre.rb +144 -0
- data/lib/theatre/dsl/callback_definition_loader.rb +83 -0
- data/lib/theatre/guid.rb +23 -0
- data/lib/theatre/invocation.rb +121 -0
- data/lib/theatre/namespace_manager.rb +153 -0
- data/lib/theatre/version.rb +2 -0
- data/theatre.gemspec +41 -0
- metadata +65 -0
data/MIT-LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2008 Jay Phillips
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person
|
4
|
+
obtaining a copy of this software and associated documentation
|
5
|
+
files (the "Software"), to deal in the Software without
|
6
|
+
restriction, including without limitation the rights to use,
|
7
|
+
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
8
|
+
copies of the Software, and to permit persons to whom the
|
9
|
+
Software is furnished to do so, subject to the following
|
10
|
+
conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be
|
13
|
+
included in all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
16
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
17
|
+
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
18
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
19
|
+
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
20
|
+
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
21
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
22
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
data/README.markdown
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
Theatre
|
2
|
+
=======
|
3
|
+
|
4
|
+
Present status: **Release candidate**
|
5
|
+
|
6
|
+
A library for choreographing a dynamic pool of hierarchically organized actors. This was originally extracted from the [Adhearsion](http://adhearsion.com) framework by Jay Phillips.
|
7
|
+
|
8
|
+
In the Adhearsion framework, it was necessary to develop an internal message-passing system that could work either synchronously or asynchronously. This is used by the framework itself and for framework extensions (called _components_) to talk with each other. The source of the events is completely undefined -- events could originate from within the framework out outside the framework. For example, a Message Queue such as [Stomp](http://stomp.codehaus.org) can wire incoming events into Theatre and catch events going to a particular destination so it can proxy them out to the server.
|
9
|
+
|
10
|
+
Motivations and Design Decisions
|
11
|
+
--------------------------------
|
12
|
+
|
13
|
+
* Must maintain Ruby 1.8 and JRuby compatibility
|
14
|
+
* Must be Thread-safe
|
15
|
+
* Must provide some level of transparency into the events going through it
|
16
|
+
* Must be dynamic enough to reallocate the number of triggerrs based on load
|
17
|
+
* Must help facilitate test-driven development of Actor functionality
|
18
|
+
* Must allow external persistence in case of a crash
|
19
|
+
|
20
|
+
Example
|
21
|
+
-------
|
22
|
+
|
23
|
+
Below is an example taken from Adhearsion for executing framework-level callbacks. Note: the framework treats this callback synchronously.
|
24
|
+
|
25
|
+
events.framework.asterisk.before_call.each do |event|
|
26
|
+
# Pull headers from event and respond to it here.
|
27
|
+
end
|
28
|
+
|
29
|
+
Below is an example of integration with [Stomp](http://stomp.codehaus.org/), a simple yet robust open-protocol message queue.
|
30
|
+
|
31
|
+
events.stomp.new_call.each do |event|
|
32
|
+
# Handle all events from the Stomp MQ server whose name is "new_call" (the String)
|
33
|
+
end
|
34
|
+
|
35
|
+
This will filter all events whose name is "new_call" and yield the Stomp::Message to the block.
|
36
|
+
|
37
|
+
Framework terminology
|
38
|
+
--------------------
|
39
|
+
|
40
|
+
Below are definitions of terms I use in Theatre. See the respective links for more information.
|
41
|
+
|
42
|
+
* **callback**: This is the block given to the `each` method which triggers events coming in.
|
43
|
+
* **payload**: This is the "message" sent to the Theatre and is what will ultimately be yielded to the callback
|
44
|
+
* **[Actor](http://en.wikipedia.org/wiki/Actor_model)**: This refers to concurrent responders to events in a concurrent system.
|
45
|
+
|
46
|
+
Synchronous vs. Asynchronous
|
47
|
+
----------------------------
|
48
|
+
|
49
|
+
With Theatre, all events are asynchronous with the optional ability to synchronously block until the event is scheduled, triggered, and has returned. If you wish to synchronously trigger the event, simple call `wait` on an `Invocation` object returned from `trigger` and then check the `Invocation#current_state` property for `:success` or `:error`. Optionally the `Invocation#success?` and `Invocation#error?` methods also provide more intuitive access to the finished state. If the event finished with `:success`, you may retrieve the returned value of the event Proc by calling `Invocation#returned_value`. If the event finished with `:error`, you may get the Exception with `Invocation#error`.
|
50
|
+
|
51
|
+
Because many callbacks can be registered for a particular namespace, each needing its own Invocation object, the `Theatre#trigger` method returns an Array of Invocation objects.
|
52
|
+
|
53
|
+
# We'll assume only one callback is registered and call #first on the Array of Invocations returned by #trigger
|
54
|
+
invocation = my_theatre.trigger("your/namespace/here", YourSpecialClass.new("this can be anything")).first
|
55
|
+
invocation.wait
|
56
|
+
raise invocation.error if invocation.error?
|
57
|
+
log "Actor finished with return value #{invocation.returned_value}"
|
58
|
+
|
59
|
+
Ruby 1.8 vs. Ruby 1.9 vs. JRuby
|
60
|
+
-------------------------------
|
61
|
+
|
62
|
+
Theatre was created for Ruby 1.8 because no good Actor system existed on Ruby 1.8 that met Adhearsion's needs (e.g. hierarchal with synchronous and asynchronous modes. If you wish to achieve real processor-level concurrency, use JRuby.
|
63
|
+
|
64
|
+
Presently Ruby 1.9 compatibility is not a priority but patches for compatibility will be accepted, as long as they preserve compatibility with both MRI and JRuby.
|
data/Rakefile
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
begin
|
2
|
+
require 'yard'
|
3
|
+
YARD::Rake::YardocTask.new do |t|
|
4
|
+
t.files = ['lib/**/*.rb'] + %w[README.markdown TODO.markdown LICENSE]
|
5
|
+
end
|
6
|
+
rescue LoadError
|
7
|
+
STDERR.puts "\nCould not require() YARD! Install with 'gem install yard' to get the 'yardoc' task\n\n"
|
8
|
+
end
|
9
|
+
|
10
|
+
require 'rake/gempackagetask'
|
11
|
+
require 'rubygems'
|
12
|
+
require 'spec/rake/spectask'
|
13
|
+
|
14
|
+
SPEC_GLOB = 'spec/**/*_spec.rb'
|
15
|
+
GEMSPEC = eval File.read("theatre.gemspec")
|
16
|
+
|
17
|
+
begin
|
18
|
+
require 'rcov/rcovtask'
|
19
|
+
Rcov::RcovTask.new do |t|
|
20
|
+
t.test_files = Dir[*SPEC_GLOB]
|
21
|
+
t.output_dir = 'coverage'
|
22
|
+
t.verbose = true
|
23
|
+
t.rcov_opts.concat %w[--sort coverage --sort-reverse -x gems -x /var --no-validator-links]
|
24
|
+
end
|
25
|
+
rescue LoadError
|
26
|
+
STDERR.puts "Could not load rcov tasks -- rcov does not appear to be installed. Continuing anyway."
|
27
|
+
end
|
28
|
+
|
29
|
+
Rake::GemPackageTask.new(GEMSPEC).define
|
30
|
+
|
31
|
+
desc "Run all RSpecs"
|
32
|
+
Spec::Rake::SpecTask.new do |t|
|
33
|
+
t.spec_files = FileList[SPEC_GLOB]
|
34
|
+
end
|
35
|
+
|
36
|
+
desc "Compares Theatre's files with those listed in theatre.gemspec"
|
37
|
+
task :check_gemspec_files do
|
38
|
+
|
39
|
+
files_from_gemspec = THEATRE_FILES
|
40
|
+
files_from_filesystem = Dir.glob(File.dirname(__FILE__) + "/**/*").map do |filename|
|
41
|
+
filename[0...Dir.pwd.length] == Dir.pwd ? filename[(Dir.pwd.length+1)..-1] : filename
|
42
|
+
end.sort
|
43
|
+
files_from_filesystem.reject! { |f| File.directory? f }
|
44
|
+
|
45
|
+
puts
|
46
|
+
puts 'Pipe this command to "grep -v \'spec/\'" to ignore spec files'
|
47
|
+
puts
|
48
|
+
puts '##########################################'
|
49
|
+
puts '## Files on filesystem not in the gemspec:'
|
50
|
+
puts '##########################################'
|
51
|
+
puts((files_from_filesystem - files_from_gemspec).map { |f| " " + f })
|
52
|
+
|
53
|
+
|
54
|
+
puts '##########################################'
|
55
|
+
puts '## Files in gemspec not in the filesystem:'
|
56
|
+
puts '##########################################'
|
57
|
+
puts((files_from_gemspec - files_from_filesystem).map { |f| " " + f })
|
58
|
+
end
|
59
|
+
|
60
|
+
desc "Test that the .gemspec file executes"
|
61
|
+
task :debug_gem do
|
62
|
+
require 'rubygems/specification'
|
63
|
+
gemspec = File.read('adhearsion.gemspec')
|
64
|
+
spec = nil
|
65
|
+
Thread.new { spec = eval("$SAFE = 3\n#{gemspec}") }.join
|
66
|
+
puts "SUCCESS: Gemspec runs at the $SAFE level 3."
|
67
|
+
end
|
data/TODO.markdown
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
Ideas for improvement
|
2
|
+
=====================
|
3
|
+
|
4
|
+
Want to contribute to Theatre? See if any of these features may interest you and improve the way you work with the library. If so, feel free to fork this project on Github and add it!
|
5
|
+
|
6
|
+
Theatre-owned namespaces
|
7
|
+
------------------------
|
8
|
+
|
9
|
+
Theatre would register event callbacks within its own Theatre-specific namespace. When responding to these events, it could do something such as report the average runtime for a particular namespace or how long it's been since the Theatre started.
|
10
|
+
|
11
|
+
Prioritized namespaces
|
12
|
+
----------------------
|
13
|
+
|
14
|
+
Certain events should be executed as fast as possible. For example, in Adhearsion, maybe `before_call` and `after_call` events should be prioritized over other events.
|
15
|
+
|
16
|
+
Handling within the caller's Thread
|
17
|
+
-----------------------------------
|
18
|
+
When calling Theatre#trigger, maybe it'd make sense to have some events run within the caller's Thread instead of running it in another Thread. This should probably be split into a separate method called `trigger_syncrhonously` or something.
|
data/algorithms.markdown
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
Formulas
|
2
|
+
========
|
3
|
+
|
4
|
+
This is a work-in-progress document covering the mathematical algorithms behind Theatre.
|
5
|
+
|
6
|
+
Running average without history
|
7
|
+
-------------------------------
|
8
|
+
|
9
|
+
In our system, we're either given or can calculate for the following information. When I say "number", in this case I mean "a stored time in seconds it took to execute a callback."
|
10
|
+
|
11
|
+
current_average = current average of all numbers in data set
|
12
|
+
count = quantity of data items in current_average's data set
|
13
|
+
reduced_average = current average adjusted for one more number to be added to the data set
|
14
|
+
data = new item for the dataset
|
15
|
+
new_average = new average of all numbers in data set, including "data"
|
16
|
+
|
17
|
+
|
18
|
+
count / (count+1) = current_average / reduced_average
|
19
|
+
∴ reduced_average = (current_average * (count + 1)) / count
|
20
|
+
|
21
|
+
new_average = reduced_average + (data / (count + 1))
|
22
|
+
|
23
|
+
|
24
|
+
Accounting for variations
|
25
|
+
-------------------------
|
26
|
+
|
27
|
+
Two things can happen which would make the system have an inappropriate number of responders:
|
28
|
+
|
29
|
+
* Spikes or sudden drops in traffic
|
30
|
+
* A singularity input to a callback which causes it to take a uniquely long time to execute
|
31
|
+
|
32
|
+
If only a "running average" algorithm is used to track average callback time, the system will respond to variations in throughput less effectively the longer the system runs. The solution is to calculate several running times.
|
33
|
+
|
34
|
+
For these reasons, the system should allow the user to specify sampling times and accuracy rates. Some systems may
|
35
|
+
|
36
|
+
Erlang B/C calculations
|
37
|
+
-----------------------
|
38
|
+
|
39
|
+
With Agner Erlang's formulas, we calculate the appropriate number of responders to a given throughput of actions. For his formulas, we must know the following information:
|
40
|
+
|
41
|
+
* What is the average runtime of each callback?
|
42
|
+
* How many callbacks do we execute per hour?
|
43
|
+
* What is an acceptable amount of wait time?
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# Adds messages to the Theatre with growing intensity
|
2
|
+
|
3
|
+
require File.dirname(__FILE__) + "/../lib/theatre.rb"
|
4
|
+
|
5
|
+
theatre = Theatre.new
|
6
|
+
|
7
|
+
# TODO: Register namespaces here
|
8
|
+
|
9
|
+
|
10
|
+
# As the current time diverges from the start time, enqueue more messages per second
|
11
|
+
start_time = Time.now
|
12
|
+
|
13
|
+
# This is a function of the number of seconds that have passed since starting. For example, at 10 seconds into the demo,
|
14
|
+
# we'll be pushing in 13 messages per second. At 100 seconds (1 minute, 40 seconds), we'll be doing 130 messages per second.
|
15
|
+
growth_rate = 1.3
|
16
|
+
|
17
|
+
# We need to keep a running count of how long it takes to actually dispatch a payload into the theatre.
|
18
|
+
trigger_count = 0
|
19
|
+
average_time = 0
|
20
|
+
|
21
|
+
loop do
|
22
|
+
|
23
|
+
seconds_since_start = (Time.now - start_time).to_i
|
24
|
+
times_per_second = (time_since_start * growth_rate).to_i
|
25
|
+
|
26
|
+
times_per_second.times do
|
27
|
+
|
28
|
+
before_handling = Time.now
|
29
|
+
theatre.trigger "/my/special/namespace", :payload
|
30
|
+
after_handling = Time.now
|
31
|
+
|
32
|
+
trigger_times.push after_handling - before_handling
|
33
|
+
|
34
|
+
# Update the running average
|
35
|
+
# count / (count+1) = current_average / reduced_average
|
36
|
+
# ∴ reduced_average = (current_average * (count + 1)) / count
|
37
|
+
#
|
38
|
+
# new_average = reduced_average + (data / (count + 1))
|
39
|
+
|
40
|
+
trigger_count / (trigger_count + 1)
|
41
|
+
|
42
|
+
sleep average_time
|
43
|
+
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
data/lib/theatre.rb
ADDED
@@ -0,0 +1,144 @@
|
|
1
|
+
require 'thread'
|
2
|
+
require 'rubygems'
|
3
|
+
|
4
|
+
$: << File.expand_path(File.dirname(__FILE__))
|
5
|
+
|
6
|
+
require 'theatre/version'
|
7
|
+
require 'theatre/namespace_manager'
|
8
|
+
require 'theatre/invocation'
|
9
|
+
require 'theatre/dsl/callback_definition_loader'
|
10
|
+
|
11
|
+
module Theatre
|
12
|
+
|
13
|
+
class Theatre
|
14
|
+
|
15
|
+
attr_reader :namespace_manager
|
16
|
+
|
17
|
+
##
|
18
|
+
# Creates a new stopped Theatre. You must call start!() after you instantiate this for it to begin processing events.
|
19
|
+
#
|
20
|
+
# @param [Fixnum] thread_count Number of Threads to spawn when started.
|
21
|
+
#
|
22
|
+
def initialize(thread_count=6)
|
23
|
+
@thread_count = thread_count
|
24
|
+
@started = false
|
25
|
+
@namespace_manager = ActorNamespaceManager.new
|
26
|
+
@thread_group = ThreadGroup.new
|
27
|
+
@master_queue = Queue.new
|
28
|
+
end
|
29
|
+
|
30
|
+
##
|
31
|
+
# Send a message to this Theatre for asynchronous processing.
|
32
|
+
#
|
33
|
+
# @param [String] namespace The namespace to which the payload should be sent
|
34
|
+
# @param [Object] payload The actual content to be sent to the callback. Optional.
|
35
|
+
# @return [Array<Theatre::Invocation>] An Array of Invocation objects
|
36
|
+
# @raise Theatre::NamespaceNotFound Raised when told to enqueue an unrecognized namespace
|
37
|
+
#
|
38
|
+
def trigger(namespace, payload=:argument_undefined)
|
39
|
+
@namespace_manager.callbacks_for_namespaces(namespace).map do |callback|
|
40
|
+
invocation = if payload.equal?(:argument_undefined)
|
41
|
+
Invocation.new(namespace, callback)
|
42
|
+
else
|
43
|
+
Invocation.new(namespace, callback, payload)
|
44
|
+
end
|
45
|
+
invocation.queued
|
46
|
+
@master_queue << invocation
|
47
|
+
invocation
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
##
|
52
|
+
# Send a message to this Theatre for synchronous processing. The execution of this will not go through this Theatre's
|
53
|
+
# Thread pool. If an error occurred in any of callbacks, the Exception object will be placed in the returned Array
|
54
|
+
# instead for you to act upon.
|
55
|
+
#
|
56
|
+
# @param [String] namespace The namespace to which the payload should be sent
|
57
|
+
# @param [Object] payload The actual content to be sent to the callback. Optional.
|
58
|
+
# @return [Array] An Array containing each callback's return value (or Exception raised, if any) when given the payload
|
59
|
+
# @raise Theatre::NamespaceNotFound Raised when told to enqueue an unrecognized namespace
|
60
|
+
#
|
61
|
+
def trigger_immediately(namespace, payload=:argument_undefined)
|
62
|
+
@namespace_manager.callbacks_for_namespaces(namespace).map do |callback|
|
63
|
+
begin
|
64
|
+
invocation = if payload.equal?(:argument_undefined)
|
65
|
+
callback.call
|
66
|
+
else
|
67
|
+
callback.call payload
|
68
|
+
end
|
69
|
+
rescue => captured_error_to_be_returned
|
70
|
+
captured_error_to_be_returned
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def load_events_code(code, *args)
|
76
|
+
CallbackDefinitionLoader.new(self, *args).load_events_code(code)
|
77
|
+
end
|
78
|
+
|
79
|
+
def load_events_file(file, *args)
|
80
|
+
CallbackDefinitionLoader.new(self, *args).load_events_file(file)
|
81
|
+
end
|
82
|
+
|
83
|
+
def register_namespace_name(*args)
|
84
|
+
@namespace_manager.register_namespace_name(*args)
|
85
|
+
end
|
86
|
+
|
87
|
+
def register_callback_at_namespace(*args)
|
88
|
+
@namespace_manager.register_callback_at_namespace(*args)
|
89
|
+
end
|
90
|
+
|
91
|
+
def join
|
92
|
+
@thread_group.list.each do |thread|
|
93
|
+
begin
|
94
|
+
thread.join
|
95
|
+
rescue
|
96
|
+
# Ignore any exceptions
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
##
|
102
|
+
# Starts this Theatre.
|
103
|
+
#
|
104
|
+
# When this method is called, the Threads are spawned and begin pulling messages off this Theatre's master queue.
|
105
|
+
#
|
106
|
+
def start!
|
107
|
+
return false if @thread_group.list.any? # Already started
|
108
|
+
@started_time = Time.now
|
109
|
+
@thread_count.times do
|
110
|
+
@thread_group.add Thread.new(&method(:thread_loop))
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
##
|
115
|
+
# Notifies all Threads for this Theatre to stop by sending them special messages. Any messages which were queued and
|
116
|
+
# untriggered when this method is received will still be processed. Note: you may start this Theatre again later once it
|
117
|
+
# has been stopped.
|
118
|
+
#
|
119
|
+
def graceful_stop!
|
120
|
+
@thread_count.times { @master_queue << :THEATRE_SHUTDOWN! }
|
121
|
+
@started_time = nil
|
122
|
+
end
|
123
|
+
|
124
|
+
protected
|
125
|
+
|
126
|
+
# This will use the Adhearsion logger eventually.
|
127
|
+
def warn(exception)
|
128
|
+
# STDERR.puts exception.message, *exception.backtrace
|
129
|
+
end
|
130
|
+
|
131
|
+
def thread_loop
|
132
|
+
loop do
|
133
|
+
begin
|
134
|
+
next_invocation = @master_queue.pop
|
135
|
+
return :stopped if next_invocation.equal? :THEATRE_SHUTDOWN!
|
136
|
+
next_invocation.start
|
137
|
+
rescue => error
|
138
|
+
warn error
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
end
|
144
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
module Theatre
|
2
|
+
|
3
|
+
##
|
4
|
+
# This class provides the a wrapper aroung which an events.rb file can be instance_eval'd.
|
5
|
+
#
|
6
|
+
class CallbackDefinitionLoader
|
7
|
+
|
8
|
+
attr_reader :theatre, :root_name
|
9
|
+
def initialize(theatre, root_name=:events)
|
10
|
+
@theatre = theatre
|
11
|
+
@root_name = root_name
|
12
|
+
create_recorder_method root_name
|
13
|
+
end
|
14
|
+
|
15
|
+
def anonymous_recorder
|
16
|
+
BlankSlateMessageRecorder.new(&method(:callback_registered))
|
17
|
+
end
|
18
|
+
|
19
|
+
##
|
20
|
+
# Parses the given Ruby source code file and returns this object.
|
21
|
+
#
|
22
|
+
# @param [String, File] file The filename or File object for the Ruby source code file to parse.
|
23
|
+
#
|
24
|
+
def load_events_file(file)
|
25
|
+
file = File.open(file) if file.kind_of? String
|
26
|
+
instance_eval file.read, file.path
|
27
|
+
self
|
28
|
+
end
|
29
|
+
|
30
|
+
##
|
31
|
+
# Parses the given Ruby source code and returns this object.
|
32
|
+
#
|
33
|
+
# NOTE: Only use this if you're generating the code yourself! If you're loading a file from the filesystem, you should
|
34
|
+
# use load_events_file() since load_events_file() will properly attribute errors in the code to the file from which the
|
35
|
+
# code was loaded.
|
36
|
+
#
|
37
|
+
# @param [String] code The Ruby source code to parse
|
38
|
+
#
|
39
|
+
def load_events_code(code)
|
40
|
+
instance_eval code
|
41
|
+
self
|
42
|
+
end
|
43
|
+
|
44
|
+
protected
|
45
|
+
|
46
|
+
##
|
47
|
+
# Immediately register the namespace and callback with the Theatre instance given to the constructor. This method is only
|
48
|
+
# called when a new BlankSlateMessageRecorder is instantiated and receives #each().
|
49
|
+
#
|
50
|
+
def callback_registered(namespaces, callback)
|
51
|
+
# Get rid of all arguments passed to the namespaces. Will support arguments in the future.
|
52
|
+
namespaces = namespaces.map { |namespace| namespace.first }
|
53
|
+
|
54
|
+
theatre.namespace_manager.register_callback_at_namespace namespaces, callback
|
55
|
+
end
|
56
|
+
|
57
|
+
def create_recorder_method(record_method_name)
|
58
|
+
(class << self; self; end).send(:alias_method, record_method_name, :anonymous_recorder)
|
59
|
+
end
|
60
|
+
|
61
|
+
class BlankSlateMessageRecorder
|
62
|
+
|
63
|
+
(instance_methods - %w[__send__ __id__]).each { |m| undef_method m }
|
64
|
+
|
65
|
+
def initialize(¬ify_on_completion)
|
66
|
+
@notify_on_completion = notify_on_completion
|
67
|
+
@namespaces = []
|
68
|
+
end
|
69
|
+
|
70
|
+
def method_missing(*method_name_and_args)
|
71
|
+
raise ArgumentError, "Supplying a block is not supported" if block_given?
|
72
|
+
@namespaces << method_name_and_args
|
73
|
+
self
|
74
|
+
end
|
75
|
+
|
76
|
+
def each(&callback)
|
77
|
+
@notify_on_completion.call(@namespaces, callback)
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
83
|
+
end
|
data/lib/theatre/guid.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# Right now Adhearsion also defines this method. The eventual solution will be to extract the Adhearsion features on which
|
2
|
+
# Theatre depends and make that a dependent library.
|
3
|
+
|
4
|
+
unless respond_to? :new_guid
|
5
|
+
|
6
|
+
def random_character
|
7
|
+
case random_digit = rand(62)
|
8
|
+
when 0...10 : random_digit.to_s
|
9
|
+
when 10...36 : (random_digit + 55).chr
|
10
|
+
when 36...62 : (random_digit + 61).chr
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def random_string(length_of_string=8)
|
15
|
+
Array.new(length_of_string) { random_character }.join
|
16
|
+
end
|
17
|
+
|
18
|
+
# This GUID implementation doesn't adhere to the RFC which wants to make certain segments based on the MAC address of a
|
19
|
+
# network interface card and other wackiness. It's sufficiently random for our needs.
|
20
|
+
def new_guid
|
21
|
+
[8,4,4,4,12].map { |segment_length| random_string(segment_length) }.join('-')
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,121 @@
|
|
1
|
+
require 'theatre/guid'
|
2
|
+
require 'thread'
|
3
|
+
require 'monitor'
|
4
|
+
|
5
|
+
module Theatre
|
6
|
+
|
7
|
+
##
|
8
|
+
# An Invocation is an object which Theatre generates and returns from Theatre#trigger.
|
9
|
+
#
|
10
|
+
class Invocation
|
11
|
+
|
12
|
+
attr_reader :queued_time, :started_time, :finished_time, :unique_id, :callback, :namespace, :error, :returned_value
|
13
|
+
|
14
|
+
class InvalidStateError < Exception; end
|
15
|
+
|
16
|
+
##
|
17
|
+
# Create a new Invocation.
|
18
|
+
#
|
19
|
+
# @param [String] namespace The "/foo/bar/qaz" path to the namespace to which this Invocation belongs.
|
20
|
+
# @param [Proc] callback The block which should be executed by an Actor scheduler.
|
21
|
+
# @param [Object] payload The message that will be sent to the callback for processing.
|
22
|
+
#
|
23
|
+
def initialize(namespace, callback, payload=:theatre_no_payload)
|
24
|
+
raise ArgumentError, "Callback must be a Proc" unless callback.kind_of? Proc
|
25
|
+
@payload = payload
|
26
|
+
@unique_id = new_guid.freeze
|
27
|
+
@callback = callback
|
28
|
+
@current_state = :new
|
29
|
+
@state_lock = Mutex.new
|
30
|
+
|
31
|
+
# Used just to protect access to the @returned_value instance variable
|
32
|
+
@returned_value_lock = Monitor.new
|
33
|
+
|
34
|
+
# Used when wait() is called to notify all waiting threads by using a ConditionVariable
|
35
|
+
@returned_value_blocker = Monitor::ConditionVariable.new @returned_value_lock
|
36
|
+
end
|
37
|
+
|
38
|
+
def queued
|
39
|
+
with_state_lock do
|
40
|
+
raise InvalidStateError unless @current_state == :new
|
41
|
+
@current_state = :queued
|
42
|
+
@queued_time = Time.now.freeze
|
43
|
+
end
|
44
|
+
true
|
45
|
+
end
|
46
|
+
|
47
|
+
def current_state
|
48
|
+
with_state_lock { @current_state }
|
49
|
+
end
|
50
|
+
|
51
|
+
def start
|
52
|
+
with_state_lock do
|
53
|
+
raise InvalidStateError unless @current_state == :queued
|
54
|
+
@current_state = :running
|
55
|
+
end
|
56
|
+
@started_time = Time.now.freeze
|
57
|
+
|
58
|
+
begin
|
59
|
+
self.returned_value = if @payload.equal? :theatre_no_payload
|
60
|
+
@callback.call
|
61
|
+
else
|
62
|
+
@callback.call @payload
|
63
|
+
end
|
64
|
+
with_state_lock { @current_state = :success }
|
65
|
+
rescue => @error
|
66
|
+
with_state_lock { @current_state = :error }
|
67
|
+
ensure
|
68
|
+
@finished_time = Time.now.freeze
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def execution_duration
|
73
|
+
return nil unless @finished_time
|
74
|
+
@finished_time - @started_time
|
75
|
+
end
|
76
|
+
|
77
|
+
def error?
|
78
|
+
current_state.equal? :error
|
79
|
+
end
|
80
|
+
|
81
|
+
def success?
|
82
|
+
current_state.equal? :success
|
83
|
+
end
|
84
|
+
|
85
|
+
##
|
86
|
+
# When this Invocation has been queued, started, and entered either the :success or :error state, this method will
|
87
|
+
# finally return. Until then, it blocks the Thread.
|
88
|
+
#
|
89
|
+
# @return [Object] The result of invoking this Invocation's callback
|
90
|
+
#
|
91
|
+
def wait
|
92
|
+
with_returned_value_lock { return @returned_value if defined? @returned_value }
|
93
|
+
@returned_value_blocker.wait
|
94
|
+
# Return the returned_value
|
95
|
+
with_returned_value_lock { @returned_value }
|
96
|
+
end
|
97
|
+
|
98
|
+
protected
|
99
|
+
|
100
|
+
##
|
101
|
+
# Protected setter which does some other housework when the returned value is found (such as notifying wait()ers)
|
102
|
+
#
|
103
|
+
# @param [returned_value] The value to set this returned value to.
|
104
|
+
#
|
105
|
+
def returned_value=(returned_value)
|
106
|
+
with_returned_value_lock do
|
107
|
+
@returned_value = returned_value
|
108
|
+
@returned_value_blocker.broadcast
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def with_returned_value_lock(&block)
|
113
|
+
@returned_value_lock.synchronize(&block)
|
114
|
+
end
|
115
|
+
|
116
|
+
def with_state_lock(&block)
|
117
|
+
@state_lock.synchronize(&block)
|
118
|
+
end
|
119
|
+
|
120
|
+
end
|
121
|
+
end
|
@@ -0,0 +1,153 @@
|
|
1
|
+
module Theatre
|
2
|
+
|
3
|
+
##
|
4
|
+
# Manages the hierarchial namespaces of a Theatre. This class is Thread-safe.
|
5
|
+
#
|
6
|
+
class ActorNamespaceManager
|
7
|
+
|
8
|
+
VALID_NAMESPACE = %r{^(/[\w_]+)+$}
|
9
|
+
|
10
|
+
class << self
|
11
|
+
def valid_namespace_path?(namespace_path)
|
12
|
+
namespace_path =~ VALID_NAMESPACE
|
13
|
+
end
|
14
|
+
|
15
|
+
##
|
16
|
+
# Since there are a couple ways to represent namespaces, this is a helper method which will normalize
|
17
|
+
# them into the most practical: an Array of Symbols
|
18
|
+
# @param [String, Array] paths The namespace to register. Can be in "/foo/bar" or *[foo,bar] format
|
19
|
+
def normalize_path_to_array(paths)
|
20
|
+
paths = paths.is_a?(Array) ? paths.flatten : Array(paths)
|
21
|
+
paths.map! { |path_segment| path_segment.kind_of?(String) ? path_segment.split('/') : path_segment }
|
22
|
+
paths.flatten!
|
23
|
+
paths.reject! { |path| path.nil? || (path.kind_of?(String) && path.empty?) }
|
24
|
+
paths.map { |path| path.to_sym }
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
def initialize
|
30
|
+
@registry_lock = Mutex.new
|
31
|
+
@root = RootNamespaceNode.new
|
32
|
+
end
|
33
|
+
|
34
|
+
##
|
35
|
+
# Have this registry recognize a new path and prepare it for callback registrations. All path segements will be created
|
36
|
+
# in order. For example, when registering "/foo/bar/qaz" when no namespaces at all have been registered, this method will
|
37
|
+
# first register "foo", then "bar", then "qaz". If the namespace was already registered, it will not be affected.
|
38
|
+
#
|
39
|
+
# @param [String, Array] paths The namespace to register. Can be in "/foo/bar" or *[foo,bar] format
|
40
|
+
# @return [NamespaceNode] The NamespaceNode representing the path given.
|
41
|
+
# @raise NamespaceNotFound if a segment has not been registered yet
|
42
|
+
#
|
43
|
+
def register_namespace_name(*paths)
|
44
|
+
paths = self.class.normalize_path_to_array paths
|
45
|
+
|
46
|
+
paths.inject(@root) do |node, name|
|
47
|
+
node.register_namespace_name name
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
##
|
52
|
+
# Returns a Proc found after searching with the namespace you provide
|
53
|
+
#
|
54
|
+
# @raise NamespaceNotFound if a segment has not been registered yet
|
55
|
+
#
|
56
|
+
def callbacks_for_namespaces(*paths)
|
57
|
+
search_for_namespace(paths).callbacks
|
58
|
+
end
|
59
|
+
|
60
|
+
##
|
61
|
+
# Find a namespace in the tree.
|
62
|
+
#
|
63
|
+
# @param [Array, String] paths Must be an Array of segments or a name like "/foo/bar/qaz"
|
64
|
+
# @raise NamespaceNotFound if a segment has not been registered yet
|
65
|
+
#
|
66
|
+
def search_for_namespace(paths)
|
67
|
+
paths = self.class.normalize_path_to_array paths
|
68
|
+
path_string = "/"
|
69
|
+
|
70
|
+
found_namespace = paths.inject(@root) do |last_node,this_node_name|
|
71
|
+
raise NamespaceNotFound.new(path_string) if last_node.nil?
|
72
|
+
path_string << this_node_name.to_s
|
73
|
+
last_node.child_named this_node_name
|
74
|
+
end
|
75
|
+
raise NamespaceNotFound.new("/#{paths.join('/')}") unless found_namespace
|
76
|
+
found_namespace
|
77
|
+
end
|
78
|
+
|
79
|
+
##
|
80
|
+
# Registers the given callback at a namespace, assuming the namespace was already registered.
|
81
|
+
#
|
82
|
+
# @param [Array] paths Must be an Array of segments
|
83
|
+
# @param [Proc] callback
|
84
|
+
# @raise NamespaceNotFound if a segment has not been registered yet
|
85
|
+
#
|
86
|
+
def register_callback_at_namespace(paths, callback)
|
87
|
+
raise ArgumentError, "callback must be a Proc" unless callback.kind_of? Proc
|
88
|
+
search_for_namespace(paths).register_callback callback
|
89
|
+
end
|
90
|
+
|
91
|
+
protected
|
92
|
+
|
93
|
+
##
|
94
|
+
# Used by NamespaceManager to build a tree of namespaces. Has a Hash of children which is not
|
95
|
+
# Thread-safe. For Thread-safety, all access should semaphore through the NamespaceManager.
|
96
|
+
class NamespaceNode
|
97
|
+
|
98
|
+
attr_reader :name
|
99
|
+
def initialize(name)
|
100
|
+
@name = name.freeze
|
101
|
+
@children = {}
|
102
|
+
@callbacks = []
|
103
|
+
end
|
104
|
+
|
105
|
+
def register_namespace_name(name)
|
106
|
+
@children[name] ||= NamespaceNode.new(name)
|
107
|
+
end
|
108
|
+
|
109
|
+
def register_callback(callback)
|
110
|
+
@callbacks << callback
|
111
|
+
callback
|
112
|
+
end
|
113
|
+
|
114
|
+
def callbacks
|
115
|
+
@callbacks.clone
|
116
|
+
end
|
117
|
+
|
118
|
+
def delete_callback(callback)
|
119
|
+
@callbacks.delete callback
|
120
|
+
end
|
121
|
+
|
122
|
+
def child_named(name)
|
123
|
+
@children[name]
|
124
|
+
end
|
125
|
+
|
126
|
+
def destroy_namespace(name)
|
127
|
+
@children.delete name
|
128
|
+
end
|
129
|
+
|
130
|
+
def root?
|
131
|
+
false
|
132
|
+
end
|
133
|
+
|
134
|
+
end
|
135
|
+
|
136
|
+
class RootNamespaceNode < NamespaceNode
|
137
|
+
def initialize
|
138
|
+
super :ROOT
|
139
|
+
end
|
140
|
+
def root?
|
141
|
+
true
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
end
|
146
|
+
|
147
|
+
class NamespaceNotFound < Exception
|
148
|
+
def initialize(full_path)
|
149
|
+
super "Could not find #{full_path.inspect} in the namespace registry. Did you register it yet?"
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
end
|
data/theatre.gemspec
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
THEATRE_FILES = %w[
|
2
|
+
MIT-LICENSE
|
3
|
+
README.markdown
|
4
|
+
Rakefile
|
5
|
+
TODO.markdown
|
6
|
+
algorithms.markdown
|
7
|
+
benchmark/growing_usage.rb
|
8
|
+
lib/theatre.rb
|
9
|
+
lib/theatre/dsl/callback_definition_loader.rb
|
10
|
+
lib/theatre/guid.rb
|
11
|
+
lib/theatre/invocation.rb
|
12
|
+
lib/theatre/namespace_manager.rb
|
13
|
+
lib/theatre/version.rb
|
14
|
+
theatre.gemspec
|
15
|
+
]
|
16
|
+
|
17
|
+
Gem::Specification.new do |s|
|
18
|
+
s.name = "theatre"
|
19
|
+
s.version = "0.8.0"
|
20
|
+
|
21
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :require_rubygems_version=
|
22
|
+
|
23
|
+
s.authors = ["Jay Phillips"]
|
24
|
+
s.date = "2008-08-21"
|
25
|
+
|
26
|
+
s.description = "A library for choreographing a dynamic pool of hierarchially organized actors on Ruby v1.8"
|
27
|
+
s.summary = "A library for choreographing a dynamic pool of hierarchially organized actors on Ruby v1.8"
|
28
|
+
|
29
|
+
s.email = "Jay -at- Codemecca.com"
|
30
|
+
|
31
|
+
s.files = THEATRE_FILES
|
32
|
+
|
33
|
+
s.has_rdoc = false
|
34
|
+
|
35
|
+
s.rubyforge_project = "theatre"
|
36
|
+
s.homepage = "http://github.com/jicksta/theatre"
|
37
|
+
|
38
|
+
s.require_paths = ["lib"]
|
39
|
+
s.rubygems_version = "1.2.0"
|
40
|
+
|
41
|
+
end
|
metadata
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: theatre
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.8.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Jay Phillips
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2008-08-21 00:00:00 -07:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description: A library for choreographing a dynamic pool of hierarchially organized actors on Ruby v1.8
|
17
|
+
email: Jay -at- Codemecca.com
|
18
|
+
executables: []
|
19
|
+
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files: []
|
23
|
+
|
24
|
+
files:
|
25
|
+
- MIT-LICENSE
|
26
|
+
- README.markdown
|
27
|
+
- Rakefile
|
28
|
+
- TODO.markdown
|
29
|
+
- algorithms.markdown
|
30
|
+
- benchmark/growing_usage.rb
|
31
|
+
- lib/theatre.rb
|
32
|
+
- lib/theatre/dsl/callback_definition_loader.rb
|
33
|
+
- lib/theatre/guid.rb
|
34
|
+
- lib/theatre/invocation.rb
|
35
|
+
- lib/theatre/namespace_manager.rb
|
36
|
+
- lib/theatre/version.rb
|
37
|
+
- theatre.gemspec
|
38
|
+
has_rdoc: false
|
39
|
+
homepage: http://github.com/jicksta/theatre
|
40
|
+
post_install_message:
|
41
|
+
rdoc_options: []
|
42
|
+
|
43
|
+
require_paths:
|
44
|
+
- lib
|
45
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
46
|
+
requirements:
|
47
|
+
- - ">="
|
48
|
+
- !ruby/object:Gem::Version
|
49
|
+
version: "0"
|
50
|
+
version:
|
51
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - ">="
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: "0"
|
56
|
+
version:
|
57
|
+
requirements: []
|
58
|
+
|
59
|
+
rubyforge_project: theatre
|
60
|
+
rubygems_version: 1.2.0
|
61
|
+
signing_key:
|
62
|
+
specification_version: 2
|
63
|
+
summary: A library for choreographing a dynamic pool of hierarchially organized actors on Ruby v1.8
|
64
|
+
test_files: []
|
65
|
+
|