jicksta-theatre 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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.
@@ -0,0 +1,61 @@
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 handlers 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 handles 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, handled, and has returned. If you wish to synchronously handle the event, simple call `wait` on the `Invocation` object returned from `handle` 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
+ invocation = my_theatre.handle "your/namespace/here", YourSpecialClass.new("this object can be anything")
52
+ invocation.wait
53
+ raise invocation.error if invocation.error?
54
+ log "Actor finished with return value #{invocation.returned_value}"
55
+
56
+ Ruby 1.8 vs. Ruby 1.9 vs. JRuby
57
+ -------------------------------
58
+
59
+ 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.
60
+
61
+ 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.
@@ -0,0 +1,55 @@
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
+ Rake::GemPackageTask.new(GEMSPEC).define
18
+
19
+ desc "Run all RSpecs"
20
+ Spec::Rake::SpecTask.new do |t|
21
+ t.spec_files = FileList[SPEC_GLOB]
22
+ end
23
+
24
+ desc "Compares Theatre's files with those listed in theatre.gemspec"
25
+ task :check_gemspec_files do
26
+
27
+ files_from_gemspec = THEATRE_FILES
28
+ files_from_filesystem = Dir.glob(File.dirname(__FILE__) + "/**/*").map do |filename|
29
+ filename[0...Dir.pwd.length] == Dir.pwd ? filename[(Dir.pwd.length+1)..-1] : filename
30
+ end.sort
31
+ files_from_filesystem.reject! { |f| File.directory? f }
32
+
33
+ puts
34
+ puts 'Pipe this command to "grep -v \'spec/\'" to ignore spec files'
35
+ puts
36
+ puts '##########################################'
37
+ puts '## Files on filesystem not in the gemspec:'
38
+ puts '##########################################'
39
+ puts((files_from_filesystem - files_from_gemspec).map { |f| " " + f })
40
+
41
+
42
+ puts '##########################################'
43
+ puts '## Files in gemspec not in the filesystem:'
44
+ puts '##########################################'
45
+ puts((files_from_gemspec - files_from_filesystem).map { |f| " " + f })
46
+ end
47
+
48
+ desc "Test that the .gemspec file executes"
49
+ task :debug_gem do
50
+ require 'rubygems/specification'
51
+ gemspec = File.read('adhearsion.gemspec')
52
+ spec = nil
53
+ Thread.new { spec = eval("$SAFE = 3\n#{gemspec}") }.join
54
+ puts "SUCCESS: Gemspec runs at the $SAFE level 3."
55
+ end
@@ -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#handle, 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 `handle_syncrhonously` or something.
@@ -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
+ handle_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.handle "/my/special/namespace", :payload
30
+ after_handling = Time.now
31
+
32
+ handle_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
+ handle_count / (handle_count + 1)
41
+
42
+ sleep average_time
43
+
44
+ end
45
+
46
+ end
@@ -0,0 +1,87 @@
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 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
35
+ # @raise Theatre::NamespaceNotFound Raised when told to enqueue an unrecognized namespace
36
+ #
37
+ def handle(namespace, payload)
38
+ callback = NamespaceManager.callbacks_for_namespace(namespace)
39
+ invocation = Invocation.new(namespace, callback, payload)
40
+
41
+ @master_queue << payload
42
+ end
43
+
44
+ ##
45
+ # Starts this Theatre.
46
+ #
47
+ # When this method is called, the Threads are spawned and begin pulling messages off this Theatre's master queue.
48
+ #
49
+ def start!
50
+ return false if @thread_group.list.any? # Already started
51
+ @started_time = Time.now
52
+ @thread_count.times do
53
+ @thread_group.add Thread.new(&method(:thread_loop))
54
+ end
55
+ end
56
+
57
+ ##
58
+ # Notifies all Threads for this Theatre to stop by sending them special messages. Any messages which were queued and
59
+ # unhandled when this method is received will still be processed. Note: you may start this Theatre again later once it
60
+ # has been stopped.
61
+ #
62
+ def graceful_stop!
63
+ @thread_count.times { @master_queue << :THEATRE_SHUTDOWN! }
64
+ @started_time = nil
65
+ end
66
+
67
+ protected
68
+
69
+ def warn(message)
70
+ # Not really implemented yet.
71
+ end
72
+
73
+ def thread_loop
74
+ loop do
75
+ begin
76
+ next_invocation = @master_queue.pop
77
+ return :stopped if next_invocation.equal? :THEATRE_SHUTDOWN!
78
+ next_invocation.start
79
+ rescue => error
80
+ warn error
81
+ end
82
+ end
83
+ end
84
+
85
+ end
86
+
87
+ 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(&notify_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
@@ -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,119 @@
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#handle.
9
+ #
10
+ class Invocation
11
+
12
+ attr_reader :queued_time, :started_time, :finished_time, :unique_id, :callback, :namespace, :error, :returned_value
13
+
14
+ ##
15
+ # Create a new Invocation.
16
+ #
17
+ # @param [String] namespace The "/foo/bar/qaz" path to the namespace to which this Invocation belongs.
18
+ # @param [Proc] callback The block which should be executed by an Actor scheduler.
19
+ # @param [Object] payload The message that will be sent to the callback for processing.
20
+ #
21
+ def initialize(namespace, callback, payload=:theatre_no_payload)
22
+ @payload = payload
23
+ @unique_id = new_guid.freeze
24
+ @callback = callback
25
+ @current_state = :new
26
+ @state_lock = Mutex.new
27
+
28
+ # Used just to protect access to the @returned_value instance variable
29
+ @returned_value_lock = Monitor.new
30
+
31
+ # Used when wait() is called to notify all waiting threads by using a ConditionVariable
32
+ @returned_value_blocker = Monitor::ConditionVariable.new @returned_value_lock
33
+ end
34
+
35
+ def queued
36
+ with_state_lock do
37
+ return false unless @current_state == :new
38
+ @current_state = :queued
39
+ @queued_time = Time.now.freeze
40
+ end
41
+ true
42
+ end
43
+
44
+ def current_state
45
+ with_state_lock { @current_state }
46
+ end
47
+
48
+ def start
49
+ with_state_lock do
50
+ return false unless @current_state == :queued
51
+ @current_state = :running
52
+ end
53
+
54
+ @started_time = Time.now.freeze
55
+
56
+ begin
57
+ self.returned_value = if @payload.equal? :theatre_no_payload
58
+ @callback.call
59
+ else
60
+ @callback.call @payload
61
+ end
62
+ with_state_lock { @current_state = :success }
63
+ rescue => @error
64
+ with_state_lock { @current_state = :error }
65
+ ensure
66
+ @finished_time = Time.now.freeze
67
+ end
68
+ end
69
+
70
+ def execution_duration
71
+ return nil unless @finished_time
72
+ @finished_time - @started_time
73
+ end
74
+
75
+ def error?
76
+ current_state.equal? :error
77
+ end
78
+
79
+ def success?
80
+ current_state.equal? :success
81
+ end
82
+
83
+ ##
84
+ # When this Invocation has been queued, started, and entered either the :success or :error state, this method will
85
+ # finally return. Until then, it blocks the Thread.
86
+ #
87
+ # @return [Object] The result of invoking this Invocation's callback
88
+ #
89
+ def wait
90
+ with_returned_value_lock { return @returned_value if defined? @returned_value }
91
+ @returned_value_blocker.wait
92
+ # Return the returned_value
93
+ with_returned_value_lock { @returned_value }
94
+ end
95
+
96
+ protected
97
+
98
+ ##
99
+ # Protected setter which does some other housework when the returned value is found (such as notifying wait()ers)
100
+ #
101
+ # @param [returned_value] The value to set this returned value to.
102
+ #
103
+ def returned_value=(returned_value)
104
+ with_returned_value_lock do
105
+ @returned_value = returned_value
106
+ @returned_value_blocker.broadcast
107
+ end
108
+ end
109
+
110
+ def with_returned_value_lock(&block)
111
+ @returned_value_lock.synchronize(&block)
112
+ end
113
+
114
+ def with_state_lock(&block)
115
+ @state_lock.synchronize(&block)
116
+ end
117
+
118
+ end
119
+ end
@@ -0,0 +1,160 @@
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 callback_for_namespaces(*paths)
57
+ search_for_namespace(paths).callbacks
58
+ end
59
+
60
+ def load_events_code(code, *args)
61
+ CallbackDefinitionLoader.new(self, *args).load_events_code(code)
62
+ end
63
+
64
+ def load_events_file(file, *args)
65
+ CallbackDefinitionLoader.new(self, *args).load_events_file(file)
66
+ end
67
+
68
+ ##
69
+ # Find a namespace in the tree.
70
+ #
71
+ # @param [Array, String] paths Must be an Array of segments or a name like "/foo/bar/qaz"
72
+ # @raise NamespaceNotFound if a segment has not been registered yet
73
+ #
74
+ def search_for_namespace(paths)
75
+ paths = self.class.normalize_path_to_array paths
76
+ path_string = "/"
77
+
78
+ found_namespace = paths.inject(@root) do |last_node,this_node_name|
79
+ raise NamespaceNotFound.new(path_string) if last_node.nil?
80
+ path_string << this_node_name.to_s
81
+ last_node.child_named this_node_name
82
+ end
83
+ raise NamespaceNotFound.new("/#{paths.join('/')}") unless found_namespace
84
+ found_namespace
85
+ end
86
+
87
+ ##
88
+ # Registers the given callback at a namespace, assuming the namespace was already registered.
89
+ #
90
+ # @param [Array] paths Must be an Array of segments
91
+ # @param [Proc] callback
92
+ # @raise NamespaceNotFound if a segment has not been registered yet
93
+ #
94
+ def register_callback_at_namespace(paths, callback)
95
+ search_for_namespace(paths).register_callback callback
96
+ end
97
+
98
+ protected
99
+
100
+ ##
101
+ # Used by NamespaceManager to build a tree of namespaces. Has a Hash of children which is not
102
+ # Thread-safe. For Thread-safety, all access should semaphore through the NamespaceManager.
103
+ class NamespaceNode
104
+
105
+ attr_reader :name
106
+ def initialize(name)
107
+ @name = name.freeze
108
+ @children = {}
109
+ @callbacks = []
110
+ end
111
+
112
+ def register_namespace_name(name)
113
+ @children[name] ||= NamespaceNode.new(name)
114
+ end
115
+
116
+ def register_callback(callback)
117
+ @callbacks << callbacks
118
+ callback
119
+ end
120
+
121
+ def callbacks
122
+ @callbacks.clone
123
+ end
124
+
125
+ def delete_callback(callback)
126
+ @callbacks.delete callback
127
+ end
128
+
129
+ def child_named(name)
130
+ @children[name]
131
+ end
132
+
133
+ def destroy_namespace(name)
134
+ @children.delete name
135
+ end
136
+
137
+ def root?
138
+ false
139
+ end
140
+
141
+ end
142
+
143
+ class RootNamespaceNode < NamespaceNode
144
+ def initialize
145
+ super :ROOT
146
+ end
147
+ def root?
148
+ true
149
+ end
150
+ end
151
+
152
+ end
153
+
154
+ class NamespaceNotFound < Exception
155
+ def initialize(full_path)
156
+ super "Could not find #{full_path.inspect} in the namespace registry. Did you register it yet?"
157
+ end
158
+ end
159
+
160
+ end
@@ -0,0 +1,2 @@
1
+ THEATRE_VERSION_MAJOR, THEATRE_VERSION_MINOR, THEATRE_VERSION_REVISION = THEATRE_VERSION = [0,8,0]
2
+ THEATRE_VERSION_STRING = THEATRE_VERSION.join '.'
@@ -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: jicksta-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
+