rinda 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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