rinda2 0.1.1

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: 13d1278aefb6ed5124732435a552d304d7fb4138
4
+ data.tar.gz: 0b8a86151cb6881f59eaf7d328f686ed8f401c0b
5
+ SHA512:
6
+ metadata.gz: 125cd4ac651251f21a637431194837d15e617a87c599b1024ab9eb76336fb211e78dad59646307e06a6516f837238aacb6cdb8306c61cc3907834b3671a448c1
7
+ data.tar.gz: 78ac6e59f23304a346a99f3eeffb868e64959248ce2cc3b1705ed6ad68399a85d3bc066582037ff71425b3657bcbfadea7fa1ddd93e1f4a3d999290399551de7
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in rinda2.gemspec
4
+ gemspec
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016 Nathan Baum
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,41 @@
1
+ # Rinda2
2
+
3
+ Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/rinda2`. To experiment with that code, run `bin/console` for an interactive prompt.
4
+
5
+ TODO: Delete this and the text above, and describe your gem
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'rinda2'
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ $ bundle
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install rinda2
22
+
23
+ ## Usage
24
+
25
+ TODO: Write usage instructions here
26
+
27
+ ## Development
28
+
29
+ After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
30
+
31
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
32
+
33
+ ## Contributing
34
+
35
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/rinda2.
36
+
37
+
38
+ ## License
39
+
40
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
41
+
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+ task :default => :spec
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "rinda2"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,8 @@
1
+ require "rinda2/version"
2
+ require "rinda2/ring"
3
+ require "rinda2/rinda"
4
+ require "rinda2/tuplespace"
5
+
6
+ module Rinda2
7
+ # Your code goes here...
8
+ end
@@ -0,0 +1,291 @@
1
+ # frozen_string_literal: false
2
+ require 'drb/drb'
3
+ require 'thread'
4
+
5
+ module Rinda
6
+ class RindaError < RuntimeError; end
7
+ class InvalidHashTupleKey < RindaError; end
8
+ class RequestCanceledError < ThreadError; end
9
+ class RequestExpiredError < ThreadError; end
10
+
11
+ class Tuple
12
+
13
+ ##
14
+ # Creates a new Tuple from +ary_or_hash+ which must be an Array or Hash.
15
+
16
+ def initialize(ary_or_hash)
17
+ if hash?(ary_or_hash)
18
+ init_with_hash(ary_or_hash)
19
+ else
20
+ init_with_ary(ary_or_hash)
21
+ end
22
+ end
23
+
24
+ ##
25
+ # The number of elements in the tuple.
26
+
27
+ def size
28
+ @tuple.size
29
+ end
30
+
31
+ ##
32
+ # Accessor method for elements of the tuple.
33
+
34
+ def [](k)
35
+ @tuple[k]
36
+ end
37
+
38
+ ##
39
+ # Fetches item +k+ from the tuple.
40
+
41
+ def fetch(k)
42
+ @tuple.fetch(k)
43
+ end
44
+
45
+ ##
46
+ # Iterate through the tuple, yielding the index or key, and the
47
+ # value, thus ensuring arrays are iterated similarly to hashes.
48
+
49
+ def each # FIXME
50
+ if Hash === @tuple
51
+ @tuple.each { |k, v| yield(k, v) }
52
+ else
53
+ @tuple.each_with_index { |v, k| yield(k, v) }
54
+ end
55
+ end
56
+
57
+ ##
58
+ # Return the tuple itself
59
+ def value
60
+ @tuple
61
+ end
62
+
63
+ private
64
+
65
+ def hash?(ary_or_hash)
66
+ ary_or_hash.respond_to?(:keys)
67
+ end
68
+
69
+ ##
70
+ # Munges +ary+ into a valid Tuple.
71
+
72
+ def init_with_ary(ary)
73
+ @tuple = Array.new(ary.size)
74
+ @tuple.size.times do |i|
75
+ @tuple[i] = ary[i]
76
+ end
77
+ end
78
+
79
+ ##
80
+ # Ensures +hash+ is a valid Tuple.
81
+
82
+ def init_with_hash(hash)
83
+ @tuple = Hash.new
84
+ hash.each do |k, v|
85
+ raise InvalidHashTupleKey unless String === k
86
+ @tuple[k] = v
87
+ end
88
+ end
89
+
90
+ end
91
+
92
+ ##
93
+ # Templates are used to match tuples in Rinda.
94
+
95
+ class Template < Tuple
96
+
97
+ ##
98
+ # Matches this template against +tuple+. The +tuple+ must be the same
99
+ # size as the template. An element with a +nil+ value in a template acts
100
+ # as a wildcard, matching any value in the corresponding position in the
101
+ # tuple. Elements of the template match the +tuple+ if the are #== or
102
+ # #===.
103
+ #
104
+ # Template.new([:foo, 5]).match Tuple.new([:foo, 5]) # => true
105
+ # Template.new([:foo, nil]).match Tuple.new([:foo, 5]) # => true
106
+ # Template.new([String]).match Tuple.new(['hello']) # => true
107
+ #
108
+ # Template.new([:foo]).match Tuple.new([:foo, 5]) # => false
109
+ # Template.new([:foo, 6]).match Tuple.new([:foo, 5]) # => false
110
+ # Template.new([:foo, nil]).match Tuple.new([:foo]) # => false
111
+ # Template.new([:foo, 6]).match Tuple.new([:foo]) # => false
112
+
113
+ def match(tuple)
114
+ return false unless tuple.respond_to?(:size)
115
+ return false unless tuple.respond_to?(:fetch)
116
+ return false unless self.size == tuple.size
117
+ each do |k, v|
118
+ begin
119
+ it = tuple.fetch(k)
120
+ rescue
121
+ return false
122
+ end
123
+ next if v.nil?
124
+ next if v == it
125
+ next if v === it
126
+ return false
127
+ end
128
+ return true
129
+ end
130
+
131
+ ##
132
+ # Alias for #match.
133
+
134
+ def ===(tuple)
135
+ match(tuple)
136
+ end
137
+
138
+ end
139
+
140
+ ##
141
+ # <i>Documentation?</i>
142
+
143
+ class DRbObjectTemplate
144
+
145
+ ##
146
+ # Creates a new DRbObjectTemplate that will match against +uri+ and +ref+.
147
+
148
+ def initialize(uri=nil, ref=nil)
149
+ @drb_uri = uri
150
+ @drb_ref = ref
151
+ end
152
+
153
+ ##
154
+ # This DRbObjectTemplate matches +ro+ if the remote object's drburi and
155
+ # drbref are the same. +nil+ is used as a wildcard.
156
+
157
+ def ===(ro)
158
+ return true if super(ro)
159
+ unless @drb_uri.nil?
160
+ return false unless (@drb_uri === ro.__drburi rescue false)
161
+ end
162
+ unless @drb_ref.nil?
163
+ return false unless (@drb_ref === ro.__drbref rescue false)
164
+ end
165
+ true
166
+ end
167
+
168
+ end
169
+
170
+ ##
171
+ # TupleSpaceProxy allows a remote Tuplespace to appear as local.
172
+
173
+ class TupleSpaceProxy
174
+ ##
175
+ # A Port ensures that a moved tuple arrives properly at its destination
176
+ # and does not get lost.
177
+ #
178
+ # See https://bugs.ruby-lang.org/issues/8125
179
+
180
+ class Port # :nodoc:
181
+ attr_reader :value
182
+
183
+ def self.deliver
184
+ port = new
185
+
186
+ begin
187
+ yield(port)
188
+ ensure
189
+ port.close
190
+ end
191
+
192
+ port.value
193
+ end
194
+
195
+ def initialize
196
+ @open = true
197
+ @value = nil
198
+ end
199
+
200
+ ##
201
+ # Don't let the DRb thread push to it when remote sends tuple
202
+
203
+ def close
204
+ @open = false
205
+ end
206
+
207
+ ##
208
+ # Stores +value+ and ensure it does not get marshaled multiple times.
209
+
210
+ def push value
211
+ raise 'port closed' unless @open
212
+
213
+ @value = value
214
+
215
+ nil # avoid Marshal
216
+ end
217
+ end
218
+
219
+ ##
220
+ # Creates a new TupleSpaceProxy to wrap +ts+.
221
+
222
+ def initialize(ts)
223
+ @ts = ts
224
+ end
225
+
226
+ ##
227
+ # Adds +tuple+ to the proxied TupleSpace. See TupleSpace#write.
228
+
229
+ def write(tuple, sec=nil)
230
+ @ts.write(tuple, sec)
231
+ end
232
+
233
+ ##
234
+ # Takes +tuple+ from the proxied TupleSpace. See TupleSpace#take.
235
+
236
+ def take(tuple, sec=nil, &block)
237
+ Port.deliver do |port|
238
+ @ts.move(DRbObject.new(port), tuple, sec, &block)
239
+ end
240
+ end
241
+
242
+ ##
243
+ # Reads +tuple+ from the proxied TupleSpace. See TupleSpace#read.
244
+
245
+ def read(tuple, sec=nil, &block)
246
+ @ts.read(tuple, sec, &block)
247
+ end
248
+
249
+ ##
250
+ # Reads all tuples matching +tuple+ from the proxied TupleSpace. See
251
+ # TupleSpace#read_all.
252
+
253
+ def read_all(tuple)
254
+ @ts.read_all(tuple)
255
+ end
256
+
257
+ ##
258
+ # Registers for notifications of event +ev+ on the proxied TupleSpace.
259
+ # See TupleSpace#notify
260
+
261
+ def notify(ev, tuple, sec=nil)
262
+ @ts.notify(ev, tuple, sec)
263
+ end
264
+
265
+ end
266
+
267
+ ##
268
+ # An SimpleRenewer allows a TupleSpace to check if a TupleEntry is still
269
+ # alive.
270
+
271
+ class SimpleRenewer
272
+
273
+ include DRbUndumped
274
+
275
+ ##
276
+ # Creates a new SimpleRenewer that keeps an object alive for another +sec+
277
+ # seconds.
278
+
279
+ def initialize(sec=180)
280
+ @sec = sec
281
+ end
282
+
283
+ ##
284
+ # Called by the TupleSpace to check if the object is still alive.
285
+
286
+ def renew
287
+ @sec
288
+ end
289
+ end
290
+
291
+ end
@@ -0,0 +1,488 @@
1
+ # frozen_string_literal: false
2
+ #
3
+ # Note: Rinda::Ring API is unstable.
4
+ #
5
+ require 'drb/drb'
6
+ require 'rinda/rinda'
7
+ require 'thread'
8
+ require 'ipaddr'
9
+
10
+ module Rinda
11
+
12
+ ##
13
+ # The default port Ring discovery will use.
14
+
15
+ Ring_PORT = 7647
16
+
17
+ ##
18
+ # A RingServer allows a Rinda::TupleSpace to be located via UDP broadcasts.
19
+ # Default service location uses the following steps:
20
+ #
21
+ # 1. A RingServer begins listening on the network broadcast UDP address.
22
+ # 2. A RingFinger sends a UDP packet containing the DRb URI where it will
23
+ # listen for a reply.
24
+ # 3. The RingServer receives the UDP packet and connects back to the
25
+ # provided DRb URI with the DRb service.
26
+ #
27
+ # A RingServer requires a TupleSpace:
28
+ #
29
+ # ts = Rinda::TupleSpace.new
30
+ # rs = Rinda::RingServer.new
31
+ #
32
+ # RingServer can also listen on multicast addresses for announcements. This
33
+ # allows multiple RingServers to run on the same host. To use network
34
+ # broadcast and multicast:
35
+ #
36
+ # ts = Rinda::TupleSpace.new
37
+ # rs = Rinda::RingServer.new ts, %w[Socket::INADDR_ANY, 239.0.0.1 ff02::1]
38
+
39
+ class RingServer
40
+
41
+ include DRbUndumped
42
+
43
+ ##
44
+ # Special renewer for the RingServer to allow shutdown
45
+
46
+ class Renewer # :nodoc:
47
+ include DRbUndumped
48
+
49
+ ##
50
+ # Set to false to shutdown future requests using this Renewer
51
+
52
+ attr_writer :renew
53
+
54
+ def initialize # :nodoc:
55
+ @renew = true
56
+ end
57
+
58
+ def renew # :nodoc:
59
+ @renew ? 1 : true
60
+ end
61
+ end
62
+
63
+ ##
64
+ # Advertises +ts+ on the given +addresses+ at +port+.
65
+ #
66
+ # If +addresses+ is omitted only the UDP broadcast address is used.
67
+ #
68
+ # +addresses+ can contain multiple addresses. If a multicast address is
69
+ # given in +addresses+ then the RingServer will listen for multicast
70
+ # queries.
71
+ #
72
+ # If you use IPv4 multicast you may need to set an address of the inbound
73
+ # interface which joins a multicast group.
74
+ #
75
+ # ts = Rinda::TupleSpace.new
76
+ # rs = Rinda::RingServer.new(ts, [['239.0.0.1', '9.5.1.1']])
77
+ #
78
+ # You can set addresses as an Array Object. The first element of the
79
+ # Array is a multicast address and the second is an inbound interface
80
+ # address. If the second is omitted then '0.0.0.0' is used.
81
+ #
82
+ # If you use IPv6 multicast you may need to set both the local interface
83
+ # address and the inbound interface index:
84
+ #
85
+ # rs = Rinda::RingServer.new(ts, [['ff02::1', '::1', 1]])
86
+ #
87
+ # The first element is a multicast address and the second is an inbound
88
+ # interface address. The third is an inbound interface index.
89
+ #
90
+ # At this time there is no easy way to get an interface index by name.
91
+ #
92
+ # If the second is omitted then '::1' is used.
93
+ # If the third is omitted then 0 (default interface) is used.
94
+
95
+ def initialize(ts, addresses=[Socket::INADDR_ANY], port=Ring_PORT)
96
+ @port = port
97
+
98
+ if Integer === addresses then
99
+ addresses, @port = [Socket::INADDR_ANY], addresses
100
+ end
101
+
102
+ @renewer = Renewer.new
103
+
104
+ @ts = ts
105
+ @sockets = []
106
+ addresses.each do |address|
107
+ if Array === address
108
+ make_socket(*address)
109
+ else
110
+ make_socket(address)
111
+ end
112
+ end
113
+
114
+ @w_services = write_services
115
+ @r_service = reply_service
116
+ end
117
+
118
+ ##
119
+ # Creates a socket at +address+
120
+ #
121
+ # If +address+ is multicast address then +interface_address+ and
122
+ # +multicast_interface+ can be set as optional.
123
+ #
124
+ # A created socket is bound to +interface_address+. If you use IPv4
125
+ # multicast then the interface of +interface_address+ is used as the
126
+ # inbound interface. If +interface_address+ is omitted or nil then
127
+ # '0.0.0.0' or '::1' is used.
128
+ #
129
+ # If you use IPv6 multicast then +multicast_interface+ is used as the
130
+ # inbound interface. +multicast_interface+ is a network interface index.
131
+ # If +multicast_interface+ is omitted then 0 (default interface) is used.
132
+
133
+ def make_socket(address, interface_address=nil, multicast_interface=0)
134
+ addrinfo = Addrinfo.udp(address, @port)
135
+
136
+ socket = Socket.new(addrinfo.pfamily, addrinfo.socktype,
137
+ addrinfo.protocol)
138
+ @sockets << socket
139
+
140
+ if addrinfo.ipv4_multicast? or addrinfo.ipv6_multicast? then
141
+ if Socket.const_defined?(:SO_REUSEPORT) then
142
+ socket.setsockopt(:SOCKET, :SO_REUSEPORT, true)
143
+ else
144
+ socket.setsockopt(:SOCKET, :SO_REUSEADDR, true)
145
+ end
146
+
147
+ if addrinfo.ipv4_multicast? then
148
+ interface_address = '0.0.0.0' if interface_address.nil?
149
+ socket.bind(Addrinfo.udp('0.0.0.0', @port))
150
+
151
+ mreq = IPAddr.new(addrinfo.ip_address).hton +
152
+ IPAddr.new(interface_address).hton
153
+
154
+ socket.setsockopt(:IPPROTO_IP, :IP_ADD_MEMBERSHIP, mreq)
155
+ else
156
+ interface_address = '::1' if interface_address.nil?
157
+ socket.bind(Addrinfo.udp(interface_address, @port))
158
+
159
+ mreq = IPAddr.new(addrinfo.ip_address).hton +
160
+ [multicast_interface].pack('I')
161
+
162
+ socket.setsockopt(:IPPROTO_IPV6, :IPV6_JOIN_GROUP, mreq)
163
+ end
164
+ else
165
+ socket.bind(addrinfo)
166
+ end
167
+
168
+ socket
169
+ end
170
+
171
+ ##
172
+ # Creates threads that pick up UDP packets and passes them to do_write for
173
+ # decoding.
174
+
175
+ def write_services
176
+ @sockets.map do |s|
177
+ Thread.new(s) do |socket|
178
+ loop do
179
+ msg = socket.recv(1024)
180
+ do_write(msg)
181
+ end
182
+ end
183
+ end
184
+ end
185
+
186
+ ##
187
+ # Extracts the response URI from +msg+ and adds it to TupleSpace where it
188
+ # will be picked up by +reply_service+ for notification.
189
+
190
+ def do_write(msg)
191
+ Thread.new do
192
+ begin
193
+ tuple, sec = Marshal.load(msg)
194
+ @ts.write(tuple, sec)
195
+ rescue
196
+ end
197
+ end
198
+ end
199
+
200
+ ##
201
+ # Creates a thread that notifies waiting clients from the TupleSpace.
202
+
203
+ def reply_service
204
+ Thread.new do
205
+ loop do
206
+ do_reply
207
+ end
208
+ end
209
+ end
210
+
211
+ ##
212
+ # Pulls lookup tuples out of the TupleSpace and sends their DRb object the
213
+ # address of the local TupleSpace.
214
+
215
+ def do_reply
216
+ tuple = @ts.take([:lookup_ring, nil], @renewer)
217
+ Thread.new { tuple[1].call(@ts) rescue nil}
218
+ rescue
219
+ end
220
+
221
+ ##
222
+ # Shuts down the RingServer
223
+
224
+ def shutdown
225
+ @renewer.renew = false
226
+
227
+ @w_services.each do |thread|
228
+ thread.kill
229
+ thread.join
230
+ end
231
+
232
+ @sockets.each do |socket|
233
+ socket.close
234
+ end
235
+
236
+ @r_service.kill
237
+ @r_service.join
238
+ end
239
+
240
+ end
241
+
242
+ ##
243
+ # RingFinger is used by RingServer clients to discover the RingServer's
244
+ # TupleSpace. Typically, all a client needs to do is call
245
+ # RingFinger.primary to retrieve the remote TupleSpace, which it can then
246
+ # begin using.
247
+ #
248
+ # To find the first available remote TupleSpace:
249
+ #
250
+ # Rinda::RingFinger.primary
251
+ #
252
+ # To create a RingFinger that broadcasts to a custom list:
253
+ #
254
+ # rf = Rinda::RingFinger.new ['localhost', '192.0.2.1']
255
+ # rf.primary
256
+ #
257
+ # Rinda::RingFinger also understands multicast addresses and sets them up
258
+ # properly. This allows you to run multiple RingServers on the same host:
259
+ #
260
+ # rf = Rinda::RingFinger.new ['239.0.0.1']
261
+ # rf.primary
262
+ #
263
+ # You can set the hop count (or TTL) for multicast searches using
264
+ # #multicast_hops.
265
+ #
266
+ # If you use IPv6 multicast you may need to set both an address and the
267
+ # outbound interface index:
268
+ #
269
+ # rf = Rinda::RingFinger.new ['ff02::1']
270
+ # rf.multicast_interface = 1
271
+ # rf.primary
272
+ #
273
+ # At this time there is no easy way to get an interface index by name.
274
+
275
+ class RingFinger
276
+
277
+ @@broadcast_list = ['<broadcast>', 'localhost']
278
+
279
+ @@finger = nil
280
+
281
+ ##
282
+ # Creates a singleton RingFinger and looks for a RingServer. Returns the
283
+ # created RingFinger.
284
+
285
+ def self.finger
286
+ unless @@finger
287
+ @@finger = self.new
288
+ @@finger.lookup_ring_any
289
+ end
290
+ @@finger
291
+ end
292
+
293
+ ##
294
+ # Returns the first advertised TupleSpace.
295
+
296
+ def self.primary
297
+ finger.primary
298
+ end
299
+
300
+ ##
301
+ # Contains all discovered TupleSpaces except for the primary.
302
+
303
+ def self.to_a
304
+ finger.to_a
305
+ end
306
+
307
+ ##
308
+ # The list of addresses where RingFinger will send query packets.
309
+
310
+ attr_accessor :broadcast_list
311
+
312
+ ##
313
+ # Maximum number of hops for sent multicast packets (if using a multicast
314
+ # address in the broadcast list). The default is 1 (same as UDP
315
+ # broadcast).
316
+
317
+ attr_accessor :multicast_hops
318
+
319
+ ##
320
+ # The interface index to send IPv6 multicast packets from.
321
+
322
+ attr_accessor :multicast_interface
323
+
324
+ ##
325
+ # The port that RingFinger will send query packets to.
326
+
327
+ attr_accessor :port
328
+
329
+ ##
330
+ # Contain the first advertised TupleSpace after lookup_ring_any is called.
331
+
332
+ attr_accessor :primary
333
+
334
+ ##
335
+ # Creates a new RingFinger that will look for RingServers at +port+ on
336
+ # the addresses in +broadcast_list+.
337
+ #
338
+ # If +broadcast_list+ contains a multicast address then multicast queries
339
+ # will be made using the given multicast_hops and multicast_interface.
340
+
341
+ def initialize(broadcast_list=@@broadcast_list, port=Ring_PORT)
342
+ @broadcast_list = broadcast_list || ['localhost']
343
+ @port = port
344
+ @primary = nil
345
+ @rings = []
346
+
347
+ @multicast_hops = 1
348
+ @multicast_interface = 0
349
+ end
350
+
351
+ ##
352
+ # Contains all discovered TupleSpaces except for the primary.
353
+
354
+ def to_a
355
+ @rings
356
+ end
357
+
358
+ ##
359
+ # Iterates over all discovered TupleSpaces starting with the primary.
360
+
361
+ def each
362
+ lookup_ring_any unless @primary
363
+ return unless @primary
364
+ yield(@primary)
365
+ @rings.each { |x| yield(x) }
366
+ end
367
+
368
+ ##
369
+ # Looks up RingServers waiting +timeout+ seconds. RingServers will be
370
+ # given +block+ as a callback, which will be called with the remote
371
+ # TupleSpace.
372
+
373
+ def lookup_ring(timeout=5, &block)
374
+ return lookup_ring_any(timeout) unless block_given?
375
+
376
+ msg = Marshal.dump([[:lookup_ring, DRbObject.new(block)], timeout])
377
+ @broadcast_list.each do |it|
378
+ send_message(it, msg)
379
+ end
380
+ sleep(timeout)
381
+ end
382
+
383
+ ##
384
+ # Returns the first found remote TupleSpace. Any further recovered
385
+ # TupleSpaces can be found by calling +to_a+.
386
+
387
+ def lookup_ring_any(timeout=5)
388
+ queue = Queue.new
389
+
390
+ Thread.new do
391
+ self.lookup_ring(timeout) do |ts|
392
+ queue.push(ts)
393
+ end
394
+ queue.push(nil)
395
+ end
396
+
397
+ @primary = queue.pop
398
+ raise('RingNotFound') if @primary.nil?
399
+
400
+ Thread.new do
401
+ while it = queue.pop
402
+ @rings.push(it)
403
+ end
404
+ end
405
+
406
+ @primary
407
+ end
408
+
409
+ ##
410
+ # Creates a socket for +address+ with the appropriate multicast options
411
+ # for multicast addresses.
412
+
413
+ def make_socket(address, interface_address = nil) # :nodoc:
414
+ addrinfo = Addrinfo.udp(address, @port)
415
+
416
+ soc = Socket.new(addrinfo.pfamily, addrinfo.socktype, addrinfo.protocol)
417
+ begin
418
+ if addrinfo.ipv4_multicast? then
419
+ interface_address ||= '0.0.0.0'
420
+ soc.setsockopt(Socket::Option.ipv4_multicast_loop(1))
421
+ soc.setsockopt(Socket::Option.ipv4_multicast_ttl(@multicast_hops))
422
+ ifreq = IPAddr.new(interface_address).hton
423
+ soc.setsockopt(:IPPROTO_IP, :IP_MULTICAST_IF, ifreq)
424
+ elsif addrinfo.ipv6_multicast? then
425
+ soc.setsockopt(:IPPROTO_IPV6, :IPV6_MULTICAST_LOOP, true)
426
+ soc.setsockopt(:IPPROTO_IPV6, :IPV6_MULTICAST_HOPS,
427
+ [@multicast_hops].pack('I'))
428
+ soc.setsockopt(:IPPROTO_IPV6, :IPV6_MULTICAST_IF,
429
+ [@multicast_interface].pack('I'))
430
+ else
431
+ soc.setsockopt(:SOL_SOCKET, :SO_BROADCAST, true)
432
+ end
433
+
434
+ soc.connect(addrinfo)
435
+ rescue Exception
436
+ soc.close
437
+ raise
438
+ end
439
+
440
+ soc
441
+ end
442
+
443
+ def send_message(address, message) # :nodoc:
444
+ soc = if Array === address
445
+ make_socket(*address)
446
+ else
447
+ make_socket(address)
448
+ end
449
+
450
+ soc.send(message, 0)
451
+ rescue
452
+ nil
453
+ ensure
454
+ soc.close if soc
455
+ end
456
+
457
+ end
458
+
459
+ ##
460
+ # RingProvider uses a RingServer advertised TupleSpace as a name service.
461
+ # TupleSpace clients can register themselves with the remote TupleSpace and
462
+ # look up other provided services via the remote TupleSpace.
463
+ #
464
+ # Services are registered with a tuple of the format [:name, klass,
465
+ # DRbObject, description].
466
+
467
+ class RingProvider
468
+
469
+ ##
470
+ # Creates a RingProvider that will provide a +klass+ service running on
471
+ # +front+, with a +description+. +renewer+ is optional.
472
+
473
+ def initialize(klass, front, desc, renewer = nil)
474
+ @tuple = [:name, klass, front, desc]
475
+ @renewer = renewer || Rinda::SimpleRenewer.new
476
+ end
477
+
478
+ ##
479
+ # Advertises this service on the primary remote TupleSpace.
480
+
481
+ def provide
482
+ ts = Rinda::RingFinger.primary
483
+ ts.write(@tuple, @renewer)
484
+ end
485
+
486
+ end
487
+
488
+ end