PageRenderTime 52ms CodeModel.GetById 23ms RepoModel.GetById 1ms app.codeStats 0ms

/lib/rinda/ring.rb

https://github.com/ahwuyeah/ruby
Ruby | 504 lines | 237 code | 96 blank | 171 comment | 15 complexity | 23f9d901efde1b4cd0e284782d1e5245 MD5 | raw file
Possible License(s): BSD-3-Clause, AGPL-3.0, Unlicense, GPL-2.0
  1. #
  2. # Note: Rinda::Ring API is unstable.
  3. #
  4. require 'drb/drb'
  5. require 'rinda/rinda'
  6. require 'thread'
  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. @sockets << socket
  117. if addrinfo.ipv4_multicast? or addrinfo.ipv6_multicast? then
  118. if Socket.const_defined?(:SO_REUSEPORT) then
  119. socket.setsockopt(:SOCKET, :SO_REUSEPORT, true)
  120. else
  121. socket.setsockopt(:SOCKET, :SO_REUSEADDR, true)
  122. end
  123. if addrinfo.ipv4_multicast? then
  124. interface_address = '0.0.0.0' if interface_address.nil?
  125. socket.bind(Addrinfo.udp(interface_address, @port))
  126. mreq = IPAddr.new(addrinfo.ip_address).hton +
  127. IPAddr.new(interface_address).hton
  128. socket.setsockopt(:IPPROTO_IP, :IP_ADD_MEMBERSHIP, mreq)
  129. else
  130. interface_address = '::1' if interface_address.nil?
  131. socket.bind(Addrinfo.udp(interface_address, @port))
  132. mreq = IPAddr.new(addrinfo.ip_address).hton +
  133. [multicast_interface].pack('I')
  134. socket.setsockopt(:IPPROTO_IPV6, :IPV6_JOIN_GROUP, mreq)
  135. end
  136. else
  137. socket.bind(addrinfo)
  138. end
  139. socket
  140. end
  141. ##
  142. # Creates threads that pick up UDP packets and passes them to do_write for
  143. # decoding.
  144. def write_services
  145. @sockets.map do |s|
  146. Thread.new(s) do |socket|
  147. loop do
  148. msg = socket.recv(1024)
  149. do_write(msg)
  150. end
  151. end
  152. end
  153. end
  154. ##
  155. # Extracts the response URI from +msg+ and adds it to TupleSpace where it
  156. # will be picked up by +reply_service+ for notification.
  157. def do_write(msg)
  158. Thread.new do
  159. begin
  160. tuple, sec = Marshal.load(msg)
  161. @ts.write(tuple, sec)
  162. rescue
  163. end
  164. end
  165. end
  166. ##
  167. # Creates a thread that notifies waiting clients from the TupleSpace.
  168. def reply_service
  169. Thread.new do
  170. loop do
  171. do_reply
  172. end
  173. end
  174. end
  175. ##
  176. # Pulls lookup tuples out of the TupleSpace and sends their DRb object the
  177. # address of the local TupleSpace.
  178. def do_reply
  179. tuple = @ts.take([:lookup_ring, nil], @renewer)
  180. Thread.new { tuple[1].call(@ts) rescue nil}
  181. rescue
  182. end
  183. ##
  184. # Shuts down the RingServer
  185. def shutdown
  186. @renewer.renew = false
  187. @w_services.each do |thread|
  188. thread.kill
  189. thread.join
  190. end
  191. @sockets.each do |socket|
  192. socket.close
  193. end
  194. @r_service.kill
  195. @r_service.join
  196. end
  197. end
  198. ##
  199. # RingFinger is used by RingServer clients to discover the RingServer's
  200. # TupleSpace. Typically, all a client needs to do is call
  201. # RingFinger.primary to retrieve the remote TupleSpace, which it can then
  202. # begin using.
  203. #
  204. # To find the first available remote TupleSpace:
  205. #
  206. # Rinda::RingFinger.primary
  207. #
  208. # To create a RingFinger that broadcasts to a custom list:
  209. #
  210. # rf = Rinda::RingFinger.new ['localhost', '192.0.2.1']
  211. # rf.primary
  212. #
  213. # Rinda::RingFinger also understands multicast addresses and sets them up
  214. # properly. This allows you to run multiple RingServers on the same host:
  215. #
  216. # rf = Rinda::RingFinger.new ['239.0.0.1']
  217. # rf.primary
  218. #
  219. # You can set the hop count (or TTL) for multicast searches using
  220. # #multicast_hops.
  221. #
  222. # If you use IPv6 multicast you may need to set both an address and the
  223. # outbound interface index:
  224. #
  225. # rf = Rinda::RingFinger.new ['ff02::1']
  226. # rf.multicast_interface = 1
  227. # rf.primary
  228. #
  229. # At this time there is no easy way to get an interface index by name.
  230. class RingFinger
  231. @@broadcast_list = ['<broadcast>', 'localhost']
  232. @@finger = nil
  233. ##
  234. # Creates a singleton RingFinger and looks for a RingServer. Returns the
  235. # created RingFinger.
  236. def self.finger
  237. unless @@finger
  238. @@finger = self.new
  239. @@finger.lookup_ring_any
  240. end
  241. @@finger
  242. end
  243. ##
  244. # Returns the first advertised TupleSpace.
  245. def self.primary
  246. finger.primary
  247. end
  248. ##
  249. # Contains all discovered TupleSpaces except for the primary.
  250. def self.to_a
  251. finger.to_a
  252. end
  253. ##
  254. # The list of addresses where RingFinger will send query packets.
  255. attr_accessor :broadcast_list
  256. ##
  257. # Maximum number of hops for sent multicast packets (if using a multicast
  258. # address in the broadcast list). The default is 1 (same as UDP
  259. # broadcast).
  260. attr_accessor :multicast_hops
  261. ##
  262. # The interface index to send IPv6 multicast packets from.
  263. attr_accessor :multicast_interface
  264. ##
  265. # The port that RingFinger will send query packets to.
  266. attr_accessor :port
  267. ##
  268. # Contain the first advertised TupleSpace after lookup_ring_any is called.
  269. attr_accessor :primary
  270. ##
  271. # Creates a new RingFinger that will look for RingServers at +port+ on
  272. # the addresses in +broadcast_list+.
  273. #
  274. # If +broadcast_list+ contains a multicast address then multicast queries
  275. # will be made using the given multicast_hops and multicast_interface.
  276. def initialize(broadcast_list=@@broadcast_list, port=Ring_PORT)
  277. @broadcast_list = broadcast_list || ['localhost']
  278. @port = port
  279. @primary = nil
  280. @rings = []
  281. @multicast_hops = 1
  282. @multicast_interface = 0
  283. end
  284. ##
  285. # Contains all discovered TupleSpaces except for the primary.
  286. def to_a
  287. @rings
  288. end
  289. ##
  290. # Iterates over all discovered TupleSpaces starting with the primary.
  291. def each
  292. lookup_ring_any unless @primary
  293. return unless @primary
  294. yield(@primary)
  295. @rings.each { |x| yield(x) }
  296. end
  297. ##
  298. # Looks up RingServers waiting +timeout+ seconds. RingServers will be
  299. # given +block+ as a callback, which will be called with the remote
  300. # TupleSpace.
  301. def lookup_ring(timeout=5, &block)
  302. return lookup_ring_any(timeout) unless block_given?
  303. msg = Marshal.dump([[:lookup_ring, DRbObject.new(block)], timeout])
  304. @broadcast_list.each do |it|
  305. send_message(it, msg)
  306. end
  307. sleep(timeout)
  308. end
  309. ##
  310. # Returns the first found remote TupleSpace. Any further recovered
  311. # TupleSpaces can be found by calling +to_a+.
  312. def lookup_ring_any(timeout=5)
  313. queue = Queue.new
  314. Thread.new do
  315. self.lookup_ring(timeout) do |ts|
  316. queue.push(ts)
  317. end
  318. queue.push(nil)
  319. end
  320. @primary = queue.pop
  321. raise('RingNotFound') if @primary.nil?
  322. Thread.new do
  323. while it = queue.pop
  324. @rings.push(it)
  325. end
  326. end
  327. @primary
  328. end
  329. ##
  330. # Creates a socket for +address+ with the appropriate multicast options
  331. # for multicast addresses.
  332. def make_socket(address) # :nodoc:
  333. addrinfo = Addrinfo.udp(address, @port)
  334. soc = Socket.new(addrinfo.pfamily, addrinfo.socktype, addrinfo.protocol)
  335. begin
  336. if addrinfo.ipv4_multicast? then
  337. soc.setsockopt(Socket::Option.ipv4_multicast_loop(1))
  338. soc.setsockopt(Socket::Option.ipv4_multicast_ttl(@multicast_hops))
  339. elsif addrinfo.ipv6_multicast? then
  340. soc.setsockopt(:IPPROTO_IPV6, :IPV6_MULTICAST_LOOP, true)
  341. soc.setsockopt(:IPPROTO_IPV6, :IPV6_MULTICAST_HOPS,
  342. [@multicast_hops].pack('I'))
  343. soc.setsockopt(:IPPROTO_IPV6, :IPV6_MULTICAST_IF,
  344. [@multicast_interface].pack('I'))
  345. else
  346. soc.setsockopt(:SOL_SOCKET, :SO_BROADCAST, true)
  347. end
  348. soc.connect(addrinfo)
  349. rescue Exception
  350. soc.close
  351. raise
  352. end
  353. soc
  354. end
  355. def send_message(address, message) # :nodoc:
  356. soc = make_socket(address)
  357. soc.send(message, 0)
  358. rescue
  359. nil
  360. ensure
  361. soc.close if soc
  362. end
  363. end
  364. ##
  365. # RingProvider uses a RingServer advertised TupleSpace as a name service.
  366. # TupleSpace clients can register themselves with the remote TupleSpace and
  367. # look up other provided services via the remote TupleSpace.
  368. #
  369. # Services are registered with a tuple of the format [:name, klass,
  370. # DRbObject, description].
  371. class RingProvider
  372. ##
  373. # Creates a RingProvider that will provide a +klass+ service running on
  374. # +front+, with a +description+. +renewer+ is optional.
  375. def initialize(klass, front, desc, renewer = nil)
  376. @tuple = [:name, klass, front, desc]
  377. @renewer = renewer || Rinda::SimpleRenewer.new
  378. end
  379. ##
  380. # Advertises this service on the primary remote TupleSpace.
  381. def provide
  382. ts = Rinda::RingFinger.primary
  383. ts.write(@tuple, @renewer)
  384. end
  385. end
  386. end
  387. if __FILE__ == $0
  388. DRb.start_service
  389. case ARGV.shift
  390. when 's'
  391. require 'rinda/tuplespace'
  392. ts = Rinda::TupleSpace.new
  393. Rinda::RingServer.new(ts)
  394. $stdin.gets
  395. when 'w'
  396. finger = Rinda::RingFinger.new(nil)
  397. finger.lookup_ring do |ts2|
  398. p ts2
  399. ts2.write([:hello, :world])
  400. end
  401. when 'r'
  402. finger = Rinda::RingFinger.new(nil)
  403. finger.lookup_ring do |ts2|
  404. p ts2
  405. p ts2.take([nil, nil])
  406. end
  407. end
  408. end