rinda2 0.1.1

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: 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