fhsjaagshs/access_stack

View on GitHub
lib/access_stack.rb

Summary

Maintainability
A
1 hr
Test Coverage
#!/usr/bin/env ruby

require "threadsafety"
require "timeout"

class AccessStack
  include Threadsafety # makes setters threadsafe + adds threadsafe method
    attr_reader :count,
              :create,
              :destroy,
              :validate
  attr_accessor :pool,
                :checkout_timeout,
                :reaping_frequency,
                :dead_connection_timeout

    TimeoutError = Class.new StandardError
  DestructorError = Class.new StandardError
  CreatorError = Class.new StandardError
  
  NO_DESTRUCTOR_MSG = "This pool is lacking a constructor block."
  NO_CONSTRUCTOR_MSG = "This pool is lacking a destructor block."
  
=begin
  pool - size of pool (default 5)
  checkout_timeout - number of seconds to wait when getting a connection from the pool
  reaping_frequency - number of seconds to run the reaper (nil means don't run the reaper)
  dead_connection_timeout - number of seconds after which the reaper will consider a connection dead. (default 5 seconds)
=end

    def initialize params={}
    opts = params.inject({}) { |h,(k,v)| h[k.to_sym] = v; h }
    
    @pool = opts[:pool] || 5
    @checkout_timeout = opts[:checkout_timeout] || 5
    reaping_frequency = opts[:reaping_frequency] || 0 # setter starts autoreaping
    @dead_connection_timeout = opts[:dead_connection_timeout] || 5
    
        @expr_hash = {}
        @stack = []
        @count = 0
        @create = opts[:create]
        @destroy = opts[:destroy]
        @validate = opts[:validate]
    end
  
  # contructor/destructor/validator blocks
  def create █ threadsafe { @create = block }; end
  def destroy █ threadsafe { @destroy = block }; end
  def validate █ threadsafe { @validate = block }; end
  
    def empty?; @count.zero?; end
  def full?; @count == @pool; end
  def available?; @stack.count > 0 && !empty?; end # whether or not you can get an object from the pool
  def autoreaping?; @reaping_frequency > 0; end
  
  def reaping_frequency= v
    threadsafe { @reaping_frequency = v }
    start_autoreap if autoreaping?
  end
  
  def delete obj
    return obj if obj.nil?
    threadsafe do
      @count -= 1 if @stack.include? obj
          @destroy.call(@stack.delete(@expr_hash.delete(obj)) || obj)
    end
    obj
  end
  
    def with
    raise CreatorError, NO_CONSTRUCTOR_MSG if @create.nil?
    raise DestructorError, NO_DESTRUCTOR_MSG if @destroy.nil?
    
        begin
      obj = @create.call if @stack.empty? # create one if needed
      obj ||= Timeout.timeout(@checkout_timeout, TimeoutError) { threadsafe { @stack.pop } } # otherwise load from @stack
    
      if !_obj_valid?(obj)
        delete obj
        obj = @create.call
      end
    
      threadsafe { @expr_hash[obj] = Time.now }

            yield obj
        ensure
      threadsafe { @stack.push obj } unless obj.nil?
        end
    end
    
    def reap!
    raise DestructorError, NO_DESTRUCTOR_MSG if @destroy.nil?
        return if empty?
    threadsafe { @stack.reject(&method(:_obj_valid?)).each(&method(:delete)) }
    end

    def clear!
        threadsafe do
      @count = 0
      @expr_hash.clear
            @stack.each(&@destroy.method(:call))
            @stack.clear
        end
    end

  # num - there are a few cases
  #   negative - fill pool so that there are num.abs free spots for objects in the pool
  #   positive - add num elements to the pool
  #   not passed - fill the pool completely
    def fill! num=@pool-@count
    raise CreatorError, NO_CONSTRUCTOR_MSG if @create.nil?
    
    return 0 if full?
    num = @pool-num.abs if num < 0
    return 0 if num <= 0
    
    objs = num.times.map(&@create.method(:call))
    expr_addition = objs.zip([Time.now]*num)
    
    threadsafe do
      @expr_hash.merge(expr_addition)
      @stack.push *objs
      @count += num
    end
    
    start_purging!
        
        num
    end

    private

  def start_autoreap
    Thread.new do
      while autoreaping?
                sleep @reaping_frequency
                reap!
      end
    end
  end

    def _obj_valid? obj
    return false if obj.nil?
      valid = obj.respond_to?(:valid?) ? obj.valid? : (@validate.nil? ? true : @vaildate.call(obj))
        expired = (@dead_connection_timeout > 0 && (@expr_hash[obj]-Time.now).to_f > @dead_connection_timeout)
        !expired && valid
    end
end