rinda 0.1.0

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