rinda 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.github/workflows/test.yml +24 -0
- data/.gitignore +8 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +36 -0
- data/Rakefile +9 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/rinda/rinda.rb +327 -0
- data/lib/rinda/ring.rb +484 -0
- data/lib/rinda/tuplespace.rb +641 -0
- data/rinda.gemspec +24 -0
- metadata +58 -0
data/lib/rinda/ring.rb
ADDED
@@ -0,0 +1,484 @@
|
|
1
|
+
# frozen_string_literal: false
|
2
|
+
#
|
3
|
+
# Note: Rinda::Ring API is unstable.
|
4
|
+
#
|
5
|
+
require 'drb/drb'
|
6
|
+
require_relative 'rinda'
|
7
|
+
require 'ipaddr'
|
8
|
+
|
9
|
+
module Rinda
|
10
|
+
|
11
|
+
##
|
12
|
+
# The default port Ring discovery will use.
|
13
|
+
|
14
|
+
Ring_PORT = 7647
|
15
|
+
|
16
|
+
##
|
17
|
+
# A RingServer allows a Rinda::TupleSpace to be located via UDP broadcasts.
|
18
|
+
# Default service location uses the following steps:
|
19
|
+
#
|
20
|
+
# 1. A RingServer begins listening on the network broadcast UDP address.
|
21
|
+
# 2. A RingFinger sends a UDP packet containing the DRb URI where it will
|
22
|
+
# listen for a reply.
|
23
|
+
# 3. The RingServer receives the UDP packet and connects back to the
|
24
|
+
# provided DRb URI with the DRb service.
|
25
|
+
#
|
26
|
+
# A RingServer requires a TupleSpace:
|
27
|
+
#
|
28
|
+
# ts = Rinda::TupleSpace.new
|
29
|
+
# rs = Rinda::RingServer.new
|
30
|
+
#
|
31
|
+
# RingServer can also listen on multicast addresses for announcements. This
|
32
|
+
# allows multiple RingServers to run on the same host. To use network
|
33
|
+
# broadcast and multicast:
|
34
|
+
#
|
35
|
+
# ts = Rinda::TupleSpace.new
|
36
|
+
# rs = Rinda::RingServer.new ts, %w[Socket::INADDR_ANY, 239.0.0.1 ff02::1]
|
37
|
+
|
38
|
+
class RingServer
|
39
|
+
|
40
|
+
include DRbUndumped
|
41
|
+
|
42
|
+
##
|
43
|
+
# Special renewer for the RingServer to allow shutdown
|
44
|
+
|
45
|
+
class Renewer # :nodoc:
|
46
|
+
include DRbUndumped
|
47
|
+
|
48
|
+
##
|
49
|
+
# Set to false to shutdown future requests using this Renewer
|
50
|
+
|
51
|
+
attr_writer :renew
|
52
|
+
|
53
|
+
def initialize # :nodoc:
|
54
|
+
@renew = true
|
55
|
+
end
|
56
|
+
|
57
|
+
def renew # :nodoc:
|
58
|
+
@renew ? 1 : true
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
##
|
63
|
+
# Advertises +ts+ on the given +addresses+ at +port+.
|
64
|
+
#
|
65
|
+
# If +addresses+ is omitted only the UDP broadcast address is used.
|
66
|
+
#
|
67
|
+
# +addresses+ can contain multiple addresses. If a multicast address is
|
68
|
+
# given in +addresses+ then the RingServer will listen for multicast
|
69
|
+
# queries.
|
70
|
+
#
|
71
|
+
# If you use IPv4 multicast you may need to set an address of the inbound
|
72
|
+
# interface which joins a multicast group.
|
73
|
+
#
|
74
|
+
# ts = Rinda::TupleSpace.new
|
75
|
+
# rs = Rinda::RingServer.new(ts, [['239.0.0.1', '9.5.1.1']])
|
76
|
+
#
|
77
|
+
# You can set addresses as an Array Object. The first element of the
|
78
|
+
# Array is a multicast address and the second is an inbound interface
|
79
|
+
# address. If the second is omitted then '0.0.0.0' is used.
|
80
|
+
#
|
81
|
+
# If you use IPv6 multicast you may need to set both the local interface
|
82
|
+
# address and the inbound interface index:
|
83
|
+
#
|
84
|
+
# rs = Rinda::RingServer.new(ts, [['ff02::1', '::1', 1]])
|
85
|
+
#
|
86
|
+
# The first element is a multicast address and the second is an inbound
|
87
|
+
# interface address. The third is an inbound interface index.
|
88
|
+
#
|
89
|
+
# At this time there is no easy way to get an interface index by name.
|
90
|
+
#
|
91
|
+
# If the second is omitted then '::1' is used.
|
92
|
+
# If the third is omitted then 0 (default interface) is used.
|
93
|
+
|
94
|
+
def initialize(ts, addresses=[Socket::INADDR_ANY], port=Ring_PORT)
|
95
|
+
@port = port
|
96
|
+
|
97
|
+
if Integer === addresses then
|
98
|
+
addresses, @port = [Socket::INADDR_ANY], addresses
|
99
|
+
end
|
100
|
+
|
101
|
+
@renewer = Renewer.new
|
102
|
+
|
103
|
+
@ts = ts
|
104
|
+
@sockets = []
|
105
|
+
addresses.each do |address|
|
106
|
+
if Array === address
|
107
|
+
make_socket(*address)
|
108
|
+
else
|
109
|
+
make_socket(address)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
@w_services = write_services
|
114
|
+
@r_service = reply_service
|
115
|
+
end
|
116
|
+
|
117
|
+
##
|
118
|
+
# Creates a socket at +address+
|
119
|
+
#
|
120
|
+
# If +address+ is multicast address then +interface_address+ and
|
121
|
+
# +multicast_interface+ can be set as optional.
|
122
|
+
#
|
123
|
+
# A created socket is bound to +interface_address+. If you use IPv4
|
124
|
+
# multicast then the interface of +interface_address+ is used as the
|
125
|
+
# inbound interface. If +interface_address+ is omitted or nil then
|
126
|
+
# '0.0.0.0' or '::1' is used.
|
127
|
+
#
|
128
|
+
# If you use IPv6 multicast then +multicast_interface+ is used as the
|
129
|
+
# inbound interface. +multicast_interface+ is a network interface index.
|
130
|
+
# If +multicast_interface+ is omitted then 0 (default interface) is used.
|
131
|
+
|
132
|
+
def make_socket(address, interface_address=nil, multicast_interface=0)
|
133
|
+
addrinfo = Addrinfo.udp(address, @port)
|
134
|
+
|
135
|
+
socket = Socket.new(addrinfo.pfamily, addrinfo.socktype,
|
136
|
+
addrinfo.protocol)
|
137
|
+
|
138
|
+
if addrinfo.ipv4_multicast? or addrinfo.ipv6_multicast? then
|
139
|
+
if Socket.const_defined?(:SO_REUSEPORT) then
|
140
|
+
socket.setsockopt(:SOCKET, :SO_REUSEPORT, true)
|
141
|
+
else
|
142
|
+
socket.setsockopt(:SOCKET, :SO_REUSEADDR, true)
|
143
|
+
end
|
144
|
+
|
145
|
+
if addrinfo.ipv4_multicast? then
|
146
|
+
interface_address = '0.0.0.0' if interface_address.nil?
|
147
|
+
socket.bind(Addrinfo.udp(interface_address, @port))
|
148
|
+
|
149
|
+
mreq = IPAddr.new(addrinfo.ip_address).hton +
|
150
|
+
IPAddr.new(interface_address).hton
|
151
|
+
|
152
|
+
socket.setsockopt(:IPPROTO_IP, :IP_ADD_MEMBERSHIP, mreq)
|
153
|
+
else
|
154
|
+
interface_address = '::1' if interface_address.nil?
|
155
|
+
socket.bind(Addrinfo.udp(interface_address, @port))
|
156
|
+
|
157
|
+
mreq = IPAddr.new(addrinfo.ip_address).hton +
|
158
|
+
[multicast_interface].pack('I')
|
159
|
+
|
160
|
+
socket.setsockopt(:IPPROTO_IPV6, :IPV6_JOIN_GROUP, mreq)
|
161
|
+
end
|
162
|
+
else
|
163
|
+
socket.bind(addrinfo)
|
164
|
+
end
|
165
|
+
|
166
|
+
socket
|
167
|
+
rescue
|
168
|
+
socket = socket.close if socket
|
169
|
+
raise
|
170
|
+
ensure
|
171
|
+
@sockets << socket if socket
|
172
|
+
end
|
173
|
+
|
174
|
+
##
|
175
|
+
# Creates threads that pick up UDP packets and passes them to do_write for
|
176
|
+
# decoding.
|
177
|
+
|
178
|
+
def write_services
|
179
|
+
@sockets.map do |s|
|
180
|
+
Thread.new(s) do |socket|
|
181
|
+
loop do
|
182
|
+
msg = socket.recv(1024)
|
183
|
+
do_write(msg)
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
##
|
190
|
+
# Extracts the response URI from +msg+ and adds it to TupleSpace where it
|
191
|
+
# will be picked up by +reply_service+ for notification.
|
192
|
+
|
193
|
+
def do_write(msg)
|
194
|
+
Thread.new do
|
195
|
+
begin
|
196
|
+
tuple, sec = Marshal.load(msg)
|
197
|
+
@ts.write(tuple, sec)
|
198
|
+
rescue
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
##
|
204
|
+
# Creates a thread that notifies waiting clients from the TupleSpace.
|
205
|
+
|
206
|
+
def reply_service
|
207
|
+
Thread.new do
|
208
|
+
loop do
|
209
|
+
do_reply
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
##
|
215
|
+
# Pulls lookup tuples out of the TupleSpace and sends their DRb object the
|
216
|
+
# address of the local TupleSpace.
|
217
|
+
|
218
|
+
def do_reply
|
219
|
+
tuple = @ts.take([:lookup_ring, nil], @renewer)
|
220
|
+
Thread.new { tuple[1].call(@ts) rescue nil}
|
221
|
+
rescue
|
222
|
+
end
|
223
|
+
|
224
|
+
##
|
225
|
+
# Shuts down the RingServer
|
226
|
+
|
227
|
+
def shutdown
|
228
|
+
@renewer.renew = false
|
229
|
+
|
230
|
+
@w_services.each do |thread|
|
231
|
+
thread.kill
|
232
|
+
thread.join
|
233
|
+
end
|
234
|
+
|
235
|
+
@sockets.each do |socket|
|
236
|
+
socket.close
|
237
|
+
end
|
238
|
+
|
239
|
+
@r_service.kill
|
240
|
+
@r_service.join
|
241
|
+
end
|
242
|
+
|
243
|
+
end
|
244
|
+
|
245
|
+
##
|
246
|
+
# RingFinger is used by RingServer clients to discover the RingServer's
|
247
|
+
# TupleSpace. Typically, all a client needs to do is call
|
248
|
+
# RingFinger.primary to retrieve the remote TupleSpace, which it can then
|
249
|
+
# begin using.
|
250
|
+
#
|
251
|
+
# To find the first available remote TupleSpace:
|
252
|
+
#
|
253
|
+
# Rinda::RingFinger.primary
|
254
|
+
#
|
255
|
+
# To create a RingFinger that broadcasts to a custom list:
|
256
|
+
#
|
257
|
+
# rf = Rinda::RingFinger.new ['localhost', '192.0.2.1']
|
258
|
+
# rf.primary
|
259
|
+
#
|
260
|
+
# Rinda::RingFinger also understands multicast addresses and sets them up
|
261
|
+
# properly. This allows you to run multiple RingServers on the same host:
|
262
|
+
#
|
263
|
+
# rf = Rinda::RingFinger.new ['239.0.0.1']
|
264
|
+
# rf.primary
|
265
|
+
#
|
266
|
+
# You can set the hop count (or TTL) for multicast searches using
|
267
|
+
# #multicast_hops.
|
268
|
+
#
|
269
|
+
# If you use IPv6 multicast you may need to set both an address and the
|
270
|
+
# outbound interface index:
|
271
|
+
#
|
272
|
+
# rf = Rinda::RingFinger.new ['ff02::1']
|
273
|
+
# rf.multicast_interface = 1
|
274
|
+
# rf.primary
|
275
|
+
#
|
276
|
+
# At this time there is no easy way to get an interface index by name.
|
277
|
+
|
278
|
+
class RingFinger
|
279
|
+
|
280
|
+
@@broadcast_list = ['<broadcast>', 'localhost']
|
281
|
+
|
282
|
+
@@finger = nil
|
283
|
+
|
284
|
+
##
|
285
|
+
# Creates a singleton RingFinger and looks for a RingServer. Returns the
|
286
|
+
# created RingFinger.
|
287
|
+
|
288
|
+
def self.finger
|
289
|
+
unless @@finger
|
290
|
+
@@finger = self.new
|
291
|
+
@@finger.lookup_ring_any
|
292
|
+
end
|
293
|
+
@@finger
|
294
|
+
end
|
295
|
+
|
296
|
+
##
|
297
|
+
# Returns the first advertised TupleSpace.
|
298
|
+
|
299
|
+
def self.primary
|
300
|
+
finger.primary
|
301
|
+
end
|
302
|
+
|
303
|
+
##
|
304
|
+
# Contains all discovered TupleSpaces except for the primary.
|
305
|
+
|
306
|
+
def self.to_a
|
307
|
+
finger.to_a
|
308
|
+
end
|
309
|
+
|
310
|
+
##
|
311
|
+
# The list of addresses where RingFinger will send query packets.
|
312
|
+
|
313
|
+
attr_accessor :broadcast_list
|
314
|
+
|
315
|
+
##
|
316
|
+
# Maximum number of hops for sent multicast packets (if using a multicast
|
317
|
+
# address in the broadcast list). The default is 1 (same as UDP
|
318
|
+
# broadcast).
|
319
|
+
|
320
|
+
attr_accessor :multicast_hops
|
321
|
+
|
322
|
+
##
|
323
|
+
# The interface index to send IPv6 multicast packets from.
|
324
|
+
|
325
|
+
attr_accessor :multicast_interface
|
326
|
+
|
327
|
+
##
|
328
|
+
# The port that RingFinger will send query packets to.
|
329
|
+
|
330
|
+
attr_accessor :port
|
331
|
+
|
332
|
+
##
|
333
|
+
# Contain the first advertised TupleSpace after lookup_ring_any is called.
|
334
|
+
|
335
|
+
attr_accessor :primary
|
336
|
+
|
337
|
+
##
|
338
|
+
# Creates a new RingFinger that will look for RingServers at +port+ on
|
339
|
+
# the addresses in +broadcast_list+.
|
340
|
+
#
|
341
|
+
# If +broadcast_list+ contains a multicast address then multicast queries
|
342
|
+
# will be made using the given multicast_hops and multicast_interface.
|
343
|
+
|
344
|
+
def initialize(broadcast_list=@@broadcast_list, port=Ring_PORT)
|
345
|
+
@broadcast_list = broadcast_list || ['localhost']
|
346
|
+
@port = port
|
347
|
+
@primary = nil
|
348
|
+
@rings = []
|
349
|
+
|
350
|
+
@multicast_hops = 1
|
351
|
+
@multicast_interface = 0
|
352
|
+
end
|
353
|
+
|
354
|
+
##
|
355
|
+
# Contains all discovered TupleSpaces except for the primary.
|
356
|
+
|
357
|
+
def to_a
|
358
|
+
@rings
|
359
|
+
end
|
360
|
+
|
361
|
+
##
|
362
|
+
# Iterates over all discovered TupleSpaces starting with the primary.
|
363
|
+
|
364
|
+
def each
|
365
|
+
lookup_ring_any unless @primary
|
366
|
+
return unless @primary
|
367
|
+
yield(@primary)
|
368
|
+
@rings.each { |x| yield(x) }
|
369
|
+
end
|
370
|
+
|
371
|
+
##
|
372
|
+
# Looks up RingServers waiting +timeout+ seconds. RingServers will be
|
373
|
+
# given +block+ as a callback, which will be called with the remote
|
374
|
+
# TupleSpace.
|
375
|
+
|
376
|
+
def lookup_ring(timeout=5, &block)
|
377
|
+
return lookup_ring_any(timeout) unless block_given?
|
378
|
+
|
379
|
+
msg = Marshal.dump([[:lookup_ring, DRbObject.new(block)], timeout])
|
380
|
+
@broadcast_list.each do |it|
|
381
|
+
send_message(it, msg)
|
382
|
+
end
|
383
|
+
sleep(timeout)
|
384
|
+
end
|
385
|
+
|
386
|
+
##
|
387
|
+
# Returns the first found remote TupleSpace. Any further recovered
|
388
|
+
# TupleSpaces can be found by calling +to_a+.
|
389
|
+
|
390
|
+
def lookup_ring_any(timeout=5)
|
391
|
+
queue = Thread::Queue.new
|
392
|
+
|
393
|
+
Thread.new do
|
394
|
+
self.lookup_ring(timeout) do |ts|
|
395
|
+
queue.push(ts)
|
396
|
+
end
|
397
|
+
queue.push(nil)
|
398
|
+
end
|
399
|
+
|
400
|
+
@primary = queue.pop
|
401
|
+
raise('RingNotFound') if @primary.nil?
|
402
|
+
|
403
|
+
Thread.new do
|
404
|
+
while it = queue.pop
|
405
|
+
@rings.push(it)
|
406
|
+
end
|
407
|
+
end
|
408
|
+
|
409
|
+
@primary
|
410
|
+
end
|
411
|
+
|
412
|
+
##
|
413
|
+
# Creates a socket for +address+ with the appropriate multicast options
|
414
|
+
# for multicast addresses.
|
415
|
+
|
416
|
+
def make_socket(address) # :nodoc:
|
417
|
+
addrinfo = Addrinfo.udp(address, @port)
|
418
|
+
|
419
|
+
soc = Socket.new(addrinfo.pfamily, addrinfo.socktype, addrinfo.protocol)
|
420
|
+
begin
|
421
|
+
if addrinfo.ipv4_multicast? then
|
422
|
+
soc.setsockopt(Socket::Option.ipv4_multicast_loop(1))
|
423
|
+
soc.setsockopt(Socket::Option.ipv4_multicast_ttl(@multicast_hops))
|
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 = make_socket(address)
|
445
|
+
|
446
|
+
soc.send(message, 0)
|
447
|
+
rescue
|
448
|
+
nil
|
449
|
+
ensure
|
450
|
+
soc.close if soc
|
451
|
+
end
|
452
|
+
|
453
|
+
end
|
454
|
+
|
455
|
+
##
|
456
|
+
# RingProvider uses a RingServer advertised TupleSpace as a name service.
|
457
|
+
# TupleSpace clients can register themselves with the remote TupleSpace and
|
458
|
+
# look up other provided services via the remote TupleSpace.
|
459
|
+
#
|
460
|
+
# Services are registered with a tuple of the format [:name, klass,
|
461
|
+
# DRbObject, description].
|
462
|
+
|
463
|
+
class RingProvider
|
464
|
+
|
465
|
+
##
|
466
|
+
# Creates a RingProvider that will provide a +klass+ service running on
|
467
|
+
# +front+, with a +description+. +renewer+ is optional.
|
468
|
+
|
469
|
+
def initialize(klass, front, desc, renewer = nil)
|
470
|
+
@tuple = [:name, klass, front, desc]
|
471
|
+
@renewer = renewer || Rinda::SimpleRenewer.new
|
472
|
+
end
|
473
|
+
|
474
|
+
##
|
475
|
+
# Advertises this service on the primary remote TupleSpace.
|
476
|
+
|
477
|
+
def provide
|
478
|
+
ts = Rinda::RingFinger.primary
|
479
|
+
ts.write(@tuple, @renewer)
|
480
|
+
end
|
481
|
+
|
482
|
+
end
|
483
|
+
|
484
|
+
end
|