cosell 0.0.2

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,48 @@
1
+ module Cosell
2
+
3
+ # :stopdoc:
4
+ VERSION = '0.0.2'
5
+ LIBPATH = ::File.expand_path(::File.dirname(__FILE__)) + ::File::SEPARATOR
6
+ PATH = ::File.dirname(LIBPATH) + ::File::SEPARATOR
7
+
8
+ # Returns the version string for the library.
9
+ #
10
+ def self.version
11
+ VERSION
12
+ end
13
+
14
+ # Returns the library path for the module. If any arguments are given,
15
+ # they will be joined to the end of the libray path using
16
+ # <tt>File.join</tt>.
17
+ #
18
+ def self.libpath( *args )
19
+ args.empty? ? LIBPATH : ::File.join(LIBPATH, args.flatten)
20
+ end
21
+
22
+ # Returns the lpath for the module. If any arguments are given,
23
+ # they will be joined to the end of the path using
24
+ # <tt>File.join</tt>.
25
+ #
26
+ def self.path( *args )
27
+ args.empty? ? PATH : ::File.join(PATH, args.flatten)
28
+ end
29
+
30
+ # Utility method used to require all files ending in .rb that lie in the
31
+ # directory below this file that has the same name as the filename passed
32
+ # in. Optionally, a specific _directory_ name can be passed in such that
33
+ # the _filename_ does not have to be equivalent to the directory.
34
+ #
35
+ def self.require_all_libs_relative_to( fname, dir = nil )
36
+ dir ||= ::File.basename(fname, '.*')
37
+ search_me = ::File.expand_path(
38
+ ::File.join(::File.dirname(fname), dir, '**', '*.rb'))
39
+
40
+ Dir.glob(search_me).sort.each {|rb| require rb}
41
+ end
42
+ # :startdoc:
43
+
44
+ end # module Cosell
45
+
46
+ Cosell.require_all_libs_relative_to(__FILE__)
47
+
48
+ # EOF
@@ -0,0 +1,236 @@
1
+ require 'logger'
2
+
3
+ module Cosell
4
+
5
+ def initialize *args
6
+ initialize_cosell!
7
+ super
8
+ end
9
+
10
+ #
11
+ #
12
+ # ANNOUNCEMENTS QUEUE
13
+ #
14
+ #
15
+
16
+ # Place all announcments in a queue, and make announcements in a background thread.
17
+ #
18
+ # Arguments:
19
+ # :logger => a logger. Where to log exceptions and warnings.
20
+ # This argument is mandatory -- it is too hard to debug exceptions in announcement
21
+ # handler code without a logger. If you _really_ want your code to fail silently,
22
+ # you will have to create a logger on /dev/null.
23
+ # :sleep_time => how long to sleep (in seconds) after making a batch of announchements
24
+ # optional arg, default: 0.01
25
+ # :announcements_per_cycle => how many announcements to make before sleeping for sleep_time
26
+ # optional arg, default: 25
27
+ #
28
+ # WARNING: If you do not pass in a logger, announcement code will fail silently (the queue
29
+ # is in a background thread).
30
+ #
31
+ # Note: at the moment, this method may only be called once, and cannot be undone. There is
32
+ # no way to interrupt the thread.
33
+
34
+ def queue_announcements!(opts = {})
35
+
36
+ self.initialize_cosell_if_needed
37
+
38
+ # The logger in mandatory
39
+ if opts[:logger]
40
+ self.queue_logger = opts[:logger]
41
+ else
42
+ raise "Cosell error: You have to provide a logger, otherwise failures in announcement handler code are to hard to debug"
43
+ end
44
+
45
+ # kill off the last queue first
46
+ if self.announcements_thread
47
+ kill_queue!
48
+ sleep 0.01
49
+ queue_announcements! opts
50
+ end
51
+
52
+ self.should_queue_announcements = true
53
+ @__announcements_queue ||= Queue.new
54
+
55
+ how_many_per_cycle = opts[:announcements_per_cycle] || 25
56
+ cycle_duration = opts[:sleep_time] || 0.01
57
+ count = 0
58
+
59
+ self.announcements_thread = Thread.new do
60
+ loop do
61
+ if queue_killed?
62
+ self.kill_announcement_queue = false
63
+ self.announcements_thread = nil
64
+ log "Announcement queue killed with #{self.announcements_queue.size} announcements still queued", :info
65
+ break
66
+ else
67
+ begin
68
+ self.announce_now! self.announcements_queue.pop
69
+ count += 1
70
+ if (count%how_many_per_cycle).eql?(0)
71
+ log "Announcement queue finished batch of #{how_many_per_cycle}, sleeping for #{cycle_duration} sec", :debug
72
+ count = 0
73
+ sleep cycle_duration
74
+ end
75
+ rescue Exception => x
76
+ log "Exception: #{x}, trace: \n\t#{x.backtrace.join("\n\t")}", :error
77
+ end
78
+ end
79
+ end
80
+ end
81
+
82
+ end
83
+
84
+ #
85
+ #
86
+ # SUBSCRIBE/MAKE ANNOUNCEMENTS
87
+ #
88
+ #
89
+
90
+ # Pass in an anouncement class (or array of announcement classes), along with a block defining the
91
+ # action to be taken when an announcment of one of the specified classes is announced by this announcer.
92
+ # (see Cossell::Announcer for full explanation)
93
+ def subscribe *announce_classes, &block
94
+
95
+ self.initialize_cosell_if_needed
96
+
97
+ Array(announce_classes).each do |announce_class|
98
+ raise "Can only subscribe to classes. Not a class: #{announce_class}" unless announce_class.is_a?(Class)
99
+ self.subscriptions[announce_class] ||= []
100
+ self.subscriptions[announce_class] << lambda(&block)
101
+ end
102
+ end
103
+ alias_method :when_announcing, :subscribe
104
+
105
+ # Stop announcing for a given announcement class (or array of classes)
106
+ def unsubscribe *announce_classes
107
+ Array(announce_classes).each do |announce_class|
108
+ self.subscriptions.delete announce_class
109
+ end
110
+ end
111
+
112
+ # If queue_announcements? true, puts announcement in a Queue.
113
+ # Otherwise, calls announce_now!
114
+ # Queued announcements are announced in a background thread in batches
115
+ # (see the #initialize method doc for details).
116
+ def announce announcement
117
+ if self.queue_announcements?
118
+ self.announcements_queue << announcement
119
+ else
120
+ self.announce_now! announcement
121
+ end
122
+ end
123
+
124
+ #
125
+ # First, an announcement is made by calling 'as_announcement' on an_announcement_or_announcement_factory,
126
+ # and subscribers to the announcement's class are then notified
127
+ #
128
+ # subscribers to this announcer will be filtered to those that match to the announcement's class,
129
+ # and those subscriptions will be 'fired'. Subscribers should use the 'subscribe' method (also
130
+ # called 'when_announcing') to configure actions to take when a given announcement is made.
131
+ #
132
+ # Typically, an announcement is passed in for an_announcement_factory, in
133
+ # which case as_announcement does nothing but return the announcement. But any class can override
134
+ # as_announcement to adapt into an anouncement as they see fit.
135
+ #
136
+ # (see Cossell::Announcer for full explanation)
137
+ #
138
+ def announce_now! an_announcement_or_announcement_factory
139
+ announcement = an_announcement_or_announcement_factory.as_announcement
140
+
141
+ self.subscriptions.each do |subscription_type, subscriptions_for_type |
142
+ if announcement.is_a?(subscription_type)
143
+ subscriptions_for_type.each{|subscription| subscription.call(announcement) }
144
+ end
145
+ end
146
+
147
+ return announcement
148
+ end
149
+
150
+ #
151
+ #
152
+ # DEBUG
153
+ #
154
+ #
155
+
156
+ #
157
+ # Log a message every time this announcer makes an announcement
158
+ #
159
+ # Options:
160
+ # :on => Which class of announcements to spy on. Default is Object (ie. all announcements)
161
+ # :logger => The log to log to. Default is a logger on STDOUT
162
+ # :level => The log level to log with. Default is :info
163
+ # :preface_with => A message to prepend to all log messages. Default is "Announcement Spy: "
164
+ def spy!(opts = {})
165
+ on = opts[:on] || Object
166
+ logger = opts[:logger] || Logger.new(STDOUT)
167
+ level = opts[:level] || :info
168
+ preface = opts[:preface_with] || "Announcement Spy: "
169
+ self.subscribe(on){|ann| logger.send(level, "#{preface} #{ann.as_announcement_trace}")}
170
+ end
171
+
172
+ # lazy initialization of cosell.
173
+ # Optional -- calling this will get rid of any subsequent warnings about uninitialized ivs
174
+ # In most cases not necessary, and should never have an effect except to get rid of some warnings.
175
+ def initialize_cosell_if_needed
176
+ self.initialize_cosell! if @__subscriptions.nil?
177
+ end
178
+
179
+ # Will blow away any queue, and reset all state.
180
+ # Should not be necessary to call this, but left public for testing.
181
+ def initialize_cosell!
182
+ # Using pseudo-scoped var names.
183
+ # Unfortunately cant lazily init these w/out ruby warnings going berzerk in verbose mode,
184
+ # So explicitly declaring them here.
185
+ @__queue_announcements ||= false
186
+ @__announcements_queue ||= nil
187
+ @__kill_announcement_queue ||= false
188
+ @__announcements_thread ||= nil
189
+ @__subscriptions ||= {}
190
+ @__queue_logger ||= {}
191
+ end
192
+
193
+ # Kill the announcments queue.
194
+ # This is called automatically if you call queue_announcements!, before starting the next
195
+ # announcments thread, so it's optional. A way of stopping announcments.
196
+ def kill_queue!
197
+ @__kill_announcement_queue = true
198
+ end
199
+
200
+ # return whether annoucements are queued or sent out immediately when the #announce method is called.
201
+ def queue_announcements?
202
+ return @__queue_announcements.eql?(true)
203
+ end
204
+
205
+ def subscriptions= x; @__subscriptions = x; end
206
+ def subscriptions; @__subscriptions ||= []; end
207
+
208
+ protected
209
+
210
+ #:stopdoc:
211
+
212
+ def log(msg, level = :info)
213
+ self.queue_logger.send(level, msg) if self.queue_logger
214
+ end
215
+
216
+ # return whether the queue was killed by kill_queue!
217
+ def queue_killed?
218
+ @__kill_announcement_queue.eql?(true)
219
+ end
220
+
221
+ def queue_logger; @__queue_logger; end
222
+ def queue_logger= x; @__queue_logger = x; end
223
+ def announcements_queue; @__announcements_queue; end
224
+ def announcements_queue= x; @__announcements_queue = x; end
225
+ def announcements_thread; @__announcements_thread; end
226
+ def announcements_thread= x; @__announcements_thread = x; end
227
+ def kill_announcement_queue= x; @__kill_announcement_queue = x; end
228
+ def should_queue_announcements= x; @__queue_announcements = x; end
229
+
230
+ #:startdoc:
231
+ public
232
+
233
+
234
+ end
235
+
236
+
@@ -0,0 +1,32 @@
1
+ #
2
+ # Cosell is intended to be way for objects to
3
+ # communicate throughout the Object graph. It is _supposed_ to be
4
+ # pervasive. As such, it has a few top-level methods that all objects inherit.
5
+ #
6
+ class Object
7
+
8
+ # When an object (or class) is announced, :as_announcement is called, the result of which
9
+ # becomes the announcement. By default just returns self, but can be overridden if appropriate.
10
+ # By default, simply return self.
11
+ def as_announcement
12
+ return self
13
+ end
14
+
15
+ # When cosell is configured to "spy!", the result of announement.as_announcement_trace is what
16
+ # is sent to the spy log. By default just calls 'to_s'.
17
+ def as_announcement_trace
18
+ self.to_s rescue "(Warning: could not create announcement trace)"
19
+ end
20
+
21
+ # When a class is used as an announcment, an empty new instance is created using #allocate.
22
+ # Will raise an exception for those rare classes that cannot #allocate a new instance.
23
+ def self.as_announcement
24
+ new_inst = self.allocate rescue nil
25
+ raise "Cannot create an announcement out of #{self}. Please implement 'as_announcement' as a class method of #{self}." if new_inst.nil?
26
+ new_inst
27
+ end
28
+
29
+ end
30
+
31
+
32
+
@@ -0,0 +1,179 @@
1
+ require File.join(File.dirname(__FILE__), 'spec_helper')
2
+
3
+ #
4
+ # A few announcments
5
+ #
6
+ class AWordFromOurSponsor
7
+ attr_accessor :word
8
+ end
9
+ class KnockOut; end
10
+ class TKO < KnockOut; end
11
+
12
+ #
13
+ # A class whose objects can act as announcers
14
+ #
15
+ class AnyOldClass; include Cosell; end
16
+
17
+ #
18
+ # The tests
19
+ #
20
+ describe Cosell do
21
+
22
+ before(:each) do
23
+ @announcer = AnyOldClass.new
24
+ end
25
+
26
+ it "should instantiate announcement instance from class if needed" do
27
+ @announcer.announce(AWordFromOurSponsor).class.should be_eql(AWordFromOurSponsor)
28
+ @announcer.announce(AWordFromOurSponsor.new).class.should be_eql(AWordFromOurSponsor)
29
+ end
30
+
31
+ it "should execute block specified by subscription" do
32
+
33
+ #@announcer.spy!
34
+
35
+ # After subscribing to KnockOut, make sure it fires whenever
36
+ # KnockOut or it's subclass TKO are announced.
37
+
38
+ what_was_announced = nil
39
+ @announcer.when_announcing(AWordFromOurSponsor, KnockOut) { |ann| what_was_announced = ann }
40
+
41
+ what_was_announced = nil
42
+ @announcer.announce KnockOut
43
+ what_was_announced.should_not be_nil
44
+ what_was_announced.class.should be_eql KnockOut
45
+
46
+ what_was_announced = nil
47
+ @announcer.announce TKO
48
+ what_was_announced.should_not be_nil
49
+ what_was_announced.class.should be_eql TKO
50
+
51
+
52
+ # Do the same thing as above, but announce instances (test above used the class as the announcement)
53
+ # make sure if an announcement instance is announced, that the exact instance is what is announced
54
+
55
+ what_was_announced = nil
56
+ announcement = AWordFromOurSponsor.new
57
+ announcement.word = 'the'
58
+ @announcer.announce announcement
59
+ what_was_announced.should_not be_nil
60
+ what_was_announced.class.should be_eql AWordFromOurSponsor
61
+ what_was_announced.word.should be_eql('the')
62
+
63
+ what_was_announced = nil
64
+ @announcer.announce TKO.new
65
+ what_was_announced.should_not be_nil
66
+ what_was_announced.class.should be_eql TKO
67
+
68
+ end
69
+
70
+ it "should take actions only on announcements of events for which there is a subscription" do
71
+ # Make sure the subscription block fires when an AWordFromOurSponsor is
72
+ # announced, setting what_was_announced to the announcement)
73
+ what_was_announced = nil
74
+ @announcer.when_announcing(KnockOut) { |ann| what_was_announced = ann }
75
+
76
+ @announcer.announce AWordFromOurSponsor
77
+ what_was_announced.should be_nil
78
+
79
+ @announcer.announce AWordFromOurSponsor.new # also test announcement instances
80
+ what_was_announced.should be_nil
81
+
82
+ @announcer.announce TKO # subclass of Knockout, should be announced
83
+ what_was_announced.should_not be_nil
84
+ end
85
+
86
+ it "should be able to subscribe to set of announcements types" do
87
+ what_was_announced = nil
88
+ @announcer.when_announcing(AWordFromOurSponsor, KnockOut) { |ann| what_was_announced = ann }
89
+
90
+ what_was_announced = nil
91
+ @announcer.announce AWordFromOurSponsor
92
+ what_was_announced.should_not be_nil
93
+
94
+ what_was_announced = nil
95
+ @announcer.announce KnockOut
96
+ what_was_announced.should_not be_nil
97
+ end
98
+
99
+ it "should not take actions after unsubscribing" do
100
+ what_was_announced = nil
101
+ @announcer.when_announcing(AWordFromOurSponsor, KnockOut) { |ann| what_was_announced = ann }
102
+ @announcer.announce AWordFromOurSponsor
103
+ what_was_announced.should_not be_nil
104
+
105
+ @announcer.unsubscribe(AWordFromOurSponsor)
106
+ what_was_announced = nil
107
+ @announcer.announce AWordFromOurSponsor
108
+ what_was_announced.should be_nil
109
+ @announcer.announce KnockOut
110
+ what_was_announced.should_not be_nil
111
+ end
112
+
113
+ it "should be able to queue announcements" do
114
+ what_was_announced = nil
115
+ count = 0
116
+ sleep_time = 0.1
117
+ how_many_each_cycle = 77
118
+ @announcer.queue_announcements!(:sleep_time => sleep_time,
119
+ :logger => Logger.new(STDOUT),
120
+ :announcements_per_cycle => how_many_each_cycle)
121
+ @announcer.when_announcing(AWordFromOurSponsor) { |ann| count += 1 }
122
+
123
+ little_bench("time to queue 10_000 announcements"){10_000.times {@announcer.announce AWordFromOurSponsor}}
124
+
125
+ # @announcer.spy! #dbg
126
+
127
+ # Allow announcer thread to do a few batches of announcements, checking the
128
+ # count after each batch. Since we may get to this part of the thread after
129
+ # the announcer has already made a few announcements, use the count at
130
+ # this moment as the starting_count
131
+ start_count = count
132
+ #puts "-------start count: #{count}" # dbg
133
+
134
+ sleep sleep_time + 0.01
135
+ #puts "-------count: #{count}" # dbg
136
+ count.should be_eql(start_count + 1*how_many_each_cycle)
137
+
138
+ sleep sleep_time
139
+ #puts "-------count: #{count}" # dbg
140
+ count.should be_eql(start_count + 2*how_many_each_cycle)
141
+
142
+ sleep sleep_time
143
+ #puts "-------count: #{count}" # dbg
144
+ count.should be_eql(start_count + 3*how_many_each_cycle)
145
+
146
+ # See if killing the queue stops announcments that where queued
147
+ @announcer.kill_queue!
148
+ count_after_queue_stopped = count
149
+ #puts "-------count after stopping: #{count}" # dbg
150
+ sleep sleep_time * 2
151
+ count.should be_eql(count_after_queue_stopped)
152
+
153
+ end
154
+
155
+ # it "should suppress announcements during suppress_announcements block" do
156
+ # # TODO: support for this idiom:
157
+ # notifier.suppress_announcements_during {
158
+ # }
159
+ # and
160
+ # notifier.suppress_announcements(EventType,
161
+ # :during => lambda { "some operation" },
162
+ # :send_unique_events_when_done => true)
163
+ # and
164
+ # notifier.suppress_announcements(EventType,
165
+ # :during => lambda { "some operation" },
166
+ # :send_all_events_when_done => true)
167
+ # end
168
+
169
+ protected
170
+
171
+ def little_bench(msg, &block)
172
+ start = Time.now
173
+ result = block.call
174
+ puts "#{msg}: #{Time.now - start} sec"
175
+ return result
176
+ end
177
+ end
178
+
179
+ # EOF