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.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +41 -0
- data/Rakefile +2 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/rinda2.rb +8 -0
- data/lib/rinda2/rinda.rb +291 -0
- data/lib/rinda2/ring.rb +488 -0
- data/lib/rinda2/tuplespace.rb +642 -0
- data/lib/rinda2/version.rb +3 -0
- data/rinda2.gemspec +30 -0
- metadata +87 -0
checksums.yaml
ADDED
@@ -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
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -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.
|
data/README.md
ADDED
@@ -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
|
+
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -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
|
data/bin/setup
ADDED
data/lib/rinda2.rb
ADDED
data/lib/rinda2/rinda.rb
ADDED
@@ -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
|
data/lib/rinda2/ring.rb
ADDED
@@ -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
|