rubysl-rinda 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []