Class: Ratomic::LocalPool

Inherits:
Object
  • Object
show all
Defined in:
lib/ratomic/local_pool.rb

Overview

Note:

LocalPool is implemented in pure Ruby. It is not backed by the Rust native extension used by Counter, Map, and Queue. Its safety comes from Ruby Ractor locality: live resources are created and reused inside the Ractor that owns them.

Design Note

LocalPool originated while investigating Redis clients under Ruby Ractors. The original goal was to reuse Pool, but ownership-transfer semantics proved incompatible with live resources containing internal state.

The resulting architecture became known as the “Inception Pool” design:

LocalPool facade
       ↓
Ractor-local pool
       ↓
Live resources

or informally:

Pool
  
Pool
  
Resource

The public API intentionally uses the more descriptive name ‘LocalPool`.

A shareable facade over resources that stay local to each Ractor.

LocalPool is intended for live resources such as Redis clients, database connections, sockets, and other objects which must not be moved between Ractors. The facade itself is shareable, but each Ractor lazily creates and owns an independent thread-safe local pool. Threads inside the same Ractor share that local pool; different Ractors never share the live resources.

This is the correct shape for resources with process, socket, connection, or native state. Move work across Ractor boundaries, not live clients.

The factory must be Ractor-shareable because the facade stores it and each Ractor calls it when its local resource pool needs to create a resource. Prefer a small immutable callable object instead of a block when the pool will be used from multiple Ractors.

Examples:

Redis clients owned by each Ractor

RedisFactory = Data.define(:host) do
  def call
    RedisClient.new(host: host)
  end
end

REDIS = Ratomic::LocalPool.new(
  size: 5,
  timeout: 1,
  factory: RedisFactory.new("127.0.0.1".freeze)
)

REDIS.with { |client| client.call("ping") }

See Also:

Instance Method Summary collapse

Constructor Details

#initialize(size: 5, timeout: 1.0, factory: nil, &block) ⇒ LocalPool

Create a per-Ractor local pool facade.

Parameters:

  • size (Integer) (defaults to: 5)

    maximum number of resources in each Ractor-local pool

  • timeout (Numeric, nil) (defaults to: 1.0)

    checkout timeout in seconds, or nil to wait indefinitely

  • factory (#call, nil) (defaults to: nil)

    shareable object factory

Yield Returns:

  • (Object)

    resource created inside the current Ractor

Raises:

  • (ArgumentError)

    if size, timeout, or factory is invalid

  • (LocalJumpError)

    if no factory or block is given



80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
# File 'lib/ratomic/local_pool.rb', line 80

def initialize(size: 5, timeout: 1.0, factory: nil, &block)
  raise ArgumentError, "pool size must be positive" unless size.is_a?(Integer) && size.positive?
  raise ArgumentError, "pool timeout must be numeric or nil" unless timeout.nil? || timeout.is_a?(Numeric)
  raise ArgumentError, "pool timeout must be non-negative" if timeout && timeout.negative?
  raise ArgumentError, "use either factory: or block, not both" if factory && block

  factory ||= block
  raise LocalJumpError, "no factory given" unless factory
  raise ArgumentError, "factory must respond to #call" unless factory.respond_to?(:call)
  raise ArgumentError, "factory must be Ractor-shareable" unless Ractor.shareable?(factory)

  @size = size
  @timeout = timeout&.to_f
  @factory = factory
  @storage_key = :"ratomic_local_pool_#{object_id}"

  freeze
  Ractor.make_shareable(self)
end

Instance Method Details

#closenil

Close the current Ractor’s local pool, if it has been initialized.

Other Ractors own independent local pools and are not affected. Available resources are closed if they respond to #close. Resources currently checked out by threads in this Ractor are closed when returned.

Returns:

  • (nil)


122
123
124
125
126
127
128
129
# File 'lib/ratomic/local_pool.rb', line 122

def close
  pool = Ractor.current[@storage_key]
  return nil unless pool

  Ractor.current[@storage_key] = nil
  pool.close
  nil
end

#with {|object| ... } ⇒ Object

Checkout a current-Ractor-owned resource, yield it, then return it to the same Ractor-local pool.

No resource is moved between Ractors. The yielded object belongs to the Ractor which called this method.

Yield Parameters:

  • object (Object)

    current-Ractor-owned resource

Returns:

  • (Object)

    the block return value

Raises:



109
110
111
112
113
# File 'lib/ratomic/local_pool.rb', line 109

def with
  local_pool.with { |object| yield object }
rescue Timeout::Error
  raise Ratomic::Error, "pool checkout timeout"
end