rubysl-rinda 1.0.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,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 569d3924674613e8ef815b980e434f1f9083948c
4
+ data.tar.gz: bd62bc505d9fe5770dd53c548898b07d3f3b7531
5
+ SHA512:
6
+ metadata.gz: a9e02962e23175532cd263856f609c326ba754321e2922e4f86ae364af4a478c9c85fdd47c4279b8f205893b225d2968f10a8ee16c2d66bb27243ba8ee1f072a
7
+ data.tar.gz: df2c144c6265a21fc8c64feefd49a08868229f91b4b15361efe9ec953c151517585210c51b14f14d73d7762c64e3c53e9d98b03f76a8029deb074e86d5609b69
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
@@ -0,0 +1,8 @@
1
+ language: ruby
2
+ before_install:
3
+ - gem update --system
4
+ - gem --version
5
+ - gem install rubysl-bundler
6
+ script: bundle exec mspec spec
7
+ rvm:
8
+ - rbx-nightly-18mode
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in rubysl-rinda.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,25 @@
1
+ Copyright (c) 2013, Brian Shirai
2
+ All rights reserved.
3
+
4
+ Redistribution and use in source and binary forms, with or without
5
+ modification, are permitted provided that the following conditions are met:
6
+
7
+ 1. Redistributions of source code must retain the above copyright notice, this
8
+ list of conditions and the following disclaimer.
9
+ 2. Redistributions in binary form must reproduce the above copyright notice,
10
+ this list of conditions and the following disclaimer in the documentation
11
+ and/or other materials provided with the distribution.
12
+ 3. Neither the name of the library nor the names of its contributors may be
13
+ used to endorse or promote products derived from this software without
14
+ specific prior written permission.
15
+
16
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
17
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
18
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
19
+ DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY DIRECT,
20
+ INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
21
+ BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22
+ DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
23
+ OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
24
+ NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
25
+ EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,29 @@
1
+ # Rubysl::Rinda
2
+
3
+ TODO: Write a gem description
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'rubysl-rinda'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install rubysl-rinda
18
+
19
+ ## Usage
20
+
21
+ TODO: Write usage instructions here
22
+
23
+ ## Contributing
24
+
25
+ 1. Fork it
26
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
27
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
28
+ 4. Push to the branch (`git push origin my-new-feature`)
29
+ 5. Create new Pull Request
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,283 @@
1
+ require 'drb/drb'
2
+ require 'thread'
3
+
4
+ ##
5
+ # A module to implement the Linda distributed computing paradigm in Ruby.
6
+ #
7
+ # Rinda is part of DRb (dRuby).
8
+ #
9
+ # == Example(s)
10
+ #
11
+ # See the sample/drb/ directory in the Ruby distribution, from 1.8.2 onwards.
12
+ #
13
+ #--
14
+ # TODO
15
+ # == Introduction to Linda/rinda?
16
+ #
17
+ # == Why is this library separate from DRb?
18
+
19
+ module Rinda
20
+
21
+ ##
22
+ # Rinda error base class
23
+
24
+ class RindaError < RuntimeError; end
25
+
26
+ ##
27
+ # Raised when a hash-based tuple has an invalid key.
28
+
29
+ class InvalidHashTupleKey < RindaError; end
30
+
31
+ ##
32
+ # Raised when trying to use a canceled tuple.
33
+
34
+ class RequestCanceledError < ThreadError; end
35
+
36
+ ##
37
+ # Raised when trying to use an expired tuple.
38
+
39
+ class RequestExpiredError < ThreadError; end
40
+
41
+ ##
42
+ # A tuple is the elementary object in Rinda programming.
43
+ # Tuples may be matched against templates if the tuple and
44
+ # the template are the same size.
45
+
46
+ class Tuple
47
+
48
+ ##
49
+ # Creates a new Tuple from +ary_or_hash+ which must be an Array or Hash.
50
+
51
+ def initialize(ary_or_hash)
52
+ if hash?(ary_or_hash)
53
+ init_with_hash(ary_or_hash)
54
+ else
55
+ init_with_ary(ary_or_hash)
56
+ end
57
+ end
58
+
59
+ ##
60
+ # The number of elements in the tuple.
61
+
62
+ def size
63
+ @tuple.size
64
+ end
65
+
66
+ ##
67
+ # Accessor method for elements of the tuple.
68
+
69
+ def [](k)
70
+ @tuple[k]
71
+ end
72
+
73
+ ##
74
+ # Fetches item +k+ from the tuple.
75
+
76
+ def fetch(k)
77
+ @tuple.fetch(k)
78
+ end
79
+
80
+ ##
81
+ # Iterate through the tuple, yielding the index or key, and the
82
+ # value, thus ensuring arrays are iterated similarly to hashes.
83
+
84
+ def each # FIXME
85
+ if Hash === @tuple
86
+ @tuple.each { |k, v| yield(k, v) }
87
+ else
88
+ @tuple.each_with_index { |v, k| yield(k, v) }
89
+ end
90
+ end
91
+
92
+ ##
93
+ # Return the tuple itself
94
+ def value
95
+ @tuple
96
+ end
97
+
98
+ private
99
+
100
+ def hash?(ary_or_hash)
101
+ ary_or_hash.respond_to?(:keys)
102
+ end
103
+
104
+ ##
105
+ # Munges +ary+ into a valid Tuple.
106
+
107
+ def init_with_ary(ary)
108
+ @tuple = Array.new(ary.size)
109
+ @tuple.size.times do |i|
110
+ @tuple[i] = ary[i]
111
+ end
112
+ end
113
+
114
+ ##
115
+ # Ensures +hash+ is a valid Tuple.
116
+
117
+ def init_with_hash(hash)
118
+ @tuple = Hash.new
119
+ hash.each do |k, v|
120
+ raise InvalidHashTupleKey unless String === k
121
+ @tuple[k] = v
122
+ end
123
+ end
124
+
125
+ end
126
+
127
+ ##
128
+ # Templates are used to match tuples in Rinda.
129
+
130
+ class Template < Tuple
131
+
132
+ ##
133
+ # Matches this template against +tuple+. The +tuple+ must be the same
134
+ # size as the template. An element with a +nil+ value in a template acts
135
+ # as a wildcard, matching any value in the corresponding position in the
136
+ # tuple. Elements of the template match the +tuple+ if the are #== or
137
+ # #===.
138
+ #
139
+ # Template.new([:foo, 5]).match Tuple.new([:foo, 5]) # => true
140
+ # Template.new([:foo, nil]).match Tuple.new([:foo, 5]) # => true
141
+ # Template.new([String]).match Tuple.new(['hello']) # => true
142
+ #
143
+ # Template.new([:foo]).match Tuple.new([:foo, 5]) # => false
144
+ # Template.new([:foo, 6]).match Tuple.new([:foo, 5]) # => false
145
+ # Template.new([:foo, nil]).match Tuple.new([:foo]) # => false
146
+ # Template.new([:foo, 6]).match Tuple.new([:foo]) # => false
147
+
148
+ def match(tuple)
149
+ return false unless tuple.respond_to?(:size)
150
+ return false unless tuple.respond_to?(:fetch)
151
+ return false unless self.size == tuple.size
152
+ each do |k, v|
153
+ begin
154
+ it = tuple.fetch(k)
155
+ rescue
156
+ return false
157
+ end
158
+ next if v.nil?
159
+ next if v == it
160
+ next if v === it
161
+ return false
162
+ end
163
+ return true
164
+ end
165
+
166
+ ##
167
+ # Alias for #match.
168
+
169
+ def ===(tuple)
170
+ match(tuple)
171
+ end
172
+
173
+ end
174
+
175
+ ##
176
+ # <i>Documentation?</i>
177
+
178
+ class DRbObjectTemplate
179
+
180
+ ##
181
+ # Creates a new DRbObjectTemplate that will match against +uri+ and +ref+.
182
+
183
+ def initialize(uri=nil, ref=nil)
184
+ @drb_uri = uri
185
+ @drb_ref = ref
186
+ end
187
+
188
+ ##
189
+ # This DRbObjectTemplate matches +ro+ if the remote object's drburi and
190
+ # drbref are the same. +nil+ is used as a wildcard.
191
+
192
+ def ===(ro)
193
+ return true if super(ro)
194
+ unless @drb_uri.nil?
195
+ return false unless (@drb_uri === ro.__drburi rescue false)
196
+ end
197
+ unless @drb_ref.nil?
198
+ return false unless (@drb_ref === ro.__drbref rescue false)
199
+ end
200
+ true
201
+ end
202
+
203
+ end
204
+
205
+ ##
206
+ # TupleSpaceProxy allows a remote Tuplespace to appear as local.
207
+
208
+ class TupleSpaceProxy
209
+
210
+ ##
211
+ # Creates a new TupleSpaceProxy to wrap +ts+.
212
+
213
+ def initialize(ts)
214
+ @ts = ts
215
+ end
216
+
217
+ ##
218
+ # Adds +tuple+ to the proxied TupleSpace. See TupleSpace#write.
219
+
220
+ def write(tuple, sec=nil)
221
+ @ts.write(tuple, sec)
222
+ end
223
+
224
+ ##
225
+ # Takes +tuple+ from the proxied TupleSpace. See TupleSpace#take.
226
+
227
+ def take(tuple, sec=nil, &block)
228
+ port = []
229
+ @ts.move(DRbObject.new(port), tuple, sec, &block)
230
+ port[0]
231
+ end
232
+
233
+ ##
234
+ # Reads +tuple+ from the proxied TupleSpace. See TupleSpace#read.
235
+
236
+ def read(tuple, sec=nil, &block)
237
+ @ts.read(tuple, sec, &block)
238
+ end
239
+
240
+ ##
241
+ # Reads all tuples matching +tuple+ from the proxied TupleSpace. See
242
+ # TupleSpace#read_all.
243
+
244
+ def read_all(tuple)
245
+ @ts.read_all(tuple)
246
+ end
247
+
248
+ ##
249
+ # Registers for notifications of event +ev+ on the proxied TupleSpace.
250
+ # See TupleSpace#notify
251
+
252
+ def notify(ev, tuple, sec=nil)
253
+ @ts.notify(ev, tuple, sec)
254
+ end
255
+
256
+ end
257
+
258
+ ##
259
+ # An SimpleRenewer allows a TupleSpace to check if a TupleEntry is still
260
+ # alive.
261
+
262
+ class SimpleRenewer
263
+
264
+ include DRbUndumped
265
+
266
+ ##
267
+ # Creates a new SimpleRenewer that keeps an object alive for another +sec+
268
+ # seconds.
269
+
270
+ def initialize(sec=180)
271
+ @sec = sec
272
+ end
273
+
274
+ ##
275
+ # Called by the TupleSpace to check if the object is still alive.
276
+
277
+ def renew
278
+ @sec
279
+ end
280
+ end
281
+
282
+ end
283
+
@@ -0,0 +1,271 @@
1
+ #
2
+ # Note: Rinda::Ring API is unstable.
3
+ #
4
+ require 'drb/drb'
5
+ require 'rinda/rinda'
6
+ require 'thread'
7
+
8
+ module Rinda
9
+
10
+ ##
11
+ # The default port Ring discovery will use.
12
+
13
+ Ring_PORT = 7647
14
+
15
+ ##
16
+ # A RingServer allows a Rinda::TupleSpace to be located via UDP broadcasts.
17
+ # Service location uses the following steps:
18
+ #
19
+ # 1. A RingServer begins listening on the broadcast UDP address.
20
+ # 2. A RingFinger sends a UDP packet containing the DRb URI where it will
21
+ # listen for a reply.
22
+ # 3. The RingServer recieves the UDP packet and connects back to the
23
+ # provided DRb URI with the DRb service.
24
+
25
+ class RingServer
26
+
27
+ include DRbUndumped
28
+
29
+ ##
30
+ # Advertises +ts+ on the UDP broadcast address at +port+.
31
+
32
+ def initialize(ts, port=Ring_PORT)
33
+ @ts = ts
34
+ @soc = UDPSocket.open
35
+ @soc.bind('', port)
36
+ @w_service = write_service
37
+ @r_service = reply_service
38
+ end
39
+
40
+ ##
41
+ # Creates a thread that picks up UDP packets and passes them to do_write
42
+ # for decoding.
43
+
44
+ def write_service
45
+ Thread.new do
46
+ loop do
47
+ msg = @soc.recv(1024)
48
+ do_write(msg)
49
+ end
50
+ end
51
+ end
52
+
53
+ ##
54
+ # Extracts the response URI from +msg+ and adds it to TupleSpace where it
55
+ # will be picked up by +reply_service+ for notification.
56
+
57
+ def do_write(msg)
58
+ Thread.new do
59
+ begin
60
+ tuple, sec = Marshal.load(msg)
61
+ @ts.write(tuple, sec)
62
+ rescue
63
+ end
64
+ end
65
+ end
66
+
67
+ ##
68
+ # Creates a thread that notifies waiting clients from the TupleSpace.
69
+
70
+ def reply_service
71
+ Thread.new do
72
+ loop do
73
+ do_reply
74
+ end
75
+ end
76
+ end
77
+
78
+ ##
79
+ # Pulls lookup tuples out of the TupleSpace and sends their DRb object the
80
+ # address of the local TupleSpace.
81
+
82
+ def do_reply
83
+ tuple = @ts.take([:lookup_ring, nil])
84
+ Thread.new { tuple[1].call(@ts) rescue nil}
85
+ rescue
86
+ end
87
+
88
+ end
89
+
90
+ ##
91
+ # RingFinger is used by RingServer clients to discover the RingServer's
92
+ # TupleSpace. Typically, all a client needs to do is call
93
+ # RingFinger.primary to retrieve the remote TupleSpace, which it can then
94
+ # begin using.
95
+
96
+ class RingFinger
97
+
98
+ @@broadcast_list = ['<broadcast>', 'localhost']
99
+
100
+ @@finger = nil
101
+
102
+ ##
103
+ # Creates a singleton RingFinger and looks for a RingServer. Returns the
104
+ # created RingFinger.
105
+
106
+ def self.finger
107
+ unless @@finger
108
+ @@finger = self.new
109
+ @@finger.lookup_ring_any
110
+ end
111
+ @@finger
112
+ end
113
+
114
+ ##
115
+ # Returns the first advertised TupleSpace.
116
+
117
+ def self.primary
118
+ finger.primary
119
+ end
120
+
121
+ ##
122
+ # Contains all discoverd TupleSpaces except for the primary.
123
+
124
+ def self.to_a
125
+ finger.to_a
126
+ end
127
+
128
+ ##
129
+ # The list of addresses where RingFinger will send query packets.
130
+
131
+ attr_accessor :broadcast_list
132
+
133
+ ##
134
+ # The port that RingFinger will send query packets to.
135
+
136
+ attr_accessor :port
137
+
138
+ ##
139
+ # Contain the first advertised TupleSpace after lookup_ring_any is called.
140
+
141
+ attr_accessor :primary
142
+
143
+ ##
144
+ # Creates a new RingFinger that will look for RingServers at +port+ on
145
+ # the addresses in +broadcast_list+.
146
+
147
+ def initialize(broadcast_list=@@broadcast_list, port=Ring_PORT)
148
+ @broadcast_list = broadcast_list || ['localhost']
149
+ @port = port
150
+ @primary = nil
151
+ @rings = []
152
+ end
153
+
154
+ ##
155
+ # Contains all discovered TupleSpaces except for the primary.
156
+
157
+ def to_a
158
+ @rings
159
+ end
160
+
161
+ ##
162
+ # Iterates over all discovered TupleSpaces starting with the primary.
163
+
164
+ def each
165
+ lookup_ring_any unless @primary
166
+ return unless @primary
167
+ yield(@primary)
168
+ @rings.each { |x| yield(x) }
169
+ end
170
+
171
+ ##
172
+ # Looks up RingServers waiting +timeout+ seconds. RingServers will be
173
+ # given +block+ as a callback, which will be called with the remote
174
+ # TupleSpace.
175
+
176
+ def lookup_ring(timeout=5, &block)
177
+ return lookup_ring_any(timeout) unless block_given?
178
+
179
+ msg = Marshal.dump([[:lookup_ring, DRbObject.new(block)], timeout])
180
+ @broadcast_list.each do |it|
181
+ soc = UDPSocket.open
182
+ begin
183
+ soc.setsockopt(Socket::SOL_SOCKET, Socket::SO_BROADCAST, true)
184
+ soc.send(msg, 0, it, @port)
185
+ rescue
186
+ nil
187
+ ensure
188
+ soc.close
189
+ end
190
+ end
191
+ sleep(timeout)
192
+ end
193
+
194
+ ##
195
+ # Returns the first found remote TupleSpace. Any further recovered
196
+ # TupleSpaces can be found by calling +to_a+.
197
+
198
+ def lookup_ring_any(timeout=5)
199
+ queue = Queue.new
200
+
201
+ th = Thread.new do
202
+ self.lookup_ring(timeout) do |ts|
203
+ queue.push(ts)
204
+ end
205
+ queue.push(nil)
206
+ while it = queue.pop
207
+ @rings.push(it)
208
+ end
209
+ end
210
+
211
+ @primary = queue.pop
212
+ raise('RingNotFound') if @primary.nil?
213
+ @primary
214
+ end
215
+
216
+ end
217
+
218
+ ##
219
+ # RingProvider uses a RingServer advertised TupleSpace as a name service.
220
+ # TupleSpace clients can register themselves with the remote TupleSpace and
221
+ # look up other provided services via the remote TupleSpace.
222
+ #
223
+ # Services are registered with a tuple of the format [:name, klass,
224
+ # DRbObject, description].
225
+
226
+ class RingProvider
227
+
228
+ ##
229
+ # Creates a RingProvider that will provide a +klass+ service running on
230
+ # +front+, with a +description+. +renewer+ is optional.
231
+
232
+ def initialize(klass, front, desc, renewer = nil)
233
+ @tuple = [:name, klass, front, desc]
234
+ @renewer = renewer || Rinda::SimpleRenewer.new
235
+ end
236
+
237
+ ##
238
+ # Advertises this service on the primary remote TupleSpace.
239
+
240
+ def provide
241
+ ts = Rinda::RingFinger.primary
242
+ ts.write(@tuple, @renewer)
243
+ end
244
+
245
+ end
246
+
247
+ end
248
+
249
+ if __FILE__ == $0
250
+ DRb.start_service
251
+ case ARGV.shift
252
+ when 's'
253
+ require 'rinda/tuplespace'
254
+ ts = Rinda::TupleSpace.new
255
+ place = Rinda::RingServer.new(ts)
256
+ $stdin.gets
257
+ when 'w'
258
+ finger = Rinda::RingFinger.new(nil)
259
+ finger.lookup_ring do |ts|
260
+ p ts
261
+ ts.write([:hello, :world])
262
+ end
263
+ when 'r'
264
+ finger = Rinda::RingFinger.new(nil)
265
+ finger.lookup_ring do |ts|
266
+ p ts
267
+ p ts.take([nil, nil])
268
+ end
269
+ end
270
+ end
271
+
@@ -0,0 +1,589 @@
1
+ require 'monitor'
2
+ require 'thread'
3
+ require 'drb/drb'
4
+ require 'rinda/rinda'
5
+
6
+ module Rinda
7
+
8
+ ##
9
+ # A TupleEntry is a Tuple (i.e. a possible entry in some Tuplespace)
10
+ # together with expiry and cancellation data.
11
+
12
+ class TupleEntry
13
+
14
+ include DRbUndumped
15
+
16
+ attr_accessor :expires
17
+
18
+ ##
19
+ # Creates a TupleEntry based on +ary+ with an optional renewer or expiry
20
+ # time +sec+.
21
+ #
22
+ # A renewer must implement the +renew+ method which returns a Numeric,
23
+ # nil, or true to indicate when the tuple has expired.
24
+
25
+ def initialize(ary, sec=nil)
26
+ @cancel = false
27
+ @expires = nil
28
+ @tuple = make_tuple(ary)
29
+ @renewer = nil
30
+ renew(sec)
31
+ end
32
+
33
+ ##
34
+ # Marks this TupleEntry as canceled.
35
+
36
+ def cancel
37
+ @cancel = true
38
+ end
39
+
40
+ ##
41
+ # A TupleEntry is dead when it is canceled or expired.
42
+
43
+ def alive?
44
+ !canceled? && !expired?
45
+ end
46
+
47
+ ##
48
+ # Return the object which makes up the tuple itself: the Array
49
+ # or Hash.
50
+
51
+ def value; @tuple.value; end
52
+
53
+ ##
54
+ # Returns the canceled status.
55
+
56
+ def canceled?; @cancel; end
57
+
58
+ ##
59
+ # Has this tuple expired? (true/false).
60
+ #
61
+ # A tuple has expired when its expiry timer based on the +sec+ argument to
62
+ # #initialize runs out.
63
+
64
+ def expired?
65
+ return true unless @expires
66
+ return false if @expires > Time.now
67
+ return true if @renewer.nil?
68
+ renew(@renewer)
69
+ return true unless @expires
70
+ return @expires < Time.now
71
+ end
72
+
73
+ ##
74
+ # Reset the expiry time according to +sec_or_renewer+.
75
+ #
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.
79
+ #
80
+ # Otherwise the argument refers to some kind of renewer object
81
+ # which will reset its expiry time.
82
+
83
+ def renew(sec_or_renewer)
84
+ sec, @renewer = get_renewer(sec_or_renewer)
85
+ @expires = make_expires(sec)
86
+ end
87
+
88
+ ##
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)
94
+
95
+ def make_expires(sec=nil)
96
+ case sec
97
+ when Numeric
98
+ Time.now + sec
99
+ when true
100
+ Time.at(1)
101
+ when nil
102
+ Time.at(2**31-1)
103
+ end
104
+ end
105
+
106
+ ##
107
+ # Retrieves +key+ from the tuple.
108
+
109
+ def [](key)
110
+ @tuple[key]
111
+ end
112
+
113
+ ##
114
+ # Fetches +key+ from the tuple.
115
+
116
+ def fetch(key)
117
+ @tuple.fetch(key)
118
+ end
119
+
120
+ ##
121
+ # The size of the tuple.
122
+
123
+ def size
124
+ @tuple.size
125
+ end
126
+
127
+ ##
128
+ # Creates a Rinda::Tuple for +ary+.
129
+
130
+ def make_tuple(ary)
131
+ Rinda::Tuple.new(ary)
132
+ end
133
+
134
+ private
135
+
136
+ ##
137
+ # Returns a valid argument to make_expires and the renewer or nil.
138
+ #
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+
141
+ # and the renewer.
142
+
143
+ def get_renewer(it)
144
+ case it
145
+ when Numeric, true, nil
146
+ return it, nil
147
+ else
148
+ begin
149
+ return it.renew, it
150
+ rescue Exception
151
+ return it, nil
152
+ end
153
+ end
154
+ end
155
+
156
+ end
157
+
158
+ ##
159
+ # A TemplateEntry is a Template together with expiry and cancellation data.
160
+
161
+ class TemplateEntry < TupleEntry
162
+ ##
163
+ # Matches this TemplateEntry against +tuple+. See Template#match for
164
+ # details on how a Template matches a Tuple.
165
+
166
+ def match(tuple)
167
+ @tuple.match(tuple)
168
+ end
169
+
170
+ alias === match
171
+
172
+ def make_tuple(ary) # :nodoc:
173
+ Rinda::Template.new(ary)
174
+ end
175
+
176
+ end
177
+
178
+ ##
179
+ # <i>Documentation?</i>
180
+
181
+ class WaitTemplateEntry < TemplateEntry
182
+
183
+ attr_reader :found
184
+
185
+ def initialize(place, ary, expires=nil)
186
+ super(ary, expires)
187
+ @place = place
188
+ @cond = place.new_cond
189
+ @found = nil
190
+ end
191
+
192
+ def cancel
193
+ super
194
+ signal
195
+ end
196
+
197
+ def wait
198
+ @cond.wait
199
+ end
200
+
201
+ def read(tuple)
202
+ @found = tuple
203
+ signal
204
+ end
205
+
206
+ def signal
207
+ @place.synchronize do
208
+ @cond.signal
209
+ end
210
+ end
211
+
212
+ end
213
+
214
+ ##
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.
218
+ #
219
+ # See TupleSpace#notify_event for valid notification types.
220
+ #
221
+ # == Example
222
+ #
223
+ # ts = Rinda::TupleSpace.new
224
+ # observer = ts.notify 'write', [nil]
225
+ #
226
+ # Thread.start do
227
+ # observer.each { |t| p t }
228
+ # end
229
+ #
230
+ # 3.times { |i| ts.write [i] }
231
+ #
232
+ # Outputs:
233
+ #
234
+ # ['write', [0]]
235
+ # ['write', [1]]
236
+ # ['write', [2]]
237
+
238
+ class NotifyTemplateEntry < TemplateEntry
239
+
240
+ ##
241
+ # Creates a new NotifyTemplateEntry that watches +place+ for +event+s that
242
+ # match +tuple+.
243
+
244
+ def initialize(place, event, tuple, expires=nil)
245
+ ary = [event, Rinda::Template.new(tuple)]
246
+ super(ary, expires)
247
+ @queue = Queue.new
248
+ @done = false
249
+ end
250
+
251
+ ##
252
+ # Called by TupleSpace to notify this NotifyTemplateEntry of a new event.
253
+
254
+ def notify(ev)
255
+ @queue.push(ev)
256
+ end
257
+
258
+ ##
259
+ # Retrieves a notification. Raises RequestExpiredError when this
260
+ # NotifyTemplateEntry expires.
261
+
262
+ def pop
263
+ raise RequestExpiredError if @done
264
+ it = @queue.pop
265
+ @done = true if it[0] == 'close'
266
+ return it
267
+ end
268
+
269
+ ##
270
+ # Yields event/tuple pairs until this NotifyTemplateEntry expires.
271
+
272
+ def each # :yields: event, tuple
273
+ while !@done
274
+ it = pop
275
+ yield(it)
276
+ end
277
+ rescue
278
+ ensure
279
+ cancel
280
+ end
281
+
282
+ end
283
+
284
+ ##
285
+ # TupleBag is an unordered collection of tuples. It is the basis
286
+ # of Tuplespace.
287
+
288
+ class TupleBag
289
+
290
+ def initialize # :nodoc:
291
+ @hash = {}
292
+ end
293
+
294
+ ##
295
+ # +true+ if the TupleBag to see if it has any expired entries.
296
+
297
+ def has_expires?
298
+ @hash.each do |k, v|
299
+ v.each do |tuple|
300
+ return true if tuple.expires
301
+ end
302
+ end
303
+ false
304
+ end
305
+
306
+ ##
307
+ # Add +ary+ to the TupleBag.
308
+
309
+ def push(ary)
310
+ size = ary.size
311
+ @hash[size] ||= []
312
+ @hash[size].push(ary)
313
+ end
314
+
315
+ ##
316
+ # Removes +ary+ from the TupleBag.
317
+
318
+ def delete(ary)
319
+ size = ary.size
320
+ @hash.fetch(size, []).delete(ary)
321
+ end
322
+
323
+ ##
324
+ # Finds all live tuples that match +template+.
325
+
326
+ def find_all(template)
327
+ @hash.fetch(template.size, []).find_all do |tuple|
328
+ tuple.alive? && template.match(tuple)
329
+ end
330
+ end
331
+
332
+ ##
333
+ # Finds a live tuple that matches +template+.
334
+
335
+ def find(template)
336
+ @hash.fetch(template.size, []).find do |tuple|
337
+ tuple.alive? && template.match(tuple)
338
+ end
339
+ end
340
+
341
+ ##
342
+ # Finds all tuples in the TupleBag which when treated as templates, match
343
+ # +tuple+ and are alive.
344
+
345
+ def find_all_template(tuple)
346
+ @hash.fetch(tuple.size, []).find_all do |template|
347
+ template.alive? && template.match(tuple)
348
+ end
349
+ end
350
+
351
+ ##
352
+ # Delete tuples which dead tuples from the TupleBag, returning the deleted
353
+ # tuples.
354
+
355
+ def delete_unless_alive
356
+ deleted = []
357
+ @hash.keys.each do |size|
358
+ ary = []
359
+ @hash[size].each do |tuple|
360
+ if tuple.alive?
361
+ ary.push(tuple)
362
+ else
363
+ deleted.push(tuple)
364
+ end
365
+ end
366
+ @hash[size] = ary
367
+ end
368
+ deleted
369
+ end
370
+
371
+ end
372
+
373
+ ##
374
+ # The Tuplespace manages access to the tuples it contains,
375
+ # ensuring mutual exclusion requirements are met.
376
+ #
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.
379
+
380
+ class TupleSpace
381
+
382
+ include DRbUndumped
383
+ include MonitorMixin
384
+
385
+ ##
386
+ # Creates a new TupleSpace. +period+ is used to control how often to look
387
+ # for dead tuples after modifications to the TupleSpace.
388
+ #
389
+ # If no dead tuples are found +period+ seconds after the last
390
+ # modification, the TupleSpace will stop looking for dead tuples.
391
+
392
+ def initialize(period=60)
393
+ super()
394
+ @bag = TupleBag.new
395
+ @read_waiter = TupleBag.new
396
+ @take_waiter = TupleBag.new
397
+ @notify_waiter = TupleBag.new
398
+ @period = period
399
+ @keeper = nil
400
+ end
401
+
402
+ ##
403
+ # Adds +tuple+
404
+
405
+ def write(tuple, sec=nil)
406
+ entry = TupleEntry.new(tuple, sec)
407
+ start_keeper
408
+ synchronize do
409
+ if entry.expired?
410
+ @read_waiter.find_all_template(entry).each do |template|
411
+ template.read(tuple)
412
+ end
413
+ notify_event('write', entry.value)
414
+ notify_event('delete', entry.value)
415
+ else
416
+ @bag.push(entry)
417
+ @read_waiter.find_all_template(entry).each do |template|
418
+ template.read(tuple)
419
+ end
420
+ @take_waiter.find_all_template(entry).each do |template|
421
+ template.signal
422
+ end
423
+ notify_event('write', entry.value)
424
+ end
425
+ end
426
+ entry
427
+ end
428
+
429
+ ##
430
+ # Removes +tuple+
431
+
432
+ def take(tuple, sec=nil, &block)
433
+ move(nil, tuple, sec, &block)
434
+ end
435
+
436
+ ##
437
+ # Moves +tuple+ to +port+.
438
+
439
+ def move(port, tuple, sec=nil)
440
+ template = WaitTemplateEntry.new(self, tuple, sec)
441
+ yield(template) if block_given?
442
+ start_keeper
443
+ synchronize do
444
+ entry = @bag.find(template)
445
+ if entry
446
+ port.push(entry.value) if port
447
+ @bag.delete(entry)
448
+ notify_event('take', entry.value)
449
+ return entry.value
450
+ end
451
+ raise RequestExpiredError if template.expired?
452
+
453
+ begin
454
+ @take_waiter.push(template)
455
+ while true
456
+ raise RequestCanceledError if template.canceled?
457
+ raise RequestExpiredError if template.expired?
458
+ entry = @bag.find(template)
459
+ if entry
460
+ port.push(entry.value) if port
461
+ @bag.delete(entry)
462
+ notify_event('take', entry.value)
463
+ return entry.value
464
+ end
465
+ template.wait
466
+ end
467
+ ensure
468
+ @take_waiter.delete(template)
469
+ end
470
+ end
471
+ end
472
+
473
+ ##
474
+ # Reads +tuple+, but does not remove it.
475
+
476
+ def read(tuple, sec=nil)
477
+ template = WaitTemplateEntry.new(self, tuple, sec)
478
+ yield(template) if block_given?
479
+ start_keeper
480
+ synchronize do
481
+ entry = @bag.find(template)
482
+ return entry.value if entry
483
+ raise RequestExpiredError if template.expired?
484
+
485
+ begin
486
+ @read_waiter.push(template)
487
+ template.wait
488
+ raise RequestCanceledError if template.canceled?
489
+ raise RequestExpiredError if template.expired?
490
+ return template.found
491
+ ensure
492
+ @read_waiter.delete(template)
493
+ end
494
+ end
495
+ end
496
+
497
+ ##
498
+ # Returns all tuples matching +tuple+. Does not remove the found tuples.
499
+
500
+ def read_all(tuple)
501
+ template = WaitTemplateEntry.new(self, tuple, nil)
502
+ synchronize do
503
+ entry = @bag.find_all(template)
504
+ entry.collect do |e|
505
+ e.value
506
+ end
507
+ end
508
+ end
509
+
510
+ ##
511
+ # Registers for notifications of +event+. Returns a NotifyTemplateEntry.
512
+ # See NotifyTemplateEntry for examples of how to listen for notifications.
513
+ #
514
+ # +event+ can be:
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
518
+ #
519
+ # The TupleSpace will also notify you of the 'close' event when the
520
+ # NotifyTemplateEntry has expired.
521
+
522
+ def notify(event, tuple, sec=nil)
523
+ template = NotifyTemplateEntry.new(self, event, tuple, sec)
524
+ synchronize do
525
+ @notify_waiter.push(template)
526
+ end
527
+ template
528
+ end
529
+
530
+ private
531
+
532
+ ##
533
+ # Removes dead tuples.
534
+
535
+ def keep_clean
536
+ synchronize do
537
+ @read_waiter.delete_unless_alive.each do |e|
538
+ e.signal
539
+ end
540
+ @take_waiter.delete_unless_alive.each do |e|
541
+ e.signal
542
+ end
543
+ @notify_waiter.delete_unless_alive.each do |e|
544
+ e.notify(['close'])
545
+ end
546
+ @bag.delete_unless_alive.each do |e|
547
+ notify_event('delete', e.value)
548
+ end
549
+ end
550
+ end
551
+
552
+ ##
553
+ # Notifies all registered listeners for +event+ of a status change of
554
+ # +tuple+.
555
+
556
+ def notify_event(event, tuple)
557
+ ev = [event, tuple]
558
+ @notify_waiter.find_all_template(ev).each do |template|
559
+ template.notify(ev)
560
+ end
561
+ end
562
+
563
+ ##
564
+ # Creates a thread that scans the tuplespace for expired tuples.
565
+
566
+ def start_keeper
567
+ return if @keeper && @keeper.alive?
568
+ @keeper = Thread.new do
569
+ while need_keeper?
570
+ keep_clean
571
+ sleep(@period)
572
+ end
573
+ end
574
+ end
575
+
576
+ ##
577
+ # Checks the tuplespace to see if it needs cleaning.
578
+
579
+ def need_keeper?
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?
584
+ end
585
+
586
+ end
587
+
588
+ end
589
+
@@ -0,0 +1 @@
1
+ require "rubysl/rinda/version"
@@ -0,0 +1,5 @@
1
+ module RubySL
2
+ module Rinda
3
+ VERSION = "1.0.0"
4
+ end
5
+ end
@@ -0,0 +1,23 @@
1
+ # coding: utf-8
2
+ require './lib/rubysl/rinda/version'
3
+
4
+ Gem::Specification.new do |spec|
5
+ spec.name = "rubysl-rinda"
6
+ spec.version = RubySL::Rinda::VERSION
7
+ spec.authors = ["Brian Shirai"]
8
+ spec.email = ["brixen@gmail.com"]
9
+ spec.description = %q{Ruby standard library rinda.}
10
+ spec.summary = %q{Ruby standard library rinda.}
11
+ spec.homepage = "https://github.com/rubysl/rubysl-rinda"
12
+ spec.license = "BSD"
13
+
14
+ spec.files = `git ls-files`.split($/)
15
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
16
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
17
+ spec.require_paths = ["lib"]
18
+
19
+ spec.add_development_dependency "bundler", "~> 1.3"
20
+ spec.add_development_dependency "rake", "~> 10.0"
21
+ spec.add_development_dependency "mspec", "~> 1.5"
22
+ spec.add_development_dependency "rubysl-prettyprint", "~> 1.0"
23
+ end
metadata ADDED
@@ -0,0 +1,112 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rubysl-rinda
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Brian Shirai
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2013-12-26 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: '1.3'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ~>
25
+ - !ruby/object:Gem::Version
26
+ version: '1.3'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ~>
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ~>
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: mspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ~>
46
+ - !ruby/object:Gem::Version
47
+ version: '1.5'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ~>
53
+ - !ruby/object:Gem::Version
54
+ version: '1.5'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rubysl-prettyprint
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: '1.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ~>
67
+ - !ruby/object:Gem::Version
68
+ version: '1.0'
69
+ description: Ruby standard library rinda.
70
+ email:
71
+ - brixen@gmail.com
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - .gitignore
77
+ - .travis.yml
78
+ - Gemfile
79
+ - LICENSE
80
+ - README.md
81
+ - Rakefile
82
+ - lib/rinda/rinda.rb
83
+ - lib/rinda/ring.rb
84
+ - lib/rinda/tuplespace.rb
85
+ - lib/rubysl/rinda.rb
86
+ - lib/rubysl/rinda/version.rb
87
+ - rubysl-rinda.gemspec
88
+ homepage: https://github.com/rubysl/rubysl-rinda
89
+ licenses:
90
+ - BSD
91
+ metadata: {}
92
+ post_install_message:
93
+ rdoc_options: []
94
+ require_paths:
95
+ - lib
96
+ required_ruby_version: !ruby/object:Gem::Requirement
97
+ requirements:
98
+ - - '>='
99
+ - !ruby/object:Gem::Version
100
+ version: '0'
101
+ required_rubygems_version: !ruby/object:Gem::Requirement
102
+ requirements:
103
+ - - '>='
104
+ - !ruby/object:Gem::Version
105
+ version: '0'
106
+ requirements: []
107
+ rubyforge_project:
108
+ rubygems_version: 2.0.7
109
+ signing_key:
110
+ specification_version: 4
111
+ summary: Ruby standard library rinda.
112
+ test_files: []