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.
@@ -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