├── .github ├── dependabot.yml └── workflows │ └── test.yml ├── .gitignore ├── BSDL ├── COPYING ├── Gemfile ├── README.md ├── Rakefile ├── bin ├── console └── setup ├── lib └── rinda │ ├── rinda.rb │ ├── ring.rb │ └── tuplespace.rb ├── rinda.gemspec └── test └── rinda ├── test_rinda.rb └── test_tuplebag.rb /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: 'github-actions' 4 | directory: '/' 5 | schedule: 6 | interval: 'weekly' 7 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | ruby-versions: 7 | uses: ruby/actions/.github/workflows/ruby_versions.yml@master 8 | with: 9 | engine: cruby 10 | min_version: 2.7 11 | 12 | test: 13 | needs: ruby-versions 14 | name: build (${{ matrix.ruby }} / ${{ matrix.os }}) 15 | strategy: 16 | matrix: 17 | ruby: ${{ fromJson(needs.ruby-versions.outputs.versions) }} 18 | os: [ ubuntu-latest, macos-latest ] 19 | runs-on: ${{ matrix.os }} 20 | steps: 21 | - uses: actions/checkout@v4 22 | - name: Set up Ruby 23 | uses: ruby/setup-ruby@v1 24 | with: 25 | ruby-version: ${{ matrix.ruby }} 26 | - run: bundle install --jobs 4 --retry 3 27 | - name: Run test 28 | run: rake test 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | /Gemfile.lock 10 | -------------------------------------------------------------------------------- /BSDL: -------------------------------------------------------------------------------- 1 | Copyright (C) 1993-2013 Yukihiro Matsumoto. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions 5 | are met: 6 | 1. Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | 2. Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | 12 | THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND 13 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 14 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 15 | ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 16 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 17 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 18 | OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 19 | HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 20 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 21 | OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 22 | SUCH DAMAGE. 23 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | Ruby is copyrighted free software by Yukihiro Matsumoto . 2 | You can redistribute it and/or modify it under either the terms of the 3 | 2-clause BSDL (see the file BSDL), or the conditions below: 4 | 5 | 1. You may make and give away verbatim copies of the source form of the 6 | software without restriction, provided that you duplicate all of the 7 | original copyright notices and associated disclaimers. 8 | 9 | 2. You may modify your copy of the software in any way, provided that 10 | you do at least ONE of the following: 11 | 12 | a. place your modifications in the Public Domain or otherwise 13 | make them Freely Available, such as by posting said 14 | modifications to Usenet or an equivalent medium, or by allowing 15 | the author to include your modifications in the software. 16 | 17 | b. use the modified software only within your corporation or 18 | organization. 19 | 20 | c. give non-standard binaries non-standard names, with 21 | instructions on where to get the original software distribution. 22 | 23 | d. make other distribution arrangements with the author. 24 | 25 | 3. You may distribute the software in object code or binary form, 26 | provided that you do at least ONE of the following: 27 | 28 | a. distribute the binaries and library files of the software, 29 | together with instructions (in the manual page or equivalent) 30 | on where to get the original distribution. 31 | 32 | b. accompany the distribution with the machine-readable source of 33 | the software. 34 | 35 | c. give non-standard binaries non-standard names, with 36 | instructions on where to get the original software distribution. 37 | 38 | d. make other distribution arrangements with the author. 39 | 40 | 4. You may modify and include the part of the software into any other 41 | software (possibly commercial). But some files in the distribution 42 | are not written by the author, so that they are not under these terms. 43 | 44 | For the list of those files and their copying conditions, see the 45 | file LEGAL. 46 | 47 | 5. The scripts and library files supplied as input to or produced as 48 | output from the software do not automatically fall under the 49 | copyright of the software, but belong to whomever generated them, 50 | and may be sold commercially, and may be aggregated with this 51 | software. 52 | 53 | 6. THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR 54 | IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED 55 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 56 | PURPOSE. 57 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | 5 | gem "rake" 6 | gem "test-unit" 7 | gem "test-unit-ruby-core" 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rinda 2 | 3 | A module to implement the Linda distributed computing paradigm in Ruby. 4 | 5 | Rinda is part of DRb (dRuby). 6 | 7 | ## Installation 8 | 9 | Add this line to your application's Gemfile: 10 | 11 | ```ruby 12 | gem 'rinda' 13 | ``` 14 | 15 | And then execute: 16 | 17 | $ bundle install 18 | 19 | Or install it yourself as: 20 | 21 | $ gem install rinda 22 | 23 | ## Development 24 | 25 | After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. 26 | 27 | To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). 28 | 29 | ## Contributing 30 | 31 | Bug reports and pull requests are welcome on GitHub at https://github.com/ruby/rinda. 32 | 33 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rake/testtask" 3 | 4 | Rake::TestTask.new(:test) do |t| 5 | t.libs << "test/lib" 6 | t.test_files = FileList["test/**/test_*.rb"] 7 | end 8 | 9 | task :default => :test 10 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "rinda" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require "irb" 14 | IRB.start(__FILE__) 15 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /lib/rinda/rinda.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: false 2 | require 'drb/drb' 3 | 4 | ## 5 | # A module to implement the Linda distributed computing paradigm in Ruby. 6 | # 7 | # Rinda is part of DRb (dRuby). 8 | # 9 | # == Example(s) 10 | # 11 | # See the sample/drb/ directory in the Ruby distribution, from 1.8.2 onwards. 12 | # 13 | #-- 14 | # TODO 15 | # == Introduction to Linda/rinda? 16 | # 17 | # == Why is this library separate from DRb? 18 | 19 | module Rinda 20 | 21 | VERSION = "0.2.0" 22 | 23 | ## 24 | # Rinda error base class 25 | 26 | class RindaError < RuntimeError; end 27 | 28 | ## 29 | # Raised when a hash-based tuple has an invalid key. 30 | 31 | class InvalidHashTupleKey < RindaError; end 32 | 33 | ## 34 | # Raised when trying to use a canceled tuple. 35 | 36 | class RequestCanceledError < ThreadError; end 37 | 38 | ## 39 | # Raised when trying to use an expired tuple. 40 | 41 | class RequestExpiredError < ThreadError; end 42 | 43 | ## 44 | # A tuple is the elementary object in Rinda programming. 45 | # Tuples may be matched against templates if the tuple and 46 | # the template are the same size. 47 | 48 | class Tuple 49 | 50 | ## 51 | # Creates a new Tuple from +ary_or_hash+ which must be an Array or Hash. 52 | 53 | def initialize(ary_or_hash) 54 | if hash?(ary_or_hash) 55 | init_with_hash(ary_or_hash) 56 | else 57 | init_with_ary(ary_or_hash) 58 | end 59 | end 60 | 61 | ## 62 | # The number of elements in the tuple. 63 | 64 | def size 65 | @tuple.size 66 | end 67 | 68 | ## 69 | # Accessor method for elements of the tuple. 70 | 71 | def [](k) 72 | @tuple[k] 73 | end 74 | 75 | ## 76 | # Fetches item +k+ from the tuple. 77 | 78 | def fetch(k) 79 | @tuple.fetch(k) 80 | end 81 | 82 | ## 83 | # Iterate through the tuple, yielding the index or key, and the 84 | # value, thus ensuring arrays are iterated similarly to hashes. 85 | 86 | def each # FIXME 87 | if Hash === @tuple 88 | @tuple.each { |k, v| yield(k, v) } 89 | else 90 | @tuple.each_with_index { |v, k| yield(k, v) } 91 | end 92 | end 93 | 94 | ## 95 | # Return the tuple itself 96 | def value 97 | @tuple 98 | end 99 | 100 | private 101 | 102 | def hash?(ary_or_hash) 103 | ary_or_hash.respond_to?(:keys) 104 | end 105 | 106 | ## 107 | # Munges +ary+ into a valid Tuple. 108 | 109 | def init_with_ary(ary) 110 | @tuple = Array.new(ary.size) 111 | @tuple.size.times do |i| 112 | @tuple[i] = ary[i] 113 | end 114 | end 115 | 116 | ## 117 | # Ensures +hash+ is a valid Tuple. 118 | 119 | def init_with_hash(hash) 120 | @tuple = Hash.new 121 | hash.each do |k, v| 122 | raise InvalidHashTupleKey unless String === k 123 | @tuple[k] = v 124 | end 125 | end 126 | 127 | end 128 | 129 | ## 130 | # Templates are used to match tuples in Rinda. 131 | 132 | class Template < Tuple 133 | 134 | ## 135 | # Matches this template against +tuple+. The +tuple+ must be the same 136 | # size as the template. An element with a +nil+ value in a template acts 137 | # as a wildcard, matching any value in the corresponding position in the 138 | # tuple. Elements of the template match the +tuple+ if the are #== or 139 | # #===. 140 | # 141 | # Template.new([:foo, 5]).match Tuple.new([:foo, 5]) # => true 142 | # Template.new([:foo, nil]).match Tuple.new([:foo, 5]) # => true 143 | # Template.new([String]).match Tuple.new(['hello']) # => true 144 | # 145 | # Template.new([:foo]).match Tuple.new([:foo, 5]) # => false 146 | # Template.new([:foo, 6]).match Tuple.new([:foo, 5]) # => false 147 | # Template.new([:foo, nil]).match Tuple.new([:foo]) # => false 148 | # Template.new([:foo, 6]).match Tuple.new([:foo]) # => false 149 | 150 | def match(tuple) 151 | return false unless tuple.respond_to?(:size) 152 | return false unless tuple.respond_to?(:fetch) 153 | return false unless self.size == tuple.size 154 | each do |k, v| 155 | begin 156 | it = tuple.fetch(k) 157 | rescue 158 | return false 159 | end 160 | next if v.nil? 161 | next if v == it 162 | next if v === it 163 | return false 164 | end 165 | return true 166 | end 167 | 168 | ## 169 | # Alias for #match. 170 | 171 | def ===(tuple) 172 | match(tuple) 173 | end 174 | 175 | end 176 | 177 | ## 178 | # Documentation? 179 | 180 | class DRbObjectTemplate 181 | 182 | ## 183 | # Creates a new DRbObjectTemplate that will match against +uri+ and +ref+. 184 | 185 | def initialize(uri=nil, ref=nil) 186 | @drb_uri = uri 187 | @drb_ref = ref 188 | end 189 | 190 | ## 191 | # This DRbObjectTemplate matches +ro+ if the remote object's drburi and 192 | # drbref are the same. +nil+ is used as a wildcard. 193 | 194 | def ===(ro) 195 | return true if super(ro) 196 | unless @drb_uri.nil? 197 | return false unless (@drb_uri === ro.__drburi rescue false) 198 | end 199 | unless @drb_ref.nil? 200 | return false unless (@drb_ref === ro.__drbref rescue false) 201 | end 202 | true 203 | end 204 | 205 | end 206 | 207 | ## 208 | # TupleSpaceProxy allows a remote Tuplespace to appear as local. 209 | 210 | class TupleSpaceProxy 211 | ## 212 | # A Port ensures that a moved tuple arrives properly at its destination 213 | # and does not get lost. 214 | # 215 | # See https://bugs.ruby-lang.org/issues/8125 216 | 217 | class Port # :nodoc: 218 | attr_reader :value 219 | 220 | def self.deliver 221 | port = new 222 | 223 | begin 224 | yield(port) 225 | ensure 226 | port.close 227 | end 228 | 229 | port.value 230 | end 231 | 232 | def initialize 233 | @open = true 234 | @value = nil 235 | end 236 | 237 | ## 238 | # Don't let the DRb thread push to it when remote sends tuple 239 | 240 | def close 241 | @open = false 242 | end 243 | 244 | ## 245 | # Stores +value+ and ensure it does not get marshaled multiple times. 246 | 247 | def push value 248 | raise 'port closed' unless @open 249 | 250 | @value = value 251 | 252 | nil # avoid Marshal 253 | end 254 | end 255 | 256 | ## 257 | # Creates a new TupleSpaceProxy to wrap +ts+. 258 | 259 | def initialize(ts) 260 | @ts = ts 261 | end 262 | 263 | ## 264 | # Adds +tuple+ to the proxied TupleSpace. See TupleSpace#write. 265 | 266 | def write(tuple, sec=nil) 267 | @ts.write(tuple, sec) 268 | end 269 | 270 | ## 271 | # Takes +tuple+ from the proxied TupleSpace. See TupleSpace#take. 272 | 273 | def take(tuple, sec=nil, &block) 274 | Port.deliver do |port| 275 | @ts.move(DRbObject.new(port), tuple, sec, &block) 276 | end 277 | end 278 | 279 | ## 280 | # Reads +tuple+ from the proxied TupleSpace. See TupleSpace#read. 281 | 282 | def read(tuple, sec=nil, &block) 283 | @ts.read(tuple, sec, &block) 284 | end 285 | 286 | ## 287 | # Reads all tuples matching +tuple+ from the proxied TupleSpace. See 288 | # TupleSpace#read_all. 289 | 290 | def read_all(tuple) 291 | @ts.read_all(tuple) 292 | end 293 | 294 | ## 295 | # Registers for notifications of event +ev+ on the proxied TupleSpace. 296 | # See TupleSpace#notify 297 | 298 | def notify(ev, tuple, sec=nil) 299 | @ts.notify(ev, tuple, sec) 300 | end 301 | 302 | end 303 | 304 | ## 305 | # An SimpleRenewer allows a TupleSpace to check if a TupleEntry is still 306 | # alive. 307 | 308 | class SimpleRenewer 309 | 310 | include DRbUndumped 311 | 312 | ## 313 | # Creates a new SimpleRenewer that keeps an object alive for another +sec+ 314 | # seconds. 315 | 316 | def initialize(sec=180) 317 | @sec = sec 318 | end 319 | 320 | ## 321 | # Called by the TupleSpace to check if the object is still alive. 322 | 323 | def renew 324 | @sec 325 | end 326 | end 327 | 328 | end 329 | 330 | -------------------------------------------------------------------------------- /lib/rinda/ring.rb: -------------------------------------------------------------------------------- 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 = ['', '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 485 | -------------------------------------------------------------------------------- /lib/rinda/tuplespace.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: false 2 | require 'monitor' 3 | require 'drb/drb' 4 | require_relative 'rinda' 5 | require 'forwardable' 6 | 7 | module Rinda 8 | 9 | ## 10 | # A TupleEntry is a Tuple (i.e. a possible entry in some Tuplespace) 11 | # together with expiry and cancellation data. 12 | 13 | class TupleEntry 14 | 15 | include DRbUndumped 16 | 17 | attr_accessor :expires 18 | 19 | ## 20 | # Creates a TupleEntry based on +ary+ with an optional renewer or expiry 21 | # time +sec+. 22 | # 23 | # A renewer must implement the +renew+ method which returns a Numeric, 24 | # nil, or true to indicate when the tuple has expired. 25 | 26 | def initialize(ary, sec=nil) 27 | @cancel = false 28 | @expires = nil 29 | @tuple = make_tuple(ary) 30 | @renewer = nil 31 | renew(sec) 32 | end 33 | 34 | ## 35 | # Marks this TupleEntry as canceled. 36 | 37 | def cancel 38 | @cancel = true 39 | end 40 | 41 | ## 42 | # A TupleEntry is dead when it is canceled or expired. 43 | 44 | def alive? 45 | !canceled? && !expired? 46 | end 47 | 48 | ## 49 | # Return the object which makes up the tuple itself: the Array 50 | # or Hash. 51 | 52 | def value; @tuple.value; end 53 | 54 | ## 55 | # Returns the canceled status. 56 | 57 | def canceled?; @cancel; end 58 | 59 | ## 60 | # Has this tuple expired? (true/false). 61 | # 62 | # A tuple has expired when its expiry timer based on the +sec+ argument to 63 | # #initialize runs out. 64 | 65 | def expired? 66 | return true unless @expires 67 | return false if @expires > Time.now 68 | return true if @renewer.nil? 69 | renew(@renewer) 70 | return true unless @expires 71 | return @expires < Time.now 72 | end 73 | 74 | ## 75 | # Reset the expiry time according to +sec_or_renewer+. 76 | # 77 | # +nil+:: it is set to expire in the far future. 78 | # +true+:: it has expired. 79 | # Numeric:: it will expire in that many seconds. 80 | # 81 | # Otherwise the argument refers to some kind of renewer object 82 | # which will reset its expiry time. 83 | 84 | def renew(sec_or_renewer) 85 | sec, @renewer = get_renewer(sec_or_renewer) 86 | @expires = make_expires(sec) 87 | end 88 | 89 | ## 90 | # Returns an expiry Time based on +sec+ which can be one of: 91 | # Numeric:: +sec+ seconds into the future 92 | # +true+:: the expiry time is the start of 1970 (i.e. expired) 93 | # +nil+:: it is Tue Jan 19 03:14:07 GMT Standard Time 2038 (i.e. when 94 | # UNIX clocks will die) 95 | 96 | def make_expires(sec=nil) 97 | case sec 98 | when Numeric 99 | Time.now + sec 100 | when true 101 | Time.at(1) 102 | when nil 103 | Time.at(2**31-1) 104 | end 105 | end 106 | 107 | ## 108 | # Retrieves +key+ from the tuple. 109 | 110 | def [](key) 111 | @tuple[key] 112 | end 113 | 114 | ## 115 | # Fetches +key+ from the tuple. 116 | 117 | def fetch(key) 118 | @tuple.fetch(key) 119 | end 120 | 121 | ## 122 | # The size of the tuple. 123 | 124 | def size 125 | @tuple.size 126 | end 127 | 128 | ## 129 | # Creates a Rinda::Tuple for +ary+. 130 | 131 | def make_tuple(ary) 132 | Rinda::Tuple.new(ary) 133 | end 134 | 135 | private 136 | 137 | ## 138 | # Returns a valid argument to make_expires and the renewer or nil. 139 | # 140 | # Given +true+, +nil+, or Numeric, returns that value and +nil+ (no actual 141 | # renewer). Otherwise it returns an expiry value from calling +it.renew+ 142 | # and the renewer. 143 | 144 | def get_renewer(it) 145 | case it 146 | when Numeric, true, nil 147 | return it, nil 148 | else 149 | begin 150 | return it.renew, it 151 | rescue Exception 152 | return it, nil 153 | end 154 | end 155 | end 156 | 157 | end 158 | 159 | ## 160 | # A TemplateEntry is a Template together with expiry and cancellation data. 161 | 162 | class TemplateEntry < TupleEntry 163 | ## 164 | # Matches this TemplateEntry against +tuple+. See Template#match for 165 | # details on how a Template matches a Tuple. 166 | 167 | def match(tuple) 168 | @tuple.match(tuple) 169 | end 170 | 171 | alias === match 172 | 173 | def make_tuple(ary) # :nodoc: 174 | Rinda::Template.new(ary) 175 | end 176 | 177 | end 178 | 179 | ## 180 | # Documentation? 181 | 182 | class WaitTemplateEntry < TemplateEntry 183 | 184 | attr_reader :found 185 | 186 | def initialize(place, ary, expires=nil) 187 | super(ary, expires) 188 | @place = place 189 | @cond = place.new_cond 190 | @found = nil 191 | end 192 | 193 | def cancel 194 | super 195 | signal 196 | end 197 | 198 | def wait 199 | @cond.wait 200 | end 201 | 202 | def read(tuple) 203 | @found = tuple 204 | signal 205 | end 206 | 207 | def signal 208 | @place.synchronize do 209 | @cond.signal 210 | end 211 | end 212 | 213 | end 214 | 215 | ## 216 | # A NotifyTemplateEntry is returned by TupleSpace#notify and is notified of 217 | # TupleSpace changes. You may receive either your subscribed event or the 218 | # 'close' event when iterating over notifications. 219 | # 220 | # See TupleSpace#notify_event for valid notification types. 221 | # 222 | # == Example 223 | # 224 | # ts = Rinda::TupleSpace.new 225 | # observer = ts.notify 'write', [nil] 226 | # 227 | # Thread.start do 228 | # observer.each { |t| p t } 229 | # end 230 | # 231 | # 3.times { |i| ts.write [i] } 232 | # 233 | # Outputs: 234 | # 235 | # ['write', [0]] 236 | # ['write', [1]] 237 | # ['write', [2]] 238 | 239 | class NotifyTemplateEntry < TemplateEntry 240 | 241 | ## 242 | # Creates a new NotifyTemplateEntry that watches +place+ for +event+s that 243 | # match +tuple+. 244 | 245 | def initialize(place, event, tuple, expires=nil) 246 | ary = [event, Rinda::Template.new(tuple)] 247 | super(ary, expires) 248 | @queue = Thread::Queue.new 249 | @done = false 250 | end 251 | 252 | ## 253 | # Called by TupleSpace to notify this NotifyTemplateEntry of a new event. 254 | 255 | def notify(ev) 256 | @queue.push(ev) 257 | end 258 | 259 | ## 260 | # Retrieves a notification. Raises RequestExpiredError when this 261 | # NotifyTemplateEntry expires. 262 | 263 | def pop 264 | raise RequestExpiredError if @done 265 | it = @queue.pop 266 | @done = true if it[0] == 'close' 267 | return it 268 | end 269 | 270 | ## 271 | # Yields event/tuple pairs until this NotifyTemplateEntry expires. 272 | 273 | def each # :yields: event, tuple 274 | while !@done 275 | it = pop 276 | yield(it) 277 | end 278 | rescue 279 | ensure 280 | cancel 281 | end 282 | 283 | end 284 | 285 | ## 286 | # TupleBag is an unordered collection of tuples. It is the basis 287 | # of Tuplespace. 288 | 289 | class TupleBag 290 | class TupleBin 291 | extend Forwardable 292 | def_delegators '@bin', :find_all, :delete_if, :each, :empty? 293 | 294 | def initialize 295 | @bin = [] 296 | end 297 | 298 | def add(tuple) 299 | @bin.push(tuple) 300 | end 301 | 302 | def delete(tuple) 303 | idx = @bin.rindex(tuple) 304 | @bin.delete_at(idx) if idx 305 | end 306 | 307 | def find 308 | @bin.reverse_each do |x| 309 | return x if yield(x) 310 | end 311 | nil 312 | end 313 | end 314 | 315 | def initialize # :nodoc: 316 | @hash = {} 317 | @enum = enum_for(:each_entry) 318 | end 319 | 320 | ## 321 | # +true+ if the TupleBag to see if it has any expired entries. 322 | 323 | def has_expires? 324 | @enum.find do |tuple| 325 | tuple.expires 326 | end 327 | end 328 | 329 | ## 330 | # Add +tuple+ to the TupleBag. 331 | 332 | def push(tuple) 333 | key = bin_key(tuple) 334 | @hash[key] ||= TupleBin.new 335 | @hash[key].add(tuple) 336 | end 337 | 338 | ## 339 | # Removes +tuple+ from the TupleBag. 340 | 341 | def delete(tuple) 342 | key = bin_key(tuple) 343 | bin = @hash[key] 344 | return nil unless bin 345 | bin.delete(tuple) 346 | @hash.delete(key) if bin.empty? 347 | tuple 348 | end 349 | 350 | ## 351 | # Finds all live tuples that match +template+. 352 | def find_all(template) 353 | bin_for_find(template).find_all do |tuple| 354 | tuple.alive? && template.match(tuple) 355 | end 356 | end 357 | 358 | ## 359 | # Finds a live tuple that matches +template+. 360 | 361 | def find(template) 362 | bin_for_find(template).find do |tuple| 363 | tuple.alive? && template.match(tuple) 364 | end 365 | end 366 | 367 | ## 368 | # Finds all tuples in the TupleBag which when treated as templates, match 369 | # +tuple+ and are alive. 370 | 371 | def find_all_template(tuple) 372 | @enum.find_all do |template| 373 | template.alive? && template.match(tuple) 374 | end 375 | end 376 | 377 | ## 378 | # Delete tuples which dead tuples from the TupleBag, returning the deleted 379 | # tuples. 380 | 381 | def delete_unless_alive 382 | deleted = [] 383 | @hash.each do |key, bin| 384 | bin.delete_if do |tuple| 385 | if tuple.alive? 386 | false 387 | else 388 | deleted.push(tuple) 389 | true 390 | end 391 | end 392 | end 393 | deleted 394 | end 395 | 396 | private 397 | def each_entry(&blk) 398 | @hash.each do |k, v| 399 | v.each(&blk) 400 | end 401 | end 402 | 403 | def bin_key(tuple) 404 | head = tuple[0] 405 | if head.class == Symbol 406 | return head 407 | else 408 | false 409 | end 410 | end 411 | 412 | def bin_for_find(template) 413 | key = bin_key(template) 414 | key ? @hash.fetch(key, []) : @enum 415 | end 416 | end 417 | 418 | ## 419 | # The Tuplespace manages access to the tuples it contains, 420 | # ensuring mutual exclusion requirements are met. 421 | # 422 | # The +sec+ option for the write, take, move, read and notify methods may 423 | # either be a number of seconds or a Renewer object. 424 | 425 | class TupleSpace 426 | 427 | include DRbUndumped 428 | include MonitorMixin 429 | 430 | ## 431 | # Creates a new TupleSpace. +period+ is used to control how often to look 432 | # for dead tuples after modifications to the TupleSpace. 433 | # 434 | # If no dead tuples are found +period+ seconds after the last 435 | # modification, the TupleSpace will stop looking for dead tuples. 436 | 437 | def initialize(period=60) 438 | super() 439 | @bag = TupleBag.new 440 | @read_waiter = TupleBag.new 441 | @take_waiter = TupleBag.new 442 | @notify_waiter = TupleBag.new 443 | @period = period 444 | @keeper = nil 445 | end 446 | 447 | ## 448 | # Adds +tuple+ 449 | 450 | def write(tuple, sec=nil) 451 | entry = create_entry(tuple, sec) 452 | synchronize do 453 | if entry.expired? 454 | @read_waiter.find_all_template(entry).each do |template| 455 | template.read(tuple) 456 | end 457 | notify_event('write', entry.value) 458 | notify_event('delete', entry.value) 459 | else 460 | @bag.push(entry) 461 | start_keeper if entry.expires 462 | @read_waiter.find_all_template(entry).each do |template| 463 | template.read(tuple) 464 | end 465 | @take_waiter.find_all_template(entry).each do |template| 466 | template.signal 467 | end 468 | notify_event('write', entry.value) 469 | end 470 | end 471 | entry 472 | end 473 | 474 | ## 475 | # Removes +tuple+ 476 | 477 | def take(tuple, sec=nil, &block) 478 | move(nil, tuple, sec, &block) 479 | end 480 | 481 | ## 482 | # Moves +tuple+ to +port+. 483 | 484 | def move(port, tuple, sec=nil) 485 | template = WaitTemplateEntry.new(self, tuple, sec) 486 | yield(template) if block_given? 487 | synchronize do 488 | entry = @bag.find(template) 489 | if entry 490 | port.push(entry.value) if port 491 | @bag.delete(entry) 492 | notify_event('take', entry.value) 493 | return port ? nil : entry.value 494 | end 495 | raise RequestExpiredError if template.expired? 496 | 497 | begin 498 | @take_waiter.push(template) 499 | start_keeper if template.expires 500 | while true 501 | raise RequestCanceledError if template.canceled? 502 | raise RequestExpiredError if template.expired? 503 | entry = @bag.find(template) 504 | if entry 505 | port.push(entry.value) if port 506 | @bag.delete(entry) 507 | notify_event('take', entry.value) 508 | return port ? nil : entry.value 509 | end 510 | template.wait 511 | end 512 | ensure 513 | @take_waiter.delete(template) 514 | end 515 | end 516 | end 517 | 518 | ## 519 | # Reads +tuple+, but does not remove it. 520 | 521 | def read(tuple, sec=nil) 522 | template = WaitTemplateEntry.new(self, tuple, sec) 523 | yield(template) if block_given? 524 | synchronize do 525 | entry = @bag.find(template) 526 | return entry.value if entry 527 | raise RequestExpiredError if template.expired? 528 | 529 | begin 530 | @read_waiter.push(template) 531 | start_keeper if template.expires 532 | template.wait 533 | raise RequestCanceledError if template.canceled? 534 | raise RequestExpiredError if template.expired? 535 | return template.found 536 | ensure 537 | @read_waiter.delete(template) 538 | end 539 | end 540 | end 541 | 542 | ## 543 | # Returns all tuples matching +tuple+. Does not remove the found tuples. 544 | 545 | def read_all(tuple) 546 | template = WaitTemplateEntry.new(self, tuple, nil) 547 | synchronize do 548 | entry = @bag.find_all(template) 549 | entry.collect do |e| 550 | e.value 551 | end 552 | end 553 | end 554 | 555 | ## 556 | # Registers for notifications of +event+. Returns a NotifyTemplateEntry. 557 | # See NotifyTemplateEntry for examples of how to listen for notifications. 558 | # 559 | # +event+ can be: 560 | # 'write':: A tuple was added 561 | # 'take':: A tuple was taken or moved 562 | # 'delete':: A tuple was lost after being overwritten or expiring 563 | # 564 | # The TupleSpace will also notify you of the 'close' event when the 565 | # NotifyTemplateEntry has expired. 566 | 567 | def notify(event, tuple, sec=nil) 568 | template = NotifyTemplateEntry.new(self, event, tuple, sec) 569 | synchronize do 570 | @notify_waiter.push(template) 571 | end 572 | template 573 | end 574 | 575 | private 576 | 577 | def create_entry(tuple, sec) 578 | TupleEntry.new(tuple, sec) 579 | end 580 | 581 | ## 582 | # Removes dead tuples. 583 | 584 | def keep_clean 585 | synchronize do 586 | @read_waiter.delete_unless_alive.each do |e| 587 | e.signal 588 | end 589 | @take_waiter.delete_unless_alive.each do |e| 590 | e.signal 591 | end 592 | @notify_waiter.delete_unless_alive.each do |e| 593 | e.notify(['close']) 594 | end 595 | @bag.delete_unless_alive.each do |e| 596 | notify_event('delete', e.value) 597 | end 598 | end 599 | end 600 | 601 | ## 602 | # Notifies all registered listeners for +event+ of a status change of 603 | # +tuple+. 604 | 605 | def notify_event(event, tuple) 606 | ev = [event, tuple] 607 | @notify_waiter.find_all_template(ev).each do |template| 608 | template.notify(ev) 609 | end 610 | end 611 | 612 | ## 613 | # Creates a thread that scans the tuplespace for expired tuples. 614 | 615 | def start_keeper 616 | return if @keeper && @keeper.alive? 617 | @keeper = Thread.new do 618 | while true 619 | sleep(@period) 620 | synchronize do 621 | break unless need_keeper? 622 | keep_clean 623 | end 624 | end 625 | end 626 | end 627 | 628 | ## 629 | # Checks the tuplespace to see if it needs cleaning. 630 | 631 | def need_keeper? 632 | return true if @bag.has_expires? 633 | return true if @read_waiter.has_expires? 634 | return true if @take_waiter.has_expires? 635 | return true if @notify_waiter.has_expires? 636 | end 637 | 638 | end 639 | 640 | end 641 | 642 | -------------------------------------------------------------------------------- /rinda.gemspec: -------------------------------------------------------------------------------- 1 | name = File.basename(__FILE__, ".gemspec") 2 | version = ["lib/rinda", "."].find do |dir| 3 | break File.foreach(File.join(__dir__, dir, "#{name}.rb")) do |line| 4 | /^\s*VERSION\s*=\s*"(.*)"/ =~ line and break $1 5 | end rescue nil 6 | end 7 | 8 | Gem::Specification.new do |spec| 9 | spec.name = name 10 | spec.version = version 11 | spec.authors = ["Masatoshi SEKI"] 12 | spec.email = ["seki@ruby-lang.org"] 13 | 14 | spec.summary = %q{The Linda distributed computing paradigm in Ruby.} 15 | spec.description = %q{The Linda distributed computing paradigm in Ruby.} 16 | spec.homepage = "https://github.com/ruby/rinda" 17 | spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0") 18 | spec.licenses = ["Ruby", "BSD-2-Clause"] 19 | 20 | spec.metadata["homepage_uri"] = spec.homepage 21 | spec.metadata["source_code_uri"] = spec.homepage 22 | 23 | # Specify which files should be added to the gem when it is released. 24 | # The `git ls-files -z` loads the files in the RubyGem that have been added into git. 25 | spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do 26 | `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 27 | end 28 | spec.require_paths = ["lib"] 29 | 30 | spec.add_dependency "drb" 31 | spec.add_dependency "ipaddr" 32 | spec.add_dependency "forwardable" 33 | end 34 | -------------------------------------------------------------------------------- /test/rinda/test_rinda.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: false 2 | require 'test/unit' 3 | require 'envutil' 4 | 5 | require 'drb/drb' 6 | require 'drb/eq' 7 | require 'rinda/ring' 8 | require 'rinda/tuplespace' 9 | require 'timeout' 10 | require 'singleton' 11 | 12 | module Rinda 13 | 14 | class MockClock 15 | include Singleton 16 | 17 | class MyTS < Rinda::TupleSpace 18 | def keeper_thread 19 | nil 20 | end 21 | 22 | def stop_keeper 23 | if @keeper 24 | @keeper.kill 25 | @keeper.join 26 | @keeper = nil 27 | end 28 | end 29 | end 30 | 31 | def initialize 32 | @now = 2 33 | @reso = 1 34 | @ts = nil 35 | @inf = 2**31 - 1 36 | end 37 | 38 | def start_keeper 39 | @now = 2 40 | @reso = 1 41 | @ts&.stop_keeper 42 | @ts = MyTS.new 43 | @ts.write([2, :now]) 44 | @inf = 2**31 - 1 45 | end 46 | 47 | def stop_keeper 48 | @ts.stop_keeper 49 | end 50 | 51 | def now 52 | @now.to_f 53 | end 54 | 55 | def at(n) 56 | n 57 | end 58 | 59 | def _forward(n=nil) 60 | now ,= @ts.take([nil, :now]) 61 | @now = now + n 62 | @ts.write([@now, :now]) 63 | end 64 | 65 | def forward(n) 66 | while n > 0 67 | _forward(@reso) 68 | n -= @reso 69 | Thread.pass 70 | end 71 | end 72 | 73 | def rewind 74 | @ts.take([nil, :now]) 75 | @ts.write([@inf, :now]) 76 | @ts.take([nil, :now]) 77 | @now = 2 78 | @ts.write([2, :now]) 79 | end 80 | 81 | def sleep(n=nil) 82 | now ,= @ts.read([nil, :now]) 83 | @ts.read([(now + n)..@inf, :now]) 84 | 0 85 | end 86 | end 87 | 88 | module Time 89 | def sleep(n) 90 | @m.sleep(n) 91 | end 92 | module_function :sleep 93 | 94 | def at(n) 95 | n 96 | end 97 | module_function :at 98 | 99 | def now 100 | defined?(@m) && @m ? @m.now : 2 101 | end 102 | module_function :now 103 | 104 | def rewind 105 | @m.rewind 106 | end 107 | module_function :rewind 108 | 109 | def forward(n) 110 | @m.forward(n) 111 | end 112 | module_function :forward 113 | 114 | @m = MockClock.instance 115 | end 116 | 117 | class TupleSpace 118 | def sleep(n) 119 | Kernel.sleep(n * 0.01) 120 | end 121 | end 122 | 123 | module TupleSpaceTestModule 124 | def setup 125 | MockClock.instance.start_keeper 126 | end 127 | 128 | def teardown 129 | MockClock.instance.stop_keeper 130 | end 131 | 132 | def sleep(n) 133 | if Thread.current == Thread.main 134 | Time.forward(n) 135 | else 136 | Time.sleep(n) 137 | end 138 | end 139 | 140 | def thread_join(th) 141 | while th.alive? 142 | Kernel.sleep(0.1) 143 | sleep(1) 144 | end 145 | th.value 146 | end 147 | 148 | def test_00_tuple 149 | tuple = Rinda::TupleEntry.new([1,2,3]) 150 | assert(!tuple.canceled?) 151 | assert(!tuple.expired?) 152 | assert(tuple.alive?) 153 | end 154 | 155 | def test_00_template 156 | tmpl = Rinda::Template.new([1,2,3]) 157 | assert_equal(3, tmpl.size) 158 | assert_equal(3, tmpl[2]) 159 | assert(tmpl.match([1,2,3])) 160 | assert(!tmpl.match([1,nil,3])) 161 | 162 | tmpl = Rinda::Template.new([/^rinda/i, nil, :hello]) 163 | assert_equal(3, tmpl.size) 164 | assert(tmpl.match(['Rinda', 2, :hello])) 165 | assert(!tmpl.match(['Rinda', 2, Symbol])) 166 | assert(!tmpl.match([1, 2, :hello])) 167 | assert(tmpl.match([/^rinda/i, 2, :hello])) 168 | 169 | tmpl = Rinda::Template.new([Symbol]) 170 | assert_equal(1, tmpl.size) 171 | assert(tmpl.match([:hello])) 172 | assert(tmpl.match([Symbol])) 173 | assert(!tmpl.match(['Symbol'])) 174 | 175 | tmpl = Rinda::Template.new({"message"=>String, "name"=>String}) 176 | assert_equal(2, tmpl.size) 177 | assert(tmpl.match({"message"=>"Hello", "name"=>"Foo"})) 178 | assert(!tmpl.match({"message"=>"Hello", "name"=>"Foo", "1"=>2})) 179 | assert(!tmpl.match({"message"=>"Hi", "name"=>"Foo", "age"=>1})) 180 | assert(!tmpl.match({"message"=>"Hello", "no_name"=>"Foo"})) 181 | 182 | assert_raise(Rinda::InvalidHashTupleKey) do 183 | Rinda::Template.new({:message=>String, "name"=>String}) 184 | end 185 | tmpl = Rinda::Template.new({"name"=>String}) 186 | assert_equal(1, tmpl.size) 187 | assert(tmpl.match({"name"=>"Foo"})) 188 | assert(!tmpl.match({"message"=>"Hello", "name"=>"Foo"})) 189 | assert(!tmpl.match({"message"=>:symbol, "name"=>"Foo", "1"=>2})) 190 | assert(!tmpl.match({"message"=>"Hi", "name"=>"Foo", "age"=>1})) 191 | assert(!tmpl.match({"message"=>"Hello", "no_name"=>"Foo"})) 192 | 193 | tmpl = Rinda::Template.new({"message"=>String, "name"=>String}) 194 | assert_equal(2, tmpl.size) 195 | assert(tmpl.match({"message"=>"Hello", "name"=>"Foo"})) 196 | assert(!tmpl.match({"message"=>"Hello", "name"=>"Foo", "1"=>2})) 197 | assert(!tmpl.match({"message"=>"Hi", "name"=>"Foo", "age"=>1})) 198 | assert(!tmpl.match({"message"=>"Hello", "no_name"=>"Foo"})) 199 | 200 | tmpl = Rinda::Template.new({"message"=>String}) 201 | assert_equal(1, tmpl.size) 202 | assert(tmpl.match({"message"=>"Hello"})) 203 | assert(!tmpl.match({"message"=>"Hello", "name"=>"Foo"})) 204 | assert(!tmpl.match({"message"=>"Hello", "name"=>"Foo", "1"=>2})) 205 | assert(!tmpl.match({"message"=>"Hi", "name"=>"Foo", "age"=>1})) 206 | assert(!tmpl.match({"message"=>"Hello", "no_name"=>"Foo"})) 207 | 208 | tmpl = Rinda::Template.new({"message"=>String, "name"=>nil}) 209 | assert_equal(2, tmpl.size) 210 | assert(tmpl.match({"message"=>"Hello", "name"=>"Foo"})) 211 | assert(!tmpl.match({"message"=>"Hello", "name"=>"Foo", "1"=>2})) 212 | assert(!tmpl.match({"message"=>"Hi", "name"=>"Foo", "age"=>1})) 213 | assert(!tmpl.match({"message"=>"Hello", "no_name"=>"Foo"})) 214 | 215 | assert_raise(Rinda::InvalidHashTupleKey) do 216 | @ts.write({:message=>String, "name"=>String}) 217 | end 218 | 219 | @ts.write([1, 2, 3]) 220 | assert_equal([1, 2, 3], @ts.take([1, 2, 3])) 221 | 222 | @ts.write({'1'=>1, '2'=>2, '3'=>3}) 223 | assert_equal({'1'=>1, '2'=>2, '3'=>3}, @ts.take({'1'=>1, '2'=>2, '3'=>3})) 224 | 225 | entry = @ts.write(['1'=>1, '2'=>2, '3'=>3]) 226 | assert_raise(Rinda::RequestExpiredError) do 227 | assert_equal({'1'=>1, '2'=>2, '3'=>3}, @ts.read({'1'=>1}, 0)) 228 | end 229 | entry.cancel 230 | end 231 | 232 | def test_00_DRbObject 233 | ro = DRbObject.new(nil, "druby://host:1234") 234 | tmpl = Rinda::DRbObjectTemplate.new 235 | assert(tmpl === ro) 236 | 237 | tmpl = Rinda::DRbObjectTemplate.new("druby://host:1234") 238 | assert(tmpl === ro) 239 | 240 | tmpl = Rinda::DRbObjectTemplate.new("druby://host:12345") 241 | assert(!(tmpl === ro)) 242 | 243 | tmpl = Rinda::DRbObjectTemplate.new(/^druby:\/\/host:/) 244 | assert(tmpl === ro) 245 | 246 | ro = DRbObject.new_with(12345, 1234) 247 | assert(!(tmpl === ro)) 248 | 249 | ro = DRbObject.new_with("druby://foo:12345", 1234) 250 | assert(!(tmpl === ro)) 251 | 252 | tmpl = Rinda::DRbObjectTemplate.new(/^druby:\/\/(foo|bar):/) 253 | assert(tmpl === ro) 254 | 255 | ro = DRbObject.new_with("druby://bar:12345", 1234) 256 | assert(tmpl === ro) 257 | 258 | ro = DRbObject.new_with("druby://baz:12345", 1234) 259 | assert(!(tmpl === ro)) 260 | end 261 | 262 | def test_inp_rdp 263 | assert_raise(Rinda::RequestExpiredError) do 264 | @ts.take([:empty], 0) 265 | end 266 | 267 | assert_raise(Rinda::RequestExpiredError) do 268 | @ts.read([:empty], 0) 269 | end 270 | end 271 | 272 | def test_ruby_talk_264062 273 | th = Thread.new { 274 | assert_raise(Rinda::RequestExpiredError) do 275 | @ts.take([:empty], 1) 276 | end 277 | } 278 | sleep(10) 279 | thread_join(th) 280 | 281 | th = Thread.new { 282 | assert_raise(Rinda::RequestExpiredError) do 283 | @ts.read([:empty], 1) 284 | end 285 | } 286 | sleep(10) 287 | thread_join(th) 288 | end 289 | 290 | def test_symbol_tuple 291 | @ts.write([:symbol, :symbol]) 292 | @ts.write(['string', :string]) 293 | assert_equal([[:symbol, :symbol]], @ts.read_all([:symbol, nil])) 294 | assert_equal([[:symbol, :symbol]], @ts.read_all([Symbol, nil])) 295 | assert_equal([], @ts.read_all([:nil, nil])) 296 | end 297 | 298 | def test_core_01 299 | 5.times do 300 | @ts.write([:req, 2]) 301 | end 302 | 303 | assert_equal([[:req, 2], [:req, 2], [:req, 2], [:req, 2], [:req, 2]], 304 | @ts.read_all([nil, nil])) 305 | 306 | taker = Thread.new(5) do |count| 307 | s = 0 308 | count.times do 309 | tuple = @ts.take([:req, Integer]) 310 | assert_equal(2, tuple[1]) 311 | s += tuple[1] 312 | end 313 | @ts.write([:ans, s]) 314 | s 315 | end 316 | 317 | assert_equal(10, thread_join(taker)) 318 | assert_equal([:ans, 10], @ts.take([:ans, 10])) 319 | assert_equal([], @ts.read_all([nil, nil])) 320 | end 321 | 322 | def test_core_02 323 | taker = Thread.new(5) do |count| 324 | s = 0 325 | count.times do 326 | tuple = @ts.take([:req, Integer]) 327 | assert_equal(2, tuple[1]) 328 | s += tuple[1] 329 | end 330 | @ts.write([:ans, s]) 331 | s 332 | end 333 | 334 | 5.times do 335 | @ts.write([:req, 2]) 336 | end 337 | 338 | assert_equal(10, thread_join(taker)) 339 | assert_equal([:ans, 10], @ts.take([:ans, 10])) 340 | assert_equal([], @ts.read_all([nil, nil])) 341 | end 342 | 343 | def test_core_03_notify 344 | notify1 = @ts.notify(nil, [:req, Integer]) 345 | notify2 = @ts.notify(nil, {"message"=>String, "name"=>String}) 346 | 347 | 5.times do 348 | @ts.write([:req, 2]) 349 | end 350 | 351 | 5.times do 352 | tuple = @ts.take([:req, Integer]) 353 | assert_equal(2, tuple[1]) 354 | end 355 | 356 | 5.times do 357 | assert_equal(['write', [:req, 2]], notify1.pop) 358 | end 359 | 5.times do 360 | assert_equal(['take', [:req, 2]], notify1.pop) 361 | end 362 | 363 | @ts.write({"message"=>"first", "name"=>"3"}) 364 | @ts.write({"message"=>"second", "name"=>"1"}) 365 | @ts.write({"message"=>"third", "name"=>"0"}) 366 | @ts.take({"message"=>"third", "name"=>"0"}) 367 | @ts.take({"message"=>"first", "name"=>"3"}) 368 | 369 | assert_equal(["write", {"message"=>"first", "name"=>"3"}], notify2.pop) 370 | assert_equal(["write", {"message"=>"second", "name"=>"1"}], notify2.pop) 371 | assert_equal(["write", {"message"=>"third", "name"=>"0"}], notify2.pop) 372 | assert_equal(["take", {"message"=>"third", "name"=>"0"}], notify2.pop) 373 | assert_equal(["take", {"message"=>"first", "name"=>"3"}], notify2.pop) 374 | end 375 | 376 | def test_cancel_01 377 | entry = @ts.write([:removeme, 1]) 378 | assert_equal([[:removeme, 1]], @ts.read_all([nil, nil])) 379 | entry.cancel 380 | assert_equal([], @ts.read_all([nil, nil])) 381 | 382 | template = nil 383 | taker = Thread.new do 384 | assert_raise(Rinda::RequestCanceledError) do 385 | @ts.take([:take, nil], 10) do |t| 386 | template = t 387 | Thread.new do 388 | template.cancel 389 | end 390 | end 391 | end 392 | end 393 | 394 | sleep(2) 395 | thread_join(taker) 396 | 397 | assert(template.canceled?) 398 | 399 | @ts.write([:take, 1]) 400 | 401 | assert_equal([[:take, 1]], @ts.read_all([nil, nil])) 402 | end 403 | 404 | def test_cancel_02 405 | omit 'this test is unstable with --jit-wait' if defined?(RubyVM::RJIT) && RubyVM::RJIT.enabled? 406 | entry = @ts.write([:removeme, 1]) 407 | assert_equal([[:removeme, 1]], @ts.read_all([nil, nil])) 408 | entry.cancel 409 | assert_equal([], @ts.read_all([nil, nil])) 410 | 411 | template = nil 412 | reader = Thread.new do 413 | assert_raise(Rinda::RequestCanceledError) do 414 | @ts.read([:take, nil], 10) do |t| 415 | template = t 416 | Thread.new do 417 | template.cancel 418 | end 419 | end 420 | end 421 | end 422 | 423 | sleep(2) 424 | thread_join(reader) 425 | 426 | assert(template.canceled?) 427 | 428 | @ts.write([:take, 1]) 429 | 430 | assert_equal([[:take, 1]], @ts.read_all([nil, nil])) 431 | end 432 | 433 | class SimpleRenewer 434 | def initialize(sec, n = 1) 435 | @sec = sec 436 | @n = n 437 | end 438 | 439 | def renew 440 | return -1 if @n <= 0 441 | @n -= 1 442 | return @sec 443 | end 444 | end 445 | 446 | def test_00_renewer 447 | tuple = Rinda::TupleEntry.new([1,2,3], true) 448 | assert(!tuple.canceled?) 449 | assert(tuple.expired?) 450 | assert(!tuple.alive?) 451 | 452 | tuple = Rinda::TupleEntry.new([1,2,3], 1) 453 | assert(!tuple.canceled?) 454 | assert(!tuple.expired?) 455 | assert(tuple.alive?) 456 | sleep(2) 457 | assert(tuple.expired?) 458 | assert(!tuple.alive?) 459 | 460 | @renewer = SimpleRenewer.new(1,2) 461 | tuple = Rinda::TupleEntry.new([1,2,3], @renewer) 462 | assert(!tuple.canceled?) 463 | assert(!tuple.expired?) 464 | assert(tuple.alive?) 465 | sleep(1) 466 | assert(!tuple.canceled?) 467 | assert(!tuple.expired?) 468 | assert(tuple.alive?) 469 | sleep(2) 470 | assert(tuple.expired?) 471 | assert(!tuple.alive?) 472 | end 473 | end 474 | 475 | class TupleSpaceTest < Test::Unit::TestCase 476 | include TupleSpaceTestModule 477 | 478 | def setup 479 | super 480 | ThreadGroup.new.add(Thread.current) 481 | @ts = Rinda::TupleSpace.new(1) 482 | end 483 | def teardown 484 | # implementation-dependent 485 | @ts.instance_eval{ 486 | if th = @keeper 487 | th.kill 488 | th.join 489 | end 490 | } 491 | super 492 | end 493 | end 494 | 495 | class TupleSpaceProxyTest < Test::Unit::TestCase 496 | include TupleSpaceTestModule 497 | 498 | def setup 499 | if RUBY_PLATFORM.match?(/mingw/) 500 | @omitted = true 501 | omit 'This test seems to randomly hang on GitHub Actions MinGW' 502 | end 503 | super 504 | ThreadGroup.new.add(Thread.current) 505 | @ts_base = Rinda::TupleSpace.new(1) 506 | @ts = Rinda::TupleSpaceProxy.new(@ts_base) 507 | @server = DRb.start_service("druby://localhost:0") 508 | end 509 | def teardown 510 | return if @omitted 511 | @omitted = false 512 | 513 | # implementation-dependent 514 | @ts_base.instance_eval{ 515 | if th = @keeper 516 | th.kill 517 | th.join 518 | end 519 | } 520 | @server.stop_service 521 | DRb::DRbConn.stop_pool 522 | super 523 | end 524 | 525 | def test_remote_array_and_hash 526 | # Don't remove ary/hsh local variables. 527 | # These are necessary to protect objects from GC. 528 | ary = [1, 2, 3] 529 | @ts.write(DRbObject.new(ary)) 530 | assert_equal([1, 2, 3], @ts.take([1, 2, 3], 0)) 531 | hsh = {'head' => 1, 'tail' => 2} 532 | @ts.write(DRbObject.new(hsh)) 533 | assert_equal({'head' => 1, 'tail' => 2}, 534 | @ts.take({'head' => 1, 'tail' => 2}, 0)) 535 | end 536 | 537 | def test_take_bug_8215 538 | omit "this test randomly fails on mswin" if /mswin/ =~ RUBY_PLATFORM 539 | service = DRb.start_service("druby://localhost:0", @ts_base) 540 | 541 | uri = service.uri 542 | 543 | args = [EnvUtil.rubybin, *%W[-rdrb/drb -rdrb/eq -rrinda/ring -rrinda/tuplespace -e]] 544 | 545 | take = spawn(*args, <<-'end;', uri) 546 | uri = ARGV[0] 547 | DRb.start_service("druby://localhost:0") 548 | ro = DRbObject.new_with_uri(uri) 549 | ts = Rinda::TupleSpaceProxy.new(ro) 550 | th = Thread.new do 551 | ts.take([:test_take, nil]) 552 | rescue Interrupt 553 | # Expected 554 | end 555 | Kernel.sleep(0.1) 556 | th.raise(Interrupt) # causes loss of the taken tuple 557 | ts.write([:barrier, :continue]) 558 | Kernel.sleep 559 | end; 560 | 561 | @ts_base.take([:barrier, :continue]) 562 | 563 | write = spawn(*args, <<-'end;', uri) 564 | uri = ARGV[0] 565 | DRb.start_service("druby://localhost:0") 566 | ro = DRbObject.new_with_uri(uri) 567 | ts = Rinda::TupleSpaceProxy.new(ro) 568 | ts.write([:test_take, 42]) 569 | end; 570 | 571 | status = Process.wait(write) 572 | 573 | assert_equal([[:test_take, 42]], @ts_base.read_all([:test_take, nil]), 574 | '[bug:8215] tuple lost') 575 | ensure 576 | service.stop_service if service 577 | DRb::DRbConn.stop_pool 578 | signal = /mswin|mingw/ =~ RUBY_PLATFORM ? "KILL" : "TERM" 579 | Process.kill(signal, write) if write && status.nil? 580 | Process.kill(signal, take) if take 581 | Process.wait(write) if write && status.nil? 582 | Process.wait(take) if take 583 | end 584 | end 585 | 586 | module RingIPv4 587 | def ipv4_mc(rf) 588 | begin 589 | v4mc = rf.make_socket('239.0.0.1') 590 | rescue Errno::ENETUNREACH, Errno::ENOBUFS, Errno::ENODEV 591 | omit 'IPv4 multicast not available' 592 | end 593 | 594 | begin 595 | yield v4mc 596 | ensure 597 | v4mc.close 598 | end 599 | end 600 | end 601 | 602 | module RingIPv6 603 | def prepare_ipv6(r) 604 | begin 605 | Socket.getifaddrs.each do |ifaddr| 606 | next unless ifaddr.addr 607 | next unless ifaddr.addr.ipv6_linklocal? 608 | next if ifaddr.name[0, 2] == "lo" 609 | r.multicast_interface = ifaddr.ifindex 610 | return ifaddr 611 | end 612 | rescue NotImplementedError 613 | # ifindex() function may not be implemented on Windows. 614 | return if 615 | Socket.ip_address_list.any? { |addrinfo| addrinfo.ipv6? && !addrinfo.ipv6_loopback? } 616 | end 617 | omit 'IPv6 not available' 618 | end 619 | 620 | def ipv6_mc(rf, hops = nil) 621 | ifaddr = prepare_ipv6(rf) 622 | rf.multicast_hops = hops if hops 623 | begin 624 | v6mc = rf.make_socket("ff02::1") 625 | rescue Errno::EINVAL 626 | # somehow Debian 6.0.7 needs ifname 627 | v6mc = rf.make_socket("ff02::1%#{ifaddr.name}") 628 | rescue Errno::EADDRNOTAVAIL 629 | return # IPv6 address for multicast not available 630 | rescue Errno::ENETDOWN 631 | return # Network is down 632 | rescue Errno::EHOSTUNREACH 633 | return # Unreachable for some reason 634 | end 635 | begin 636 | yield v6mc 637 | ensure 638 | v6mc.close 639 | end 640 | end 641 | end 642 | 643 | class TestRingServer < Test::Unit::TestCase 644 | include RingIPv4 645 | 646 | def setup 647 | @port = Rinda::Ring_PORT 648 | 649 | @ts = Rinda::TupleSpace.new 650 | @rs = Rinda::RingServer.new(@ts, [], @port) 651 | @server = DRb.start_service("druby://localhost:0") 652 | end 653 | def teardown 654 | @rs.shutdown 655 | # implementation-dependent 656 | @ts.instance_eval{ 657 | if th = @keeper 658 | th.kill 659 | th.join 660 | end 661 | } 662 | @server.stop_service 663 | DRb::DRbConn.stop_pool 664 | end 665 | 666 | def test_do_reply 667 | with_timeout(30) {_test_do_reply} 668 | end 669 | 670 | def _test_do_reply 671 | called = nil 672 | 673 | callback_orig = proc { |ts| 674 | called = ts 675 | } 676 | 677 | callback = DRb::DRbObject.new callback_orig 678 | 679 | @ts.write [:lookup_ring, callback] 680 | 681 | @rs.do_reply 682 | 683 | wait_for(30) {called} 684 | 685 | assert_same @ts, called 686 | end 687 | 688 | def test_do_reply_local 689 | omit 'timeout-based test becomes unstable with --jit-wait' if defined?(RubyVM::RJIT) && RubyVM::RJIT.enabled? 690 | with_timeout(30) {_test_do_reply_local} 691 | end 692 | 693 | def _test_do_reply_local 694 | called = nil 695 | 696 | callback = proc { |ts| 697 | called = ts 698 | } 699 | 700 | @ts.write [:lookup_ring, callback] 701 | 702 | @rs.do_reply 703 | 704 | wait_for(30) {called} 705 | 706 | assert_same @ts, called 707 | end 708 | 709 | def test_make_socket_unicast 710 | v4 = @rs.make_socket('127.0.0.1') 711 | 712 | assert_equal('127.0.0.1', v4.local_address.ip_address) 713 | assert_equal(@port, v4.local_address.ip_port) 714 | end 715 | 716 | def test_make_socket_ipv4_multicast 717 | ipv4_mc(@rs) do |v4mc| 718 | begin 719 | if Socket.const_defined?(:SO_REUSEPORT) then 720 | assert(v4mc.getsockopt(:SOCKET, :SO_REUSEPORT).bool) 721 | else 722 | assert(v4mc.getsockopt(:SOCKET, :SO_REUSEADDR).bool) 723 | end 724 | rescue TypeError 725 | if /aix/ =~ RUBY_PLATFORM 726 | omit "Known bug in getsockopt(2) on AIX" 727 | end 728 | raise $! 729 | end 730 | 731 | assert_equal('0.0.0.0', v4mc.local_address.ip_address) 732 | assert_equal(@port, v4mc.local_address.ip_port) 733 | end 734 | end 735 | 736 | def test_make_socket_ipv6_multicast 737 | omit 'IPv6 not available' unless 738 | Socket.ip_address_list.any? { |addrinfo| addrinfo.ipv6? && !addrinfo.ipv6_loopback? } 739 | 740 | begin 741 | v6mc = @rs.make_socket('ff02::1') 742 | rescue Errno::EADDRNOTAVAIL 743 | return # IPv6 address for multicast not available 744 | rescue Errno::ENOBUFS => e 745 | omit "Missing multicast support in OS: #{e.message}" 746 | end 747 | 748 | if Socket.const_defined?(:SO_REUSEPORT) then 749 | assert v6mc.getsockopt(:SOCKET, :SO_REUSEPORT).bool 750 | else 751 | assert v6mc.getsockopt(:SOCKET, :SO_REUSEADDR).bool 752 | end 753 | 754 | assert_equal('::1', v6mc.local_address.ip_address) 755 | assert_equal(@port, v6mc.local_address.ip_port) 756 | end 757 | 758 | def test_ring_server_ipv4_multicast 759 | @rs.shutdown 760 | begin 761 | @rs = Rinda::RingServer.new(@ts, [['239.0.0.1', '0.0.0.0']], @port) 762 | rescue Errno::ENOBUFS, Errno::ENODEV => e 763 | omit "Missing multicast support in OS: #{e.message}" 764 | end 765 | 766 | v4mc = @rs.instance_variable_get('@sockets').first 767 | 768 | begin 769 | if Socket.const_defined?(:SO_REUSEPORT) then 770 | assert(v4mc.getsockopt(:SOCKET, :SO_REUSEPORT).bool) 771 | else 772 | assert(v4mc.getsockopt(:SOCKET, :SO_REUSEADDR).bool) 773 | end 774 | rescue TypeError 775 | if /aix/ =~ RUBY_PLATFORM 776 | omit "Known bug in getsockopt(2) on AIX" 777 | end 778 | raise $! 779 | end 780 | 781 | assert_equal('0.0.0.0', v4mc.local_address.ip_address) 782 | assert_equal(@port, v4mc.local_address.ip_port) 783 | end 784 | 785 | def test_ring_server_ipv6_multicast 786 | omit 'IPv6 not available' unless 787 | Socket.ip_address_list.any? { |addrinfo| addrinfo.ipv6? && !addrinfo.ipv6_loopback? } 788 | 789 | @rs.shutdown 790 | begin 791 | @rs = Rinda::RingServer.new(@ts, [['ff02::1', '::1', 0]], @port) 792 | rescue Errno::EADDRNOTAVAIL 793 | return # IPv6 address for multicast not available 794 | end 795 | 796 | v6mc = @rs.instance_variable_get('@sockets').first 797 | 798 | if Socket.const_defined?(:SO_REUSEPORT) then 799 | assert v6mc.getsockopt(:SOCKET, :SO_REUSEPORT).bool 800 | else 801 | assert v6mc.getsockopt(:SOCKET, :SO_REUSEADDR).bool 802 | end 803 | 804 | assert_equal('::1', v6mc.local_address.ip_address) 805 | assert_equal(@port, v6mc.local_address.ip_port) 806 | end 807 | 808 | def test_shutdown 809 | @rs.shutdown 810 | 811 | assert_nil(@rs.do_reply, 'otherwise should hang forever') 812 | end 813 | 814 | private 815 | 816 | def with_timeout(n) 817 | aoe = Thread.abort_on_exception 818 | Thread.abort_on_exception = true 819 | tl0 = Thread.list 820 | tl = nil 821 | th = Thread.new(Thread.current) do |mth| 822 | sleep n 823 | (tl = Thread.list - tl0).each {|t|t.raise(Timeout::Error)} 824 | mth.raise(Timeout::Error) 825 | end 826 | tl0 << th 827 | yield 828 | rescue Timeout::Error => e 829 | $stderr.puts "TestRingServer#with_timeout: timeout in #{n}s:" 830 | $stderr.puts caller 831 | if tl 832 | bt = e.backtrace 833 | tl.each do |t| 834 | begin 835 | t.value 836 | rescue Timeout::Error => e 837 | bt.unshift("") 838 | bt[0, 0] = e.backtrace 839 | end 840 | end 841 | end 842 | raise Timeout::Error, "timeout", bt 843 | ensure 844 | if th 845 | th.kill 846 | th.join 847 | end 848 | Thread.abort_on_exception = aoe 849 | end 850 | 851 | def wait_for(n) 852 | t = n + Process.clock_gettime(Process::CLOCK_MONOTONIC, :second) 853 | until yield 854 | if t < Process.clock_gettime(Process::CLOCK_MONOTONIC, :second) 855 | flunk "timeout during waiting call" 856 | end 857 | sleep 0.1 858 | end 859 | end 860 | end 861 | 862 | class TestRingFinger < Test::Unit::TestCase 863 | include RingIPv6 864 | include RingIPv4 865 | 866 | def setup 867 | @rf = Rinda::RingFinger.new 868 | end 869 | 870 | def test_make_socket_unicast 871 | v4 = @rf.make_socket('127.0.0.1') 872 | 873 | assert(v4.getsockopt(:SOL_SOCKET, :SO_BROADCAST).bool) 874 | rescue TypeError 875 | if /aix/ =~ RUBY_PLATFORM 876 | omit "Known bug in getsockopt(2) on AIX" 877 | end 878 | raise $! 879 | ensure 880 | v4.close if v4 881 | end 882 | 883 | def test_make_socket_ipv4_multicast 884 | ipv4_mc(@rf) do |v4mc| 885 | assert_equal(1, v4mc.getsockopt(:IPPROTO_IP, :IP_MULTICAST_LOOP).ipv4_multicast_loop) 886 | assert_equal(1, v4mc.getsockopt(:IPPROTO_IP, :IP_MULTICAST_TTL).ipv4_multicast_ttl) 887 | end 888 | end 889 | 890 | def test_make_socket_ipv6_multicast 891 | ipv6_mc(@rf) do |v6mc| 892 | assert_equal(1, v6mc.getsockopt(:IPPROTO_IPV6, :IPV6_MULTICAST_LOOP).int) 893 | assert_equal(1, v6mc.getsockopt(:IPPROTO_IPV6, :IPV6_MULTICAST_HOPS).int) 894 | end 895 | end 896 | 897 | def test_make_socket_ipv4_multicast_hops 898 | @rf.multicast_hops = 2 899 | ipv4_mc(@rf) do |v4mc| 900 | assert_equal(2, v4mc.getsockopt(:IPPROTO_IP, :IP_MULTICAST_TTL).ipv4_multicast_ttl) 901 | end 902 | end 903 | 904 | def test_make_socket_ipv6_multicast_hops 905 | ipv6_mc(@rf, 2) do |v6mc| 906 | assert_equal(2, v6mc.getsockopt(:IPPROTO_IPV6, :IPV6_MULTICAST_HOPS).int) 907 | end 908 | end 909 | 910 | end 911 | 912 | end 913 | -------------------------------------------------------------------------------- /test/rinda/test_tuplebag.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: false 2 | require 'test/unit' 3 | require 'rinda/tuplespace' 4 | 5 | class TestTupleBag < Test::Unit::TestCase 6 | 7 | def setup 8 | @tb = Rinda::TupleBag.new 9 | end 10 | 11 | def test_delete 12 | assert_nothing_raised do 13 | val = @tb.delete tup(:val, 1) 14 | assert_equal nil, val 15 | end 16 | 17 | t = tup(:val, 1) 18 | @tb.push t 19 | 20 | val = @tb.delete t 21 | 22 | assert_equal t, val 23 | 24 | assert_equal [], @tb.find_all(tem(:val, 1)) 25 | 26 | t1 = tup(:val, 1) 27 | t2 = tup(:val, 1) 28 | @tb.push t1 29 | @tb.push t2 30 | 31 | val = @tb.delete t1 32 | 33 | assert_equal t1, val 34 | 35 | assert_equal [t2], @tb.find_all(tem(:val, 1)) 36 | end 37 | 38 | def test_delete_unless_alive 39 | assert_equal [], @tb.delete_unless_alive 40 | 41 | t1 = tup(:val, nil) 42 | t2 = tup(:val, nil) 43 | 44 | @tb.push t1 45 | @tb.push t2 46 | 47 | assert_equal [], @tb.delete_unless_alive 48 | 49 | t1.cancel 50 | 51 | assert_equal [t1], @tb.delete_unless_alive, 'canceled' 52 | 53 | t2.renew Object.new 54 | 55 | assert_equal [t2], @tb.delete_unless_alive, 'expired' 56 | end 57 | 58 | def test_find 59 | template = tem(:val, nil) 60 | 61 | assert_equal nil, @tb.find(template) 62 | 63 | t1 = tup(:other, 1) 64 | @tb.push t1 65 | 66 | assert_equal nil, @tb.find(template) 67 | 68 | t2 = tup(:val, 1) 69 | @tb.push t2 70 | 71 | assert_equal t2, @tb.find(template) 72 | 73 | t2.cancel 74 | 75 | assert_equal nil, @tb.find(template), 'canceled' 76 | 77 | t3 = tup(:val, 3) 78 | @tb.push t3 79 | 80 | assert_equal t3, @tb.find(template) 81 | 82 | t3.renew Object.new 83 | 84 | assert_equal nil, @tb.find(template), 'expired' 85 | end 86 | 87 | def test_find_all 88 | template = tem(:val, nil) 89 | 90 | t1 = tup(:other, 1) 91 | @tb.push t1 92 | 93 | assert_equal [], @tb.find_all(template) 94 | 95 | t2 = tup(:val, 2) 96 | t3 = tup(:val, 3) 97 | 98 | @tb.push t2 99 | @tb.push t3 100 | 101 | assert_equal [t2, t3], @tb.find_all(template) 102 | 103 | t2.cancel 104 | 105 | assert_equal [t3], @tb.find_all(template), 'canceled' 106 | 107 | t3.renew Object.new 108 | 109 | assert_equal [], @tb.find_all(template), 'expired' 110 | end 111 | 112 | def test_find_all_template 113 | tuple = tup(:val, 1) 114 | 115 | t1 = tem(:other, nil) 116 | @tb.push t1 117 | 118 | assert_equal [], @tb.find_all_template(tuple) 119 | 120 | t2 = tem(:val, nil) 121 | t3 = tem(:val, nil) 122 | 123 | @tb.push t2 124 | @tb.push t3 125 | 126 | assert_equal [t2, t3], @tb.find_all_template(tuple) 127 | 128 | t2.cancel 129 | 130 | assert_equal [t3], @tb.find_all_template(tuple), 'canceled' 131 | 132 | t3.renew Object.new 133 | 134 | assert_equal [], @tb.find_all_template(tuple), 'expired' 135 | end 136 | 137 | def test_has_expires_eh 138 | assert !@tb.has_expires? 139 | 140 | t = tup(:val, 1) 141 | @tb.push t 142 | 143 | assert @tb.has_expires? 144 | 145 | t.renew Object.new 146 | 147 | assert !@tb.has_expires? 148 | end 149 | 150 | def test_push 151 | t = tup(:val, 1) 152 | 153 | @tb.push t 154 | 155 | assert_equal t, @tb.find(tem(:val, 1)) 156 | end 157 | 158 | ## 159 | # Create a tuple with +ary+ for its contents 160 | 161 | def tup(*ary) 162 | Rinda::TupleEntry.new ary 163 | end 164 | 165 | ## 166 | # Create a template with +ary+ for its contents 167 | 168 | def tem(*ary) 169 | Rinda::TemplateEntry.new ary 170 | end 171 | 172 | end 173 | 174 | --------------------------------------------------------------------------------