Class: LeanPool::Pool

Inherits:
Object
  • Object
show all
Defined in:
lib/lean_pool/pool.rb

Overview

A lightweight, process-free resource pool implementation.

Pool manages a collection of resources (connections, sockets, file handles, etc.) without creating per-resource processes. It provides thread-safe access to resources with automatic lifecycle management, timeout handling, and resource reuse.

Key features:

  • Thread-safe resource access using concurrent-ruby
  • Lazy or eager resource initialization
  • Automatic resource cleanup and removal
  • Configurable timeouts for checkout operations
  • Resource state tracking and statistics
  • Graceful shutdown and reload capabilities

The pool maintains a fixed maximum number of resources and automatically manages their lifecycle. Resources can be marked for removal by returning a hash with remove: true from the checkout block.

Examples:

Basic Redis connection pool

pool = LeanPool::Pool.new(size: 5) do
  Redis.new(host: "localhost", port: 6379)
end

pool.checkout do |redis|
  redis.get("key")
  redis.set("key", "value")
end

Database connection pool with custom state

pool = LeanPool::Pool.new(
  size: 10,
  timeout: 5.0,
  lazy: true,
  pool_state: { host: "localhost", port: 5432, database: "mydb" }
) do |state|
  PG.connect(
    host: state[:host],
    port: state[:port],
    dbname: state[:database]
  )
end

pool.checkout do |conn|
  conn.exec("SELECT * FROM users")
end

Removing unhealthy resources

pool.checkout do |resource|
  if resource.healthy?
    resource.perform_operation
  else
    { remove: true }  # Remove unhealthy resource from pool
  end
end

Getting pool statistics

stats = pool.stats
# => { size: 5, available: 3, in_use: 2, total: 5 }

Graceful shutdown

pool.shutdown do |resource|
  resource.close if resource.respond_to?(:close)
end

See Also:

Since:

  • 0.1.0

Instance Method Summary collapse

Constructor Details

#initialize(size: 5, timeout: 5.0, lazy: true, pool_state: nil) {|pool_state| ... } ⇒ Pool

Initialize a new resource pool.

Creates a new pool with the specified configuration. Resources can be initialized eagerly (all at once) or lazily (on-demand when needed).

Examples:

Basic initialization

pool = LeanPool::Pool.new(size: 5) { MyResource.new }

With pool state

pool = LeanPool::Pool.new(
  size: 10,
  pool_state: { host: "localhost", port: 6379 }
) do |state|
  Connection.new(state[:host], state[:port])
end

Eager initialization

pool = LeanPool::Pool.new(size: 5, lazy: false) { MyResource.new }
# All 5 resources are created immediately

Parameters:

  • size (Integer) (defaults to: 5)

    Maximum number of resources in the pool. Must be > 0.

  • timeout (Float) (defaults to: 5.0)

    Default timeout in seconds for checkout operations. Must be > 0.

  • lazy (Boolean) (defaults to: true)

    If true, resources are created on-demand when checked out. If false, all resources are created immediately during initialization.

  • pool_state (Object) (defaults to: nil)

    Optional state object to pass to the resource initializer. This can be any object (Hash, Struct, etc.) that contains configuration needed to create resources.

Yields:

  • (pool_state)

    Block that initializes a new resource

Yield Parameters:

  • pool_state (Object)

    The pool_state passed during initialization, or nil

Yield Returns:

  • (Object)

    The initialized resource to be managed by the pool

Raises:

  • (ArgumentError)

    If size <= 0, timeout <= 0, or no block is provided

Since:

  • 0.1.0



108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
# File 'lib/lean_pool/pool.rb', line 108

def initialize(size: 5, timeout: 5.0, lazy: true, pool_state: nil, &block)
  raise ArgumentError, "size must be greater than 0" if size <= 0
  raise ArgumentError, "timeout must be positive" if timeout <= 0
  raise ArgumentError, "initializer block required" unless block_given?

  @size = size
  @timeout = timeout
  @lazy = lazy
  @pool_state = pool_state
  @initializer = block
  @shutdown = Concurrent::AtomicBoolean.new(false)

  # Thread-safe collections
  @available = Concurrent::Array.new
  @in_use = Concurrent::Hash.new
  @mutex = Concurrent::ReentrantReadWriteLock.new

  # Initialize resources if not lazy
  initialize_resources unless lazy
end

Instance Method Details

#checkout(timeout: nil) {|resource| ... } ⇒ Object

Checkout a resource from the pool and execute a block with it.

Retrieves an available resource from the pool (or creates a new one if the pool is not full and lazy initialization is enabled), executes the provided block with the resource, and then returns the resource to the pool (or removes it if indicated by the block's return value).

If the pool is full and no resources are available, this method will wait up to the specified timeout for a resource to become available.

The resource is automatically returned to the pool after the block executes, unless the block returns a hash with remove: true or error: true, in which case the resource is removed from the pool.

Examples:

Basic checkout

result = pool.checkout do |resource|
  resource.perform_operation
end

With custom timeout

pool.checkout(timeout: 2.0) do |resource|
  resource.slow_operation
end

Removing unhealthy resources

pool.checkout do |resource|
  if resource.healthy?
    resource.use
  else
    { remove: true }  # Resource will be removed
  end
end

Parameters:

  • timeout (Float, nil) (defaults to: nil)

    Optional timeout override in seconds. If nil, uses the pool's default timeout.

Yields:

  • (resource)

    Block to execute with the checked out resource

Yield Parameters:

  • resource (Object)

    The checked out resource from the pool

Yield Returns:

  • (Object)

    Return value from the block. If a Hash with remove: true or error: true is returned, the resource will be removed from the pool.

Returns:

  • (Object)

    The return value from the block

Raises:

  • (TimeoutError)

    If checkout times out waiting for an available resource

  • (ShutdownError)

    If the pool has been shutdown

  • (ArgumentError)

    If no block is provided

  • (ResourceError)

    If resource creation fails

Since:

  • 0.1.0



173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
# File 'lib/lean_pool/pool.rb', line 173

def checkout(timeout: nil, &block)
  raise ShutdownError, "Pool is shutdown" if @shutdown.true?
  raise ArgumentError, "block required" unless block_given?

  timeout ||= @timeout
  deadline = Time.now + timeout

  resource = nil
  thread_id = Thread.current.object_id

  begin
    # Try to get an available resource
    @mutex.with_write_lock do
      resource = @available.pop
      
      # Create new resource if none available and pool not full
      if resource.nil? && @in_use.size < @size
        resource = create_resource
      end
    end

    # Wait for resource if pool is full
    while resource.nil? && Time.now < deadline
      sleep(0.01) # Small sleep to avoid busy waiting
      
      @mutex.with_write_lock do
        resource = @available.pop
        
        # Try to create new resource if none available and pool not full
        if resource.nil? && @in_use.size < @size
          resource = create_resource
        end
      end
    end

    raise TimeoutError, "Timeout waiting for resource" if resource.nil?

    # Mark resource as in use
    @mutex.with_write_lock do
      @in_use[thread_id] = resource
    end

    # Execute block with resource
    result = block.call(resource)

    # Check resource state and return to pool or remove
    checkin_resource(resource, result)

    result
  rescue => e
    # Remove resource if error occurred
    discard_resource(resource) if resource
    raise
  ensure
    # Always remove from in_use
    @mutex.with_write_lock do
      @in_use.delete(thread_id)
    end
  end
end

#checkout!(timeout: nil) {|resource| ... } ⇒ Object

Checkout a resource, raising on error (alias for checkout).

This is an alias for #checkout that provides a more explicit name for operations that should raise exceptions on errors.

Parameters:

  • timeout (Float, nil) (defaults to: nil)

    Optional timeout override in seconds

Yields:

  • (resource)

    Block to execute with the checked out resource

Returns:

  • (Object)

    The return value from the block

Raises:

See Also:

Since:

  • 0.1.0



245
246
247
# File 'lib/lean_pool/pool.rb', line 245

def checkout!(timeout: nil, &block)
  checkout(timeout: timeout, &block)
end

#reload(&cleanup) {|resource| ... } ⇒ void

This method returns an undefined value.

Reload the pool by shutting down and reinitializing.

Shuts down the pool (cleaning up all existing resources), then re-enables the pool for new checkouts. If the pool was initialized with lazy: false, resources will be recreated immediately. Otherwise, resources will be created on-demand as before.

This is useful after forking processes or when you need to reset the pool state without creating a new pool instance.

Examples:

Reload after fork

if fork
  pool.reload  # Reinitialize pool in child process
end

Parameters:

  • cleanup (Proc, nil)

    Optional block to cleanup resources during shutdown

Yields:

  • (resource)

    Cleanup block for each resource (passed to shutdown)

See Also:

Since:

  • 0.1.0



345
346
347
348
349
# File 'lib/lean_pool/pool.rb', line 345

def reload(&cleanup)
  shutdown(&cleanup)
  @shutdown.make_false
  initialize_resources unless @lazy
end

#shutdown(&cleanup) {|resource| ... } ⇒ void

Note:

Cleanup errors are silently ignored to ensure all resources are processed.

This method returns an undefined value.

Shutdown the pool gracefully.

Marks the pool as shutdown, preventing new checkouts, and cleans up all resources. The method will wait for in-use resources to be returned (up to the pool's timeout), then force cleanup of any remaining resources.

After shutdown, all subsequent checkout attempts will raise ShutdownError.

Examples:

With custom cleanup

pool.shutdown do |resource|
  resource.close if resource.respond_to?(:close)
  resource.cleanup if resource.respond_to?(:cleanup)
end

Without cleanup block (uses default cleanup)

pool.shutdown

Parameters:

  • cleanup (Proc, nil)

    Optional block to cleanup each resource. If provided, this block will be called for each resource. If not provided, the pool will attempt to call common cleanup methods (close, quit, disconnect) if they exist on the resource.

Yields:

  • (resource)

    Cleanup block for each resource in the pool

Since:

  • 0.1.0



302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
# File 'lib/lean_pool/pool.rb', line 302

def shutdown(&cleanup)
  @shutdown.make_true

  @mutex.with_write_lock do
    # Cleanup all available resources
    while resource = @available.pop
      cleanup_resource(resource, cleanup)
    end

    # Wait for in-use resources (with timeout)
    deadline = Time.now + @timeout
    while !@in_use.empty? && Time.now < deadline
      sleep(0.1)
    end

    # Force cleanup remaining in-use resources
    @in_use.each_value do |resource|
      cleanup_resource(resource, cleanup)
    end
    @in_use.clear
  end
end

#statsHash

Note:

This method is thread-safe and uses a read lock for minimal contention.

Get current pool statistics.

Returns a hash containing information about the current state of the pool, including the maximum size, number of available resources, number of resources currently in use, and total number of resources.

Examples:

stats = pool.stats
# => { size: 5, available: 3, in_use: 2, total: 5 }

Returns:

  • (Hash)

    Hash with the following keys:

    • :size [Integer] Maximum number of resources in the pool
    • :available [Integer] Number of resources currently available for checkout
    • :in_use [Integer] Number of resources currently checked out
    • :total [Integer] Total number of resources (available + in_use)

Since:

  • 0.1.0



266
267
268
269
270
271
272
273
274
275
# File 'lib/lean_pool/pool.rb', line 266

def stats
  @mutex.with_read_lock do
    {
      size: @size,
      available: @available.size,
      in_use: @in_use.size,
      total: @available.size + @in_use.size
    }
  end
end