PageRenderTime 38ms CodeModel.GetById 14ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/rinda/ring.rb

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