11 # A TupleEntry is a Tuple (i.e. a possible entry in some Tuplespace)
12 # together with expiry and cancellation data.
18 attr_accessor :expires
21 # Creates a TupleEntry based on +ary+ with an optional renewer or expiry
24 # A renewer must implement the +renew+ method which returns a Numeric,
25 # nil, or true to indicate when the tuple has expired.
27 def initialize(ary, sec=nil)
30 @tuple = make_tuple(ary)
36 # Marks this TupleEntry as canceled.
43 # A TupleEntry is dead when it is canceled or expired.
46 !canceled? && !expired?
50 # Return the object which makes up the tuple itself: the Array
53 def value; @tuple.value; end
56 # Returns the canceled status.
58 def canceled?; @cancel; end
61 # Has this tuple expired? (true/false).
63 # A tuple has expired when its expiry timer based on the +sec+ argument to
64 # #initialize runs out.
67 return true unless @expires
68 return false if @expires > Time.now
69 return true if @renewer.nil?
71 return true unless @expires
72 return @expires < Time.now
76 # Reset the expiry time according to +sec_or_renewer+.
78 # +nil+:: it is set to expire in the far future.
79 # +false+:: it has expired.
80 # Numeric:: it will expire in that many seconds.
82 # Otherwise the argument refers to some kind of renewer object
83 # which will reset its expiry time.
85 def renew(sec_or_renewer)
86 sec, @renewer = get_renewer(sec_or_renewer)
87 @expires = make_expires(sec)
91 # Returns an expiry Time based on +sec+ which can be one of:
92 # Numeric:: +sec+ seconds into the future
93 # +true+:: the expiry time is the start of 1970 (i.e. expired)
94 # +nil+:: it is Tue Jan 19 03:14:07 GMT Standard Time 2038 (i.e. when
95 # UNIX clocks will die)
97 def make_expires(sec=nil)
109 # Retrieves +key+ from the tuple.
116 # Fetches +key+ from the tuple.
123 # The size of the tuple.
130 # Creates a Rinda::Tuple for +ary+.
133 Rinda::Tuple.new(ary)
139 # Returns a valid argument to make_expires and the renewer or nil.
141 # Given +true+, +nil+, or Numeric, returns that value and +nil+ (no actual
142 # renewer). Otherwise it returns an expiry value from calling +it.renew+
147 when Numeric, true, nil
161 # A TemplateEntry is a Template together with expiry and cancellation data.
163 class TemplateEntry < TupleEntry
165 # Matches this TemplateEntry against +tuple+. See Template#match for
166 # details on how a Template matches a Tuple.
174 def make_tuple(ary) # :nodoc:
175 Rinda::Template.new(ary)
181 # <i>Documentation?</i>
183 class WaitTemplateEntry < TemplateEntry
187 def initialize(place, ary, expires=nil)
190 @cond = place.new_cond
209 @place.synchronize do
217 # A NotifyTemplateEntry is returned by TupleSpace#notify and is notified of
218 # TupleSpace changes. You may receive either your subscribed event or the
219 # 'close' event when iterating over notifications.
221 # See TupleSpace#notify_event for valid notification types.
225 # ts = Rinda::TupleSpace.new
226 # observer = ts.notify 'write', [nil]
229 # observer.each { |t| p t }
232 # 3.times { |i| ts.write [i] }
240 class NotifyTemplateEntry < TemplateEntry
243 # Creates a new NotifyTemplateEntry that watches +place+ for +event+s that
246 def initialize(place, event, tuple, expires=nil)
247 ary = [event, Rinda::Template.new(tuple)]
254 # Called by TupleSpace to notify this NotifyTemplateEntry of a new event.
261 # Retrieves a notification. Raises RequestExpiredError when this
262 # NotifyTemplateEntry expires.
265 raise RequestExpiredError if @done
267 @done = true if it[0] == 'close'
272 # Yields event/tuple pairs until this NotifyTemplateEntry expires.
274 def each # :yields: event, tuple
287 # TupleBag is an unordered collection of tuples. It is the basis
293 def_delegators '@bin', :find_all, :delete_if, :each, :empty?
304 idx = @bin.rindex(tuple)
305 @bin.delete_at(idx) if idx
309 @bin.reverse_each do |x|
316 def initialize # :nodoc:
318 @enum = Enumerable::Enumerator.new(self, :each_entry)
322 # +true+ if the TupleBag to see if it has any expired entries.
325 @enum.find do |tuple|
331 # Add +tuple+ to the TupleBag.
335 @hash[key] ||= TupleBin.new
336 @hash[key].add(tuple)
340 # Removes +tuple+ from the TupleBag.
345 return nil unless bin
347 @hash.delete(key) if bin.empty?
352 # Finds all live tuples that match +template+.
353 def find_all(template)
354 bin_for_find(template).find_all do |tuple|
355 tuple.alive? && template.match(tuple)
360 # Finds a live tuple that matches +template+.
363 bin_for_find(template).find do |tuple|
364 tuple.alive? && template.match(tuple)
369 # Finds all tuples in the TupleBag which when treated as templates, match
370 # +tuple+ and are alive.
372 def find_all_template(tuple)
373 @enum.find_all do |template|
374 template.alive? && template.match(tuple)
379 # Delete tuples which dead tuples from the TupleBag, returning the deleted
382 def delete_unless_alive
384 @hash.each do |key, bin|
385 bin.delete_if do |tuple|
406 if head.class == Symbol
413 def bin_for_find(template)
414 key = bin_key(template)
415 key ? @hash.fetch(key, []) : @enum
420 # The Tuplespace manages access to the tuples it contains,
421 # ensuring mutual exclusion requirements are met.
423 # The +sec+ option for the write, take, move, read and notify methods may
424 # either be a number of seconds or a Renewer object.
432 # Creates a new TupleSpace. +period+ is used to control how often to look
433 # for dead tuples after modifications to the TupleSpace.
435 # If no dead tuples are found +period+ seconds after the last
436 # modification, the TupleSpace will stop looking for dead tuples.
438 def initialize(period=60)
441 @read_waiter = TupleBag.new
442 @take_waiter = TupleBag.new
443 @notify_waiter = TupleBag.new
451 def write(tuple, sec=nil)
452 entry = create_entry(tuple, sec)
455 @read_waiter.find_all_template(entry).each do |template|
458 notify_event('write', entry.value)
459 notify_event('delete', entry.value)
462 start_keeper if entry.expires
463 @read_waiter.find_all_template(entry).each do |template|
466 @take_waiter.find_all_template(entry).each do |template|
469 notify_event('write', entry.value)
478 def take(tuple, sec=nil, &block)
479 move(nil, tuple, sec, &block)
483 # Moves +tuple+ to +port+.
485 def move(port, tuple, sec=nil)
486 template = WaitTemplateEntry.new(self, tuple, sec)
487 yield(template) if block_given?
489 entry = @bag.find(template)
491 port.push(entry.value) if port
493 notify_event('take', entry.value)
496 raise RequestExpiredError if template.expired?
499 @take_waiter.push(template)
500 start_keeper if template.expires
502 raise RequestCanceledError if template.canceled?
503 raise RequestExpiredError if template.expired?
504 entry = @bag.find(template)
506 port.push(entry.value) if port
508 notify_event('take', entry.value)
514 @take_waiter.delete(template)
520 # Reads +tuple+, but does not remove it.
522 def read(tuple, sec=nil)
523 template = WaitTemplateEntry.new(self, tuple, sec)
524 yield(template) if block_given?
526 entry = @bag.find(template)
527 return entry.value if entry
528 raise RequestExpiredError if template.expired?
531 @read_waiter.push(template)
532 start_keeper if template.expires
534 raise RequestCanceledError if template.canceled?
535 raise RequestExpiredError if template.expired?
536 return template.found
538 @read_waiter.delete(template)
544 # Returns all tuples matching +tuple+. Does not remove the found tuples.
547 template = WaitTemplateEntry.new(self, tuple, nil)
549 entry = @bag.find_all(template)
557 # Registers for notifications of +event+. Returns a NotifyTemplateEntry.
558 # See NotifyTemplateEntry for examples of how to listen for notifications.
561 # 'write':: A tuple was added
562 # 'take':: A tuple was taken or moved
563 # 'delete':: A tuple was lost after being overwritten or expiring
565 # The TupleSpace will also notify you of the 'close' event when the
566 # NotifyTemplateEntry has expired.
568 def notify(event, tuple, sec=nil)
569 template = NotifyTemplateEntry.new(self, event, tuple, sec)
571 @notify_waiter.push(template)
578 def create_entry(tuple, sec)
579 TupleEntry.new(tuple, sec)
583 # Removes dead tuples.
587 @read_waiter.delete_unless_alive.each do |e|
590 @take_waiter.delete_unless_alive.each do |e|
593 @notify_waiter.delete_unless_alive.each do |e|
596 @bag.delete_unless_alive.each do |e|
597 notify_event('delete', e.value)
603 # Notifies all registered listeners for +event+ of a status change of
606 def notify_event(event, tuple)
608 @notify_waiter.find_all_template(ev).each do |template|
614 # Creates a thread that scans the tuplespace for expired tuples.
617 return if @keeper && @keeper.alive?
618 @keeper = Thread.new do
622 break unless need_keeper?
630 # Checks the tuplespace to see if it needs cleaning.
633 return true if @bag.has_expires?
634 return true if @read_waiter.has_expires?
635 return true if @take_waiter.has_expires?
636 return true if @notify_waiter.has_expires?