cosell 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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