diff --git a/.rubocop.yml b/.rubocop.yml index 0152dd8..2dfede8 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -2,6 +2,8 @@ AllCops: DisplayCopNames: true TargetRubyVersion: 2.3.3 +Style/ClassAndModuleChildren: + Enabled: false Naming/FileName: Enabled: false Metrics/MethodLength: diff --git a/lib/random-port.rb b/lib/random-port.rb index 1684615..28551b0 100644 --- a/lib/random-port.rb +++ b/lib/random-port.rb @@ -22,4 +22,12 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. +# The module for all classes. +# +# Author:: Yegor Bugayenko (yegor256@gmail.com) +# Copyright:: Copyright (c) 2018 Yegor Bugayenko +# License:: MIT +module RandomPort +end + require 'random-port/pool' diff --git a/lib/random-port/pool.rb b/lib/random-port/pool.rb index 023a3f4..4d4d283 100644 --- a/lib/random-port/pool.rb +++ b/lib/random-port/pool.rb @@ -24,121 +24,120 @@ require 'socket' require 'monitor' +require_relative '../random-port' -module RandomPort - # Pool of TPC ports. - # - # Use it like this: - # - # RandomPort::Pool.new.acquire do |port| - # # Use the TCP port. It will be returned back - # # to the pool afterwards. - # end - # - # You can specify the maximum amount of ports to acquire, using +limit+. - # If more acquiring requests will arrive, an exception will be raised. - # - # The class is thread-safe, by default. You can configure it to be - # not-thread-safe, using optional sync argument of the constructor. - # - # Author:: Yegor Bugayenko (yegor256@gmail.com) - # Copyright:: Copyright (c) 2018 Yegor Bugayenko - # License:: MIT - class Pool - # If can't acquire by time out. - class Timeout < StandardError; end +# Pool of TPC ports. +# +# Use it like this: +# +# RandomPort::Pool.new.acquire do |port| +# # Use the TCP port. It will be returned back +# # to the pool afterwards. +# end +# +# You can specify the maximum amount of ports to acquire, using +limit+. +# If more acquiring requests will arrive, an exception will be raised. +# +# The class is thread-safe, by default. You can configure it to be +# not-thread-safe, using optional sync argument of the constructor. +# +# Author:: Yegor Bugayenko (yegor256@gmail.com) +# Copyright:: Copyright (c) 2018 Yegor Bugayenko +# License:: MIT +class RandomPort::Pool + # If can't acquire by time out. + class Timeout < StandardError; end - attr_reader :limit + attr_reader :limit - # Ctor. - def initialize(sync: false, limit: 65_536) - @ports = [] - @sync = sync - @monitor = Monitor.new - @limit = limit - end + # Ctor. + def initialize(sync: false, limit: 65_536) + @ports = [] + @sync = sync + @monitor = Monitor.new + @limit = limit + end - # Application wide pool of ports - SINGLETON = Pool.new + # Application wide pool of ports + SINGLETON = RandomPort::Pool.new - # How many ports acquired now? - def count - @ports.count - end - alias size count + # How many ports acquired now? + def count + @ports.count + end + alias size count - # Is it empty? - def empty? - @ports.empty? - end + # Is it empty? + def empty? + @ports.empty? + end - # Acquire a new random TCP port. - # - # You can specify the number of ports to acquire. If it's more than one, - # an array will be returned. - # - # You can specify the amount of seconds to wait until a new port - # is available. - def acquire(total = 1, timeout: 4) - start = Time.now - loop do - if Time.now > start + timeout - raise Timeout, "Can't find a place in the pool of #{@limit} ports \ + # Acquire a new random TCP port. + # + # You can specify the number of ports to acquire. If it's more than one, + # an array will be returned. + # + # You can specify the amount of seconds to wait until a new port + # is available. + def acquire(total = 1, timeout: 4) + start = Time.now + loop do + if Time.now > start + timeout + raise Timeout, "Can't find a place in the pool of #{@limit} ports \ for #{total} port(s), in #{format('%.02f', Time.now - start)}s" - end - opts = safe do - next if @ports.count + total > @limit - opts = Array.new(0, total) - begin - (0..(total - 1)).each do |i| - opts[i] = i.zero? ? take : take(opts[i - 1] + 1) - end - rescue Errno::EADDRINUSE, SocketError - next - end - next if opts.any? { |p| @ports.include?(p) } - d = total * (total - 1) / 2 - next unless opts.inject(&:+) - total * opts.min == d - @ports += opts - opts - end - next if opts.nil? - opts = opts[0] if total == 1 - return opts unless block_given? + end + opts = safe do + next if @ports.count + total > @limit + opts = Array.new(0, total) begin - return yield opts - ensure - release(opts) + (0..(total - 1)).each do |i| + opts[i] = i.zero? ? take : take(opts[i - 1] + 1) + end + rescue Errno::EADDRINUSE, SocketError + next end + next if opts.any? { |p| @ports.include?(p) } + d = total * (total - 1) / 2 + next unless opts.inject(&:+) - total * opts.min == d + @ports += opts + opts + end + next if opts.nil? + opts = opts[0] if total == 1 + return opts unless block_given? + begin + return yield opts + ensure + release(opts) end end + end - # Return it/them back to the pool. - def release(port) - safe do - if port.is_a?(Array) - port.each { |p| @ports.delete(p) } - else - @ports.delete(port) - end + # Return it/them back to the pool. + def release(port) + safe do + if port.is_a?(Array) + port.each { |p| @ports.delete(p) } + else + @ports.delete(port) end end + end - private + private - def take(opt = 0) - server = TCPServer.new('127.0.0.1', opt) - p = server.addr[1] - server.close - p - end + def take(opt = 0) + server = TCPServer.new('127.0.0.1', opt) + p = server.addr[1] + server.close + p + end - def safe - if @sync - @monitor.synchronize { yield } - else - yield - end + def safe + if @sync + @monitor.synchronize { yield } + else + yield end end end diff --git a/test/test_pool.rb b/test/test_pool.rb index 0b88e96..c689cc9 100644 --- a/test/test_pool.rb +++ b/test/test_pool.rb @@ -29,82 +29,90 @@ # Author:: Yegor Bugayenko (yegor256@gmail.com) # Copyright:: Copyright (c) 2018 Yegor Bugayenko # License:: MIT -module RandomPort - class TestAmount < Minitest::Test - def test_acquires_and_releases - pool = Pool.new - port = pool.acquire - assert(!port.nil?) - assert(port.positive?) - pool.release(port) - end +class RandomPort::TestPool < Minitest::Test + def test_acquires_and_releases + pool = RandomPort::Pool.new + port = pool.acquire + server = TCPServer.new(port) + server.close + assert(!port.nil?) + assert(port.positive?) + pool.release(port) + end - def test_acquires_and_releases_three_ports - pool = Pool.new(limit: 3) - assert_equal(0, pool.size) - ports = pool.acquire(3, timeout: 16) - assert_equal(3, pool.size) - assert_equal(3, ports.count) - pool.release(ports) - assert_equal(0, pool.size) + def test_acquires_and_releases_three_ports + pool = RandomPort::Pool.new(limit: 3) + assert_equal(0, pool.size) + ports = pool.acquire(3, timeout: 16) + ports.each do |p| + server = TCPServer.new(p) + server.close end + assert_equal(3, pool.size) + assert_equal(3, ports.count) + pool.release(ports) + assert_equal(0, pool.size) + end - def test_acquires_and_releases_three_ports_in_block - pool = Pool.new(limit: 3) - assert_equal(0, pool.size) - pool.acquire(3, timeout: 16) do |ports| - assert(ports.is_a?(Array)) - assert_equal(3, ports.count) - assert_equal(3, pool.size) + def test_acquires_and_releases_three_ports_in_block + pool = RandomPort::Pool.new(limit: 3) + assert_equal(0, pool.size) + pool.acquire(3, timeout: 16) do |ports| + assert(ports.is_a?(Array)) + assert_equal(3, ports.count) + assert_equal(3, pool.size) + ports.each do |p| + server = TCPServer.new(p) + server.close end - assert_equal(0, pool.size) end + assert_equal(0, pool.size) + end - def test_acquires_and_releases_in_block - result = Pool.new.acquire do |port| - assert(!port.nil?) - assert(port.positive?) - 123 - end - assert_equal(123, result) + def test_acquires_and_releases_in_block + result = RandomPort::Pool.new.acquire do |port| + assert(!port.nil?) + assert(port.positive?) + 123 end + assert_equal(123, result) + end - def test_acquires_and_releases_safely - pool = Pool.new - assert_raises do - pool.acquire do - raise 'Itended' - end + def test_acquires_and_releases_safely + pool = RandomPort::Pool.new + assert_raises do + pool.acquire do + raise 'Itended' end - assert(pool.count.zero?) end + assert(pool.count.zero?) + end - def test_acquires_and_releases_from_singleton - Pool::SINGLETON.acquire do |port| - assert(!port.nil?) - assert(port.positive?) - end + def test_acquires_and_releases_from_singleton + RandomPort::Pool::SINGLETON.acquire do |port| + assert(!port.nil?) + assert(port.positive?) end + end - def test_acquires_unique_numbers - total = 25 - numbers = (0..total - 1).map { Pool::SINGLETON.acquire } - assert_equal(total, numbers.uniq.count) - end + def test_acquires_unique_numbers + total = 25 + numbers = (0..total - 1).map { RandomPort::Pool::SINGLETON.acquire } + assert_equal(total, numbers.uniq.count) + end - def test_raises_when_too_many - pool = Pool.new(limit: 1) - pool.acquire - assert_raises Pool::Timeout do - pool.acquire(timeout: 0.1) - end + def test_raises_when_too_many + pool = RandomPort::Pool.new(limit: 1) + pool.acquire + assert_raises RandomPort::Pool::Timeout do + pool.acquire(timeout: 0.1) end + end - def test_acquires_unique_numbers_in_no_sync_mode - total = 25 - pool = Pool.new(sync: false) - numbers = (0..total - 1).map { pool.acquire } - assert_equal(total, numbers.uniq.count) - end + def test_acquires_unique_numbers_in_no_sync_mode + total = 25 + pool = RandomPort::Pool.new(sync: false) + numbers = (0..total - 1).map { pool.acquire } + assert_equal(total, numbers.uniq.count) end end