/tools/Ruby/lib/ruby/1.8/rinda/tuplespace.rb

http://github.com/agross/netopenspace · Ruby · 642 lines · 375 code · 114 blank · 153 comment · 41 complexity · 528f7f0bcdb76f860e49e4e3b9710939 MD5 · raw file

  1. require 'monitor'
  2. require 'thread'
  3. require 'drb/drb'
  4. require 'rinda/rinda'
  5. require 'enumerator'
  6. require 'forwardable'
  7. module Rinda
  8. ##
  9. # A TupleEntry is a Tuple (i.e. a possible entry in some Tuplespace)
  10. # together with expiry and cancellation data.
  11. class TupleEntry
  12. include DRbUndumped
  13. attr_accessor :expires
  14. ##
  15. # Creates a TupleEntry based on +ary+ with an optional renewer or expiry
  16. # time +sec+.
  17. #
  18. # A renewer must implement the +renew+ method which returns a Numeric,
  19. # nil, or true to indicate when the tuple has expired.
  20. def initialize(ary, sec=nil)
  21. @cancel = false
  22. @expires = nil
  23. @tuple = make_tuple(ary)
  24. @renewer = nil
  25. renew(sec)
  26. end
  27. ##
  28. # Marks this TupleEntry as canceled.
  29. def cancel
  30. @cancel = true
  31. end
  32. ##
  33. # A TupleEntry is dead when it is canceled or expired.
  34. def alive?
  35. !canceled? && !expired?
  36. end
  37. ##
  38. # Return the object which makes up the tuple itself: the Array
  39. # or Hash.
  40. def value; @tuple.value; end
  41. ##
  42. # Returns the canceled status.
  43. def canceled?; @cancel; end
  44. ##
  45. # Has this tuple expired? (true/false).
  46. #
  47. # A tuple has expired when its expiry timer based on the +sec+ argument to
  48. # #initialize runs out.
  49. def expired?
  50. return true unless @expires
  51. return false if @expires > Time.now
  52. return true if @renewer.nil?
  53. renew(@renewer)
  54. return true unless @expires
  55. return @expires < Time.now
  56. end
  57. ##
  58. # Reset the expiry time according to +sec_or_renewer+.
  59. #
  60. # +nil+:: it is set to expire in the far future.
  61. # +false+:: it has expired.
  62. # Numeric:: it will expire in that many seconds.
  63. #
  64. # Otherwise the argument refers to some kind of renewer object
  65. # which will reset its expiry time.
  66. def renew(sec_or_renewer)
  67. sec, @renewer = get_renewer(sec_or_renewer)
  68. @expires = make_expires(sec)
  69. end
  70. ##
  71. # Returns an expiry Time based on +sec+ which can be one of:
  72. # Numeric:: +sec+ seconds into the future
  73. # +true+:: the expiry time is the start of 1970 (i.e. expired)
  74. # +nil+:: it is Tue Jan 19 03:14:07 GMT Standard Time 2038 (i.e. when
  75. # UNIX clocks will die)
  76. def make_expires(sec=nil)
  77. case sec
  78. when Numeric
  79. Time.now + sec
  80. when true
  81. Time.at(1)
  82. when nil
  83. Time.at(2**31-1)
  84. end
  85. end
  86. ##
  87. # Retrieves +key+ from the tuple.
  88. def [](key)
  89. @tuple[key]
  90. end
  91. ##
  92. # Fetches +key+ from the tuple.
  93. def fetch(key)
  94. @tuple.fetch(key)
  95. end
  96. ##
  97. # The size of the tuple.
  98. def size
  99. @tuple.size
  100. end
  101. ##
  102. # Creates a Rinda::Tuple for +ary+.
  103. def make_tuple(ary)
  104. Rinda::Tuple.new(ary)
  105. end
  106. private
  107. ##
  108. # Returns a valid argument to make_expires and the renewer or nil.
  109. #
  110. # Given +true+, +nil+, or Numeric, returns that value and +nil+ (no actual
  111. # renewer). Otherwise it returns an expiry value from calling +it.renew+
  112. # and the renewer.
  113. def get_renewer(it)
  114. case it
  115. when Numeric, true, nil
  116. return it, nil
  117. else
  118. begin
  119. return it.renew, it
  120. rescue Exception
  121. return it, nil
  122. end
  123. end
  124. end
  125. end
  126. ##
  127. # A TemplateEntry is a Template together with expiry and cancellation data.
  128. class TemplateEntry < TupleEntry
  129. ##
  130. # Matches this TemplateEntry against +tuple+. See Template#match for
  131. # details on how a Template matches a Tuple.
  132. def match(tuple)
  133. @tuple.match(tuple)
  134. end
  135. alias === match
  136. def make_tuple(ary) # :nodoc:
  137. Rinda::Template.new(ary)
  138. end
  139. end
  140. ##
  141. # <i>Documentation?</i>
  142. class WaitTemplateEntry < TemplateEntry
  143. attr_reader :found
  144. def initialize(place, ary, expires=nil)
  145. super(ary, expires)
  146. @place = place
  147. @cond = place.new_cond
  148. @found = nil
  149. end
  150. def cancel
  151. super
  152. signal
  153. end
  154. def wait
  155. @cond.wait
  156. end
  157. def read(tuple)
  158. @found = tuple
  159. signal
  160. end
  161. def signal
  162. @place.synchronize do
  163. @cond.signal
  164. end
  165. end
  166. end
  167. ##
  168. # A NotifyTemplateEntry is returned by TupleSpace#notify and is notified of
  169. # TupleSpace changes. You may receive either your subscribed event or the
  170. # 'close' event when iterating over notifications.
  171. #
  172. # See TupleSpace#notify_event for valid notification types.
  173. #
  174. # == Example
  175. #
  176. # ts = Rinda::TupleSpace.new
  177. # observer = ts.notify 'write', [nil]
  178. #
  179. # Thread.start do
  180. # observer.each { |t| p t }
  181. # end
  182. #
  183. # 3.times { |i| ts.write [i] }
  184. #
  185. # Outputs:
  186. #
  187. # ['write', [0]]
  188. # ['write', [1]]
  189. # ['write', [2]]
  190. class NotifyTemplateEntry < TemplateEntry
  191. ##
  192. # Creates a new NotifyTemplateEntry that watches +place+ for +event+s that
  193. # match +tuple+.
  194. def initialize(place, event, tuple, expires=nil)
  195. ary = [event, Rinda::Template.new(tuple)]
  196. super(ary, expires)
  197. @queue = Queue.new
  198. @done = false
  199. end
  200. ##
  201. # Called by TupleSpace to notify this NotifyTemplateEntry of a new event.
  202. def notify(ev)
  203. @queue.push(ev)
  204. end
  205. ##
  206. # Retrieves a notification. Raises RequestExpiredError when this
  207. # NotifyTemplateEntry expires.
  208. def pop
  209. raise RequestExpiredError if @done
  210. it = @queue.pop
  211. @done = true if it[0] == 'close'
  212. return it
  213. end
  214. ##
  215. # Yields event/tuple pairs until this NotifyTemplateEntry expires.
  216. def each # :yields: event, tuple
  217. while !@done
  218. it = pop
  219. yield(it)
  220. end
  221. rescue
  222. ensure
  223. cancel
  224. end
  225. end
  226. ##
  227. # TupleBag is an unordered collection of tuples. It is the basis
  228. # of Tuplespace.
  229. class TupleBag
  230. class TupleBin
  231. extend Forwardable
  232. def_delegators '@bin', :find_all, :delete_if, :each, :empty?
  233. def initialize
  234. @bin = []
  235. end
  236. def add(tuple)
  237. @bin.push(tuple)
  238. end
  239. def delete(tuple)
  240. idx = @bin.rindex(tuple)
  241. @bin.delete_at(idx) if idx
  242. end
  243. def find(&blk)
  244. @bin.reverse_each do |x|
  245. return x if yield(x)
  246. end
  247. nil
  248. end
  249. end
  250. def initialize # :nodoc:
  251. @hash = {}
  252. @enum = Enumerable::Enumerator.new(self, :each_entry)
  253. end
  254. ##
  255. # +true+ if the TupleBag to see if it has any expired entries.
  256. def has_expires?
  257. @enum.find do |tuple|
  258. tuple.expires
  259. end
  260. end
  261. ##
  262. # Add +tuple+ to the TupleBag.
  263. def push(tuple)
  264. key = bin_key(tuple)
  265. @hash[key] ||= TupleBin.new
  266. @hash[key].add(tuple)
  267. end
  268. ##
  269. # Removes +tuple+ from the TupleBag.
  270. def delete(tuple)
  271. key = bin_key(tuple)
  272. bin = @hash[key]
  273. return nil unless bin
  274. bin.delete(tuple)
  275. @hash.delete(key) if bin.empty?
  276. tuple
  277. end
  278. ##
  279. # Finds all live tuples that match +template+.
  280. def find_all(template)
  281. bin_for_find(template).find_all do |tuple|
  282. tuple.alive? && template.match(tuple)
  283. end
  284. end
  285. ##
  286. # Finds a live tuple that matches +template+.
  287. def find(template)
  288. bin_for_find(template).find do |tuple|
  289. tuple.alive? && template.match(tuple)
  290. end
  291. end
  292. ##
  293. # Finds all tuples in the TupleBag which when treated as templates, match
  294. # +tuple+ and are alive.
  295. def find_all_template(tuple)
  296. @enum.find_all do |template|
  297. template.alive? && template.match(tuple)
  298. end
  299. end
  300. ##
  301. # Delete tuples which dead tuples from the TupleBag, returning the deleted
  302. # tuples.
  303. def delete_unless_alive
  304. deleted = []
  305. @hash.each do |key, bin|
  306. bin.delete_if do |tuple|
  307. if tuple.alive?
  308. false
  309. else
  310. deleted.push(tuple)
  311. true
  312. end
  313. end
  314. end
  315. deleted
  316. end
  317. private
  318. def each_entry(&blk)
  319. @hash.each do |k, v|
  320. v.each(&blk)
  321. end
  322. end
  323. def bin_key(tuple)
  324. head = tuple[0]
  325. if head.class == Symbol
  326. return head
  327. else
  328. false
  329. end
  330. end
  331. def bin_for_find(template)
  332. key = bin_key(template)
  333. key ? @hash.fetch(key, []) : @enum
  334. end
  335. end
  336. ##
  337. # The Tuplespace manages access to the tuples it contains,
  338. # ensuring mutual exclusion requirements are met.
  339. #
  340. # The +sec+ option for the write, take, move, read and notify methods may
  341. # either be a number of seconds or a Renewer object.
  342. class TupleSpace
  343. include DRbUndumped
  344. include MonitorMixin
  345. ##
  346. # Creates a new TupleSpace. +period+ is used to control how often to look
  347. # for dead tuples after modifications to the TupleSpace.
  348. #
  349. # If no dead tuples are found +period+ seconds after the last
  350. # modification, the TupleSpace will stop looking for dead tuples.
  351. def initialize(period=60)
  352. super()
  353. @bag = TupleBag.new
  354. @read_waiter = TupleBag.new
  355. @take_waiter = TupleBag.new
  356. @notify_waiter = TupleBag.new
  357. @period = period
  358. @keeper = nil
  359. end
  360. ##
  361. # Adds +tuple+
  362. def write(tuple, sec=nil)
  363. entry = create_entry(tuple, sec)
  364. synchronize do
  365. if entry.expired?
  366. @read_waiter.find_all_template(entry).each do |template|
  367. template.read(tuple)
  368. end
  369. notify_event('write', entry.value)
  370. notify_event('delete', entry.value)
  371. else
  372. @bag.push(entry)
  373. start_keeper if entry.expires
  374. @read_waiter.find_all_template(entry).each do |template|
  375. template.read(tuple)
  376. end
  377. @take_waiter.find_all_template(entry).each do |template|
  378. template.signal
  379. end
  380. notify_event('write', entry.value)
  381. end
  382. end
  383. entry
  384. end
  385. ##
  386. # Removes +tuple+
  387. def take(tuple, sec=nil, &block)
  388. move(nil, tuple, sec, &block)
  389. end
  390. ##
  391. # Moves +tuple+ to +port+.
  392. def move(port, tuple, sec=nil)
  393. template = WaitTemplateEntry.new(self, tuple, sec)
  394. yield(template) if block_given?
  395. synchronize do
  396. entry = @bag.find(template)
  397. if entry
  398. port.push(entry.value) if port
  399. @bag.delete(entry)
  400. notify_event('take', entry.value)
  401. return entry.value
  402. end
  403. raise RequestExpiredError if template.expired?
  404. begin
  405. @take_waiter.push(template)
  406. start_keeper if template.expires
  407. while true
  408. raise RequestCanceledError if template.canceled?
  409. raise RequestExpiredError if template.expired?
  410. entry = @bag.find(template)
  411. if entry
  412. port.push(entry.value) if port
  413. @bag.delete(entry)
  414. notify_event('take', entry.value)
  415. return entry.value
  416. end
  417. template.wait
  418. end
  419. ensure
  420. @take_waiter.delete(template)
  421. end
  422. end
  423. end
  424. ##
  425. # Reads +tuple+, but does not remove it.
  426. def read(tuple, sec=nil)
  427. template = WaitTemplateEntry.new(self, tuple, sec)
  428. yield(template) if block_given?
  429. synchronize do
  430. entry = @bag.find(template)
  431. return entry.value if entry
  432. raise RequestExpiredError if template.expired?
  433. begin
  434. @read_waiter.push(template)
  435. start_keeper if template.expires
  436. template.wait
  437. raise RequestCanceledError if template.canceled?
  438. raise RequestExpiredError if template.expired?
  439. return template.found
  440. ensure
  441. @read_waiter.delete(template)
  442. end
  443. end
  444. end
  445. ##
  446. # Returns all tuples matching +tuple+. Does not remove the found tuples.
  447. def read_all(tuple)
  448. template = WaitTemplateEntry.new(self, tuple, nil)
  449. synchronize do
  450. entry = @bag.find_all(template)
  451. entry.collect do |e|
  452. e.value
  453. end
  454. end
  455. end
  456. ##
  457. # Registers for notifications of +event+. Returns a NotifyTemplateEntry.
  458. # See NotifyTemplateEntry for examples of how to listen for notifications.
  459. #
  460. # +event+ can be:
  461. # 'write':: A tuple was added
  462. # 'take':: A tuple was taken or moved
  463. # 'delete':: A tuple was lost after being overwritten or expiring
  464. #
  465. # The TupleSpace will also notify you of the 'close' event when the
  466. # NotifyTemplateEntry has expired.
  467. def notify(event, tuple, sec=nil)
  468. template = NotifyTemplateEntry.new(self, event, tuple, sec)
  469. synchronize do
  470. @notify_waiter.push(template)
  471. end
  472. template
  473. end
  474. private
  475. def create_entry(tuple, sec)
  476. TupleEntry.new(tuple, sec)
  477. end
  478. ##
  479. # Removes dead tuples.
  480. def keep_clean
  481. synchronize do
  482. @read_waiter.delete_unless_alive.each do |e|
  483. e.signal
  484. end
  485. @take_waiter.delete_unless_alive.each do |e|
  486. e.signal
  487. end
  488. @notify_waiter.delete_unless_alive.each do |e|
  489. e.notify(['close'])
  490. end
  491. @bag.delete_unless_alive.each do |e|
  492. notify_event('delete', e.value)
  493. end
  494. end
  495. end
  496. ##
  497. # Notifies all registered listeners for +event+ of a status change of
  498. # +tuple+.
  499. def notify_event(event, tuple)
  500. ev = [event, tuple]
  501. @notify_waiter.find_all_template(ev).each do |template|
  502. template.notify(ev)
  503. end
  504. end
  505. ##
  506. # Creates a thread that scans the tuplespace for expired tuples.
  507. def start_keeper
  508. return if @keeper && @keeper.alive?
  509. @keeper = Thread.new do
  510. while true
  511. sleep(@period)
  512. synchronize do
  513. break unless need_keeper?
  514. keep_clean
  515. end
  516. end
  517. end
  518. end
  519. ##
  520. # Checks the tuplespace to see if it needs cleaning.
  521. def need_keeper?
  522. return true if @bag.has_expires?
  523. return true if @read_waiter.has_expires?
  524. return true if @take_waiter.has_expires?
  525. return true if @notify_waiter.has_expires?
  526. end
  527. end
  528. end