9 # A TupleEntry is a Tuple (i.e. a possible entry in some Tuplespace)
10 # together with expiry and cancellation data.
16 attr_accessor :expires
19 # Creates a TupleEntry based on +ary+ with an optional renewer or expiry
22 # A renewer must implement the +renew+ method which returns a Numeric,
23 # nil, or true to indicate when the tuple has expired.
25 def initialize(ary, sec=nil)
28 @tuple = make_tuple(ary)
34 # Marks this TupleEntry as canceled.
41 # A TupleEntry is dead when it is canceled or expired.
44 !canceled? && !expired?
48 # Return the object which makes up the tuple itself: the Array
51 def value; @tuple.value; end
54 # Returns the canceled status.
56 def canceled?; @cancel; end
59 # Has this tuple expired? (true/false).
61 # A tuple has expired when its expiry timer based on the +sec+ argument to
62 # #initialize runs out.
65 return true unless @expires
66 return false if @expires > Time.now
67 return true if @renewer.nil?
69 return true unless @expires
70 return @expires < Time.now
74 # Reset the expiry time according to +sec_or_renewer+.
76 # +nil+:: it is set to expire in the far future.
77 # +false+:: it has expired.
78 # Numeric:: it will expire in that many seconds.
80 # Otherwise the argument refers to some kind of renewer object
81 # which will reset its expiry time.
83 def renew(sec_or_renewer)
84 sec, @renewer = get_renewer(sec_or_renewer)
85 @expires = make_expires(sec)
89 # Returns an expiry Time based on +sec+ which can be one of:
90 # Numeric:: +sec+ seconds into the future
91 # +true+:: the expiry time is the start of 1970 (i.e. expired)
92 # +nil+:: it is Tue Jan 19 03:14:07 GMT Standard Time 2038 (i.e. when
93 # UNIX clocks will die)
95 def make_expires(sec=nil)
107 # Retrieves +key+ from the tuple.
114 # Fetches +key+ from the tuple.
121 # The size of the tuple.
128 # Creates a Rinda::Tuple for +ary+.
131 Rinda::Tuple.new(ary)
137 # Returns a valid argument to make_expires and the renewer or nil.
139 # Given +true+, +nil+, or Numeric, returns that value and +nil+ (no actual
140 # renewer). Otherwise it returns an expiry value from calling +it.renew+
145 when Numeric, true, nil
159 # A TemplateEntry is a Template together with expiry and cancellation data.
161 class TemplateEntry < TupleEntry
163 # Matches this TemplateEntry against +tuple+. See Template#match for
164 # details on how a Template matches a Tuple.
172 def make_tuple(ary) # :nodoc:
173 Rinda::Template.new(ary)
179 # <i>Documentation?</i>
181 class WaitTemplateEntry < TemplateEntry
185 def initialize(place, ary, expires=nil)
188 @cond = place.new_cond
207 @place.synchronize do
215 # A NotifyTemplateEntry is returned by TupleSpace#notify and is notified of
216 # TupleSpace changes. You may receive either your subscribed event or the
217 # 'close' event when iterating over notifications.
219 # See TupleSpace#notify_event for valid notification types.
223 # ts = Rinda::TupleSpace.new
224 # observer = ts.notify 'write', [nil]
227 # observer.each { |t| p t }
230 # 3.times { |i| ts.write [i] }
238 class NotifyTemplateEntry < TemplateEntry
241 # Creates a new NotifyTemplateEntry that watches +place+ for +event+s that
244 def initialize(place, event, tuple, expires=nil)
245 ary = [event, Rinda::Template.new(tuple)]
252 # Called by TupleSpace to notify this NotifyTemplateEntry of a new event.
259 # Retrieves a notification. Raises RequestExpiredError when this
260 # NotifyTemplateEntry expires.
263 raise RequestExpiredError if @done
265 @done = true if it[0] == 'close'
270 # Yields event/tuple pairs until this NotifyTemplateEntry expires.
272 def each # :yields: event, tuple
285 # TupleBag is an unordered collection of tuples. It is the basis
290 def initialize # :nodoc:
295 # +true+ if the TupleBag to see if it has any expired entries.
300 return true if tuple.expires
307 # Add +ary+ to the TupleBag.
312 @hash[size].push(ary)
316 # Removes +ary+ from the TupleBag.
320 @hash.fetch(size, []).delete(ary)
324 # Finds all live tuples that match +template+.
326 def find_all(template)
327 @hash.fetch(template.size, []).find_all do |tuple|
328 tuple.alive? && template.match(tuple)
333 # Finds a live tuple that matches +template+.
336 @hash.fetch(template.size, []).find do |tuple|
337 tuple.alive? && template.match(tuple)
342 # Finds all tuples in the TupleBag which when treated as templates, match
343 # +tuple+ and are alive.
345 def find_all_template(tuple)
346 @hash.fetch(tuple.size, []).find_all do |template|
347 template.alive? && template.match(tuple)
352 # Delete tuples which dead tuples from the TupleBag, returning the deleted
355 def delete_unless_alive
357 @hash.keys.each do |size|
359 @hash[size].each do |tuple|
374 # The Tuplespace manages access to the tuples it contains,
375 # ensuring mutual exclusion requirements are met.
377 # The +sec+ option for the write, take, move, read and notify methods may
378 # either be a number of seconds or a Renewer object.
386 # Creates a new TupleSpace. +period+ is used to control how often to look
387 # for dead tuples after modifications to the TupleSpace.
389 # If no dead tuples are found +period+ seconds after the last
390 # modification, the TupleSpace will stop looking for dead tuples.
392 def initialize(period=60)
395 @read_waiter = TupleBag.new
396 @take_waiter = TupleBag.new
397 @notify_waiter = TupleBag.new
405 def write(tuple, sec=nil)
406 entry = TupleEntry.new(tuple, sec)
410 @read_waiter.find_all_template(entry).each do |template|
413 notify_event('write', entry.value)
414 notify_event('delete', entry.value)
417 @read_waiter.find_all_template(entry).each do |template|
420 @take_waiter.find_all_template(entry).each do |template|
423 notify_event('write', entry.value)
432 def take(tuple, sec=nil, &block)
433 move(nil, tuple, sec, &block)
437 # Moves +tuple+ to +port+.
439 def move(port, tuple, sec=nil)
440 template = WaitTemplateEntry.new(self, tuple, sec)
441 yield(template) if block_given?
444 entry = @bag.find(template)
446 port.push(entry.value) if port
448 notify_event('take', entry.value)
451 raise RequestExpiredError if template.expired?
454 @take_waiter.push(template)
456 raise RequestCanceledError if template.canceled?
457 raise RequestExpiredError if template.expired?
458 entry = @bag.find(template)
460 port.push(entry.value) if port
462 notify_event('take', entry.value)
468 @take_waiter.delete(template)
474 # Reads +tuple+, but does not remove it.
476 def read(tuple, sec=nil)
477 template = WaitTemplateEntry.new(self, tuple, sec)
478 yield(template) if block_given?
481 entry = @bag.find(template)
482 return entry.value if entry
483 raise RequestExpiredError if template.expired?
486 @read_waiter.push(template)
488 raise RequestCanceledError if template.canceled?
489 raise RequestExpiredError if template.expired?
490 return template.found
492 @read_waiter.delete(template)
498 # Returns all tuples matching +tuple+. Does not remove the found tuples.
501 template = WaitTemplateEntry.new(self, tuple, nil)
503 entry = @bag.find_all(template)
511 # Registers for notifications of +event+. Returns a NotifyTemplateEntry.
512 # See NotifyTemplateEntry for examples of how to listen for notifications.
515 # 'write':: A tuple was added
516 # 'take':: A tuple was taken or moved
517 # 'delete':: A tuple was lost after being overwritten or expiring
519 # The TupleSpace will also notify you of the 'close' event when the
520 # NotifyTemplateEntry has expired.
522 def notify(event, tuple, sec=nil)
523 template = NotifyTemplateEntry.new(self, event, tuple, sec)
525 @notify_waiter.push(template)
533 # Removes dead tuples.
537 @read_waiter.delete_unless_alive.each do |e|
540 @take_waiter.delete_unless_alive.each do |e|
543 @notify_waiter.delete_unless_alive.each do |e|
546 @bag.delete_unless_alive.each do |e|
547 notify_event('delete', e.value)
553 # Notifies all registered listeners for +event+ of a status change of
556 def notify_event(event, tuple)
558 @notify_waiter.find_all_template(ev).each do |template|
564 # Creates a thread that scans the tuplespace for expired tuples.
567 return if @keeper && @keeper.alive?
568 @keeper = Thread.new do
577 # Checks the tuplespace to see if it needs cleaning.
580 return true if @bag.has_expires?
581 return true if @read_waiter.has_expires?
582 return true if @take_waiter.has_expires?
583 return true if @notify_waiter.has_expires?