lean_pool 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rspec_status +195 -0
- data/.rubocop.yml +14 -0
- data/CHANGELOG.md +19 -0
- data/Gemfile +10 -0
- data/LICENSE.txt +21 -0
- data/PUBLISH.md +66 -0
- data/QUICKSTART.md +88 -0
- data/README.md +324 -0
- data/Rakefile +11 -0
- data/examples/basic_usage.rb +73 -0
- data/lean_pool.gemspec +38 -0
- data/lib/lean_pool/errors.rb +69 -0
- data/lib/lean_pool/http_pool.rb +206 -0
- data/lib/lean_pool/pool.rb +441 -0
- data/lib/lean_pool/version.rb +14 -0
- data/lib/lean_pool.rb +62 -0
- metadata +78 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require_relative "pool"
|
|
5
|
+
|
|
6
|
+
module LeanPool
|
|
7
|
+
# HTTP connection pool implementation using Net::HTTP.
|
|
8
|
+
#
|
|
9
|
+
# HTTPPool provides a convenient wrapper around {Pool} for managing HTTP/1
|
|
10
|
+
# connections. It automatically handles connection lifecycle, request/response
|
|
11
|
+
# processing, and connection reuse.
|
|
12
|
+
#
|
|
13
|
+
# This is an example implementation demonstrating how to use LeanPool for
|
|
14
|
+
# HTTP connections. For production use with more advanced features (HTTP/2,
|
|
15
|
+
# connection keep-alive optimization, etc.), consider using libraries like
|
|
16
|
+
# HTTP.rb or Faraday with connection pooling.
|
|
17
|
+
#
|
|
18
|
+
# Features:
|
|
19
|
+
# - Automatic connection management and reuse
|
|
20
|
+
# - Support for both HTTP and HTTPS (SSL)
|
|
21
|
+
# - GET and POST request methods
|
|
22
|
+
# - Custom headers and timeouts
|
|
23
|
+
# - Automatic connection removal on errors
|
|
24
|
+
#
|
|
25
|
+
# @example Basic HTTP requests
|
|
26
|
+
# pool = LeanPool::HTTPPool.new("api.example.com", 443, size: 10, use_ssl: true)
|
|
27
|
+
#
|
|
28
|
+
# response = pool.get("/users")
|
|
29
|
+
# puts response[:status] # => 200
|
|
30
|
+
# puts response[:body]
|
|
31
|
+
# puts response[:headers]
|
|
32
|
+
#
|
|
33
|
+
# @example POST request with JSON
|
|
34
|
+
# pool = LeanPool::HTTPPool.new("api.example.com", 443, use_ssl: true)
|
|
35
|
+
#
|
|
36
|
+
# response = pool.post(
|
|
37
|
+
# "/users",
|
|
38
|
+
# body: '{"name":"John"}',
|
|
39
|
+
# headers: { "Content-Type" => "application/json" }
|
|
40
|
+
# )
|
|
41
|
+
#
|
|
42
|
+
# @example With custom timeout
|
|
43
|
+
# pool = LeanPool::HTTPPool.new("api.example.com", 80, timeout: 10.0)
|
|
44
|
+
# response = pool.get("/slow-endpoint", timeout: 15.0)
|
|
45
|
+
#
|
|
46
|
+
# @note Uses Ruby's standard Net::HTTP library. Connections are reused when
|
|
47
|
+
# possible but removed automatically if errors occur.
|
|
48
|
+
#
|
|
49
|
+
# @since 0.1.0
|
|
50
|
+
class HTTPPool
|
|
51
|
+
# Initialize a new HTTP connection pool.
|
|
52
|
+
#
|
|
53
|
+
# Creates a new pool for managing HTTP connections to the specified host and port.
|
|
54
|
+
# Connections are created lazily on-demand and reused when possible.
|
|
55
|
+
#
|
|
56
|
+
# @param host [String] The hostname or IP address to connect to
|
|
57
|
+
# @param port [Integer] The port number to connect to
|
|
58
|
+
# @param size [Integer] Maximum number of connections in the pool (default: 10)
|
|
59
|
+
# @param timeout [Float] Default timeout in seconds for operations (default: 5.0)
|
|
60
|
+
# @param use_ssl [Boolean] Whether to use SSL/TLS for connections (default: false)
|
|
61
|
+
#
|
|
62
|
+
# @example
|
|
63
|
+
# pool = LeanPool::HTTPPool.new("api.example.com", 443, size: 10, use_ssl: true)
|
|
64
|
+
def initialize(host, port, size: 10, timeout: 5.0, use_ssl: false)
|
|
65
|
+
@host = host
|
|
66
|
+
@port = port
|
|
67
|
+
@use_ssl = use_ssl
|
|
68
|
+
|
|
69
|
+
@pool = Pool.new(
|
|
70
|
+
size: size,
|
|
71
|
+
timeout: timeout,
|
|
72
|
+
pool_state: { host: host, port: port, use_ssl: use_ssl }
|
|
73
|
+
) do |state|
|
|
74
|
+
http = Net::HTTP.new(state[:host], state[:port])
|
|
75
|
+
http.use_ssl = state[:use_ssl] if state[:use_ssl]
|
|
76
|
+
http
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Perform a GET HTTP request.
|
|
81
|
+
#
|
|
82
|
+
# Retrieves a connection from the pool, performs a GET request to the specified
|
|
83
|
+
# path, and returns the response. The connection is automatically returned to
|
|
84
|
+
# the pool unless an error occurs, in which case it's removed.
|
|
85
|
+
#
|
|
86
|
+
# @param path [String] The request path (e.g., "/users" or "/api/v1/data")
|
|
87
|
+
# @param headers [Hash] Optional HTTP headers to include in the request
|
|
88
|
+
# @param timeout [Float, nil] Optional timeout override in seconds. If nil, uses
|
|
89
|
+
# the pool's default timeout.
|
|
90
|
+
# @return [Hash] Response hash with the following keys:
|
|
91
|
+
# - `:status` [Integer] HTTP status code (e.g., 200, 404, 500)
|
|
92
|
+
# - `:body` [String] Response body
|
|
93
|
+
# - `:headers` [Hash] Response headers
|
|
94
|
+
# @return [Hash] If an error occurs, returns a hash with `:error` and `:remove` keys
|
|
95
|
+
#
|
|
96
|
+
# @example Basic GET request
|
|
97
|
+
# response = pool.get("/users")
|
|
98
|
+
# puts response[:status] # => 200
|
|
99
|
+
# puts response[:body]
|
|
100
|
+
#
|
|
101
|
+
# @example With custom headers
|
|
102
|
+
# response = pool.get(
|
|
103
|
+
# "/api/data",
|
|
104
|
+
# headers: { "Authorization" => "Bearer token", "Accept" => "application/json" }
|
|
105
|
+
# )
|
|
106
|
+
#
|
|
107
|
+
# @example With custom timeout
|
|
108
|
+
# response = pool.get("/slow-endpoint", timeout: 30.0)
|
|
109
|
+
def get(path, headers: {}, timeout: nil)
|
|
110
|
+
@pool.checkout(timeout: timeout) do |http|
|
|
111
|
+
http.start unless http.started?
|
|
112
|
+
|
|
113
|
+
request = Net::HTTP::Get.new(path)
|
|
114
|
+
headers.each { |k, v| request[k] = v }
|
|
115
|
+
|
|
116
|
+
response = http.request(request)
|
|
117
|
+
|
|
118
|
+
{
|
|
119
|
+
status: response.code.to_i,
|
|
120
|
+
body: response.body,
|
|
121
|
+
headers: response.to_hash
|
|
122
|
+
}
|
|
123
|
+
end
|
|
124
|
+
rescue => e
|
|
125
|
+
# Remove connection if it's broken
|
|
126
|
+
{ error: e.message, remove: true }
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Perform a POST HTTP request.
|
|
130
|
+
#
|
|
131
|
+
# Retrieves a connection from the pool, performs a POST request to the specified
|
|
132
|
+
# path with the given body, and returns the response. The connection is automatically
|
|
133
|
+
# returned to the pool unless an error occurs, in which case it's removed.
|
|
134
|
+
#
|
|
135
|
+
# @param path [String] The request path (e.g., "/users" or "/api/v1/create")
|
|
136
|
+
# @param body [String] The request body to send (default: empty string)
|
|
137
|
+
# @param headers [Hash] Optional HTTP headers to include in the request
|
|
138
|
+
# @param timeout [Float, nil] Optional timeout override in seconds. If nil, uses
|
|
139
|
+
# the pool's default timeout.
|
|
140
|
+
# @return [Hash] Response hash with the following keys:
|
|
141
|
+
# - `:status` [Integer] HTTP status code (e.g., 200, 201, 400, 500)
|
|
142
|
+
# - `:body` [String] Response body
|
|
143
|
+
# - `:headers` [Hash] Response headers
|
|
144
|
+
# @return [Hash] If an error occurs, returns a hash with `:error` and `:remove` keys
|
|
145
|
+
#
|
|
146
|
+
# @example Basic POST request
|
|
147
|
+
# response = pool.post("/users", body: '{"name":"John"}')
|
|
148
|
+
#
|
|
149
|
+
# @example POST with JSON
|
|
150
|
+
# response = pool.post(
|
|
151
|
+
# "/api/users",
|
|
152
|
+
# body: '{"name":"John","email":"john@example.com"}',
|
|
153
|
+
# headers: { "Content-Type" => "application/json" }
|
|
154
|
+
# )
|
|
155
|
+
#
|
|
156
|
+
# @example POST with form data
|
|
157
|
+
# response = pool.post(
|
|
158
|
+
# "/submit",
|
|
159
|
+
# body: "name=John&email=john@example.com",
|
|
160
|
+
# headers: { "Content-Type" => "application/x-www-form-urlencoded" }
|
|
161
|
+
# )
|
|
162
|
+
def post(path, body: "", headers: {}, timeout: nil)
|
|
163
|
+
@pool.checkout(timeout: timeout) do |http|
|
|
164
|
+
http.start unless http.started?
|
|
165
|
+
|
|
166
|
+
request = Net::HTTP::Post.new(path)
|
|
167
|
+
headers.each { |k, v| request[k] = v }
|
|
168
|
+
request.body = body
|
|
169
|
+
|
|
170
|
+
response = http.request(request)
|
|
171
|
+
|
|
172
|
+
{
|
|
173
|
+
status: response.code.to_i,
|
|
174
|
+
body: response.body,
|
|
175
|
+
headers: response.to_hash
|
|
176
|
+
}
|
|
177
|
+
end
|
|
178
|
+
rescue => e
|
|
179
|
+
{ error: e.message, remove: true }
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Get HTTP pool statistics.
|
|
183
|
+
#
|
|
184
|
+
# Returns statistics about the underlying connection pool, including size,
|
|
185
|
+
# available connections, in-use connections, and total connections.
|
|
186
|
+
#
|
|
187
|
+
# @return [Hash] Pool statistics hash with `:size`, `:available`, `:in_use`, and `:total` keys
|
|
188
|
+
# @see Pool#stats
|
|
189
|
+
def stats
|
|
190
|
+
@pool.stats
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Shutdown the HTTP pool gracefully.
|
|
194
|
+
#
|
|
195
|
+
# Closes all HTTP connections in the pool and prevents new requests. All
|
|
196
|
+
# connections are properly finished before being removed from the pool.
|
|
197
|
+
#
|
|
198
|
+
# @return [void]
|
|
199
|
+
# @see Pool#shutdown
|
|
200
|
+
def shutdown
|
|
201
|
+
@pool.shutdown do |http|
|
|
202
|
+
http.finish if http.started?
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
end
|
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "concurrent-ruby"
|
|
4
|
+
|
|
5
|
+
module LeanPool
|
|
6
|
+
# A lightweight, process-free resource pool implementation.
|
|
7
|
+
#
|
|
8
|
+
# Pool manages a collection of resources (connections, sockets, file handles, etc.)
|
|
9
|
+
# without creating per-resource processes. It provides thread-safe access to resources
|
|
10
|
+
# with automatic lifecycle management, timeout handling, and resource reuse.
|
|
11
|
+
#
|
|
12
|
+
# Key features:
|
|
13
|
+
# - Thread-safe resource access using concurrent-ruby
|
|
14
|
+
# - Lazy or eager resource initialization
|
|
15
|
+
# - Automatic resource cleanup and removal
|
|
16
|
+
# - Configurable timeouts for checkout operations
|
|
17
|
+
# - Resource state tracking and statistics
|
|
18
|
+
# - Graceful shutdown and reload capabilities
|
|
19
|
+
#
|
|
20
|
+
# The pool maintains a fixed maximum number of resources and automatically manages
|
|
21
|
+
# their lifecycle. Resources can be marked for removal by returning a hash with
|
|
22
|
+
# `remove: true` from the checkout block.
|
|
23
|
+
#
|
|
24
|
+
# @example Basic Redis connection pool
|
|
25
|
+
# pool = LeanPool::Pool.new(size: 5) do
|
|
26
|
+
# Redis.new(host: "localhost", port: 6379)
|
|
27
|
+
# end
|
|
28
|
+
#
|
|
29
|
+
# pool.checkout do |redis|
|
|
30
|
+
# redis.get("key")
|
|
31
|
+
# redis.set("key", "value")
|
|
32
|
+
# end
|
|
33
|
+
#
|
|
34
|
+
# @example Database connection pool with custom state
|
|
35
|
+
# pool = LeanPool::Pool.new(
|
|
36
|
+
# size: 10,
|
|
37
|
+
# timeout: 5.0,
|
|
38
|
+
# lazy: true,
|
|
39
|
+
# pool_state: { host: "localhost", port: 5432, database: "mydb" }
|
|
40
|
+
# ) do |state|
|
|
41
|
+
# PG.connect(
|
|
42
|
+
# host: state[:host],
|
|
43
|
+
# port: state[:port],
|
|
44
|
+
# dbname: state[:database]
|
|
45
|
+
# )
|
|
46
|
+
# end
|
|
47
|
+
#
|
|
48
|
+
# pool.checkout do |conn|
|
|
49
|
+
# conn.exec("SELECT * FROM users")
|
|
50
|
+
# end
|
|
51
|
+
#
|
|
52
|
+
# @example Removing unhealthy resources
|
|
53
|
+
# pool.checkout do |resource|
|
|
54
|
+
# if resource.healthy?
|
|
55
|
+
# resource.perform_operation
|
|
56
|
+
# else
|
|
57
|
+
# { remove: true } # Remove unhealthy resource from pool
|
|
58
|
+
# end
|
|
59
|
+
# end
|
|
60
|
+
#
|
|
61
|
+
# @example Getting pool statistics
|
|
62
|
+
# stats = pool.stats
|
|
63
|
+
# # => { size: 5, available: 3, in_use: 2, total: 5 }
|
|
64
|
+
#
|
|
65
|
+
# @example Graceful shutdown
|
|
66
|
+
# pool.shutdown do |resource|
|
|
67
|
+
# resource.close if resource.respond_to?(:close)
|
|
68
|
+
# end
|
|
69
|
+
#
|
|
70
|
+
# @thread_safety Thread-safe. All operations use concurrent-ruby primitives
|
|
71
|
+
# for safe concurrent access.
|
|
72
|
+
#
|
|
73
|
+
# @see https://github.com/ruby-concurrency/concurrent-ruby concurrent-ruby
|
|
74
|
+
#
|
|
75
|
+
# @since 0.1.0
|
|
76
|
+
class Pool
|
|
77
|
+
# Initialize a new resource pool.
|
|
78
|
+
#
|
|
79
|
+
# Creates a new pool with the specified configuration. Resources can be
|
|
80
|
+
# initialized eagerly (all at once) or lazily (on-demand when needed).
|
|
81
|
+
#
|
|
82
|
+
# @param size [Integer] Maximum number of resources in the pool. Must be > 0.
|
|
83
|
+
# @param timeout [Float] Default timeout in seconds for checkout operations. Must be > 0.
|
|
84
|
+
# @param lazy [Boolean] If true, resources are created on-demand when checked out.
|
|
85
|
+
# If false, all resources are created immediately during initialization.
|
|
86
|
+
# @param pool_state [Object] Optional state object to pass to the resource initializer.
|
|
87
|
+
# This can be any object (Hash, Struct, etc.) that contains configuration needed
|
|
88
|
+
# to create resources.
|
|
89
|
+
# @yield [pool_state] Block that initializes a new resource
|
|
90
|
+
# @yieldparam pool_state [Object] The pool_state passed during initialization, or nil
|
|
91
|
+
# @yieldreturn [Object] The initialized resource to be managed by the pool
|
|
92
|
+
# @raise [ArgumentError] If size <= 0, timeout <= 0, or no block is provided
|
|
93
|
+
#
|
|
94
|
+
# @example Basic initialization
|
|
95
|
+
# pool = LeanPool::Pool.new(size: 5) { MyResource.new }
|
|
96
|
+
#
|
|
97
|
+
# @example With pool state
|
|
98
|
+
# pool = LeanPool::Pool.new(
|
|
99
|
+
# size: 10,
|
|
100
|
+
# pool_state: { host: "localhost", port: 6379 }
|
|
101
|
+
# ) do |state|
|
|
102
|
+
# Connection.new(state[:host], state[:port])
|
|
103
|
+
# end
|
|
104
|
+
#
|
|
105
|
+
# @example Eager initialization
|
|
106
|
+
# pool = LeanPool::Pool.new(size: 5, lazy: false) { MyResource.new }
|
|
107
|
+
# # All 5 resources are created immediately
|
|
108
|
+
def initialize(size: 5, timeout: 5.0, lazy: true, pool_state: nil, &block)
|
|
109
|
+
raise ArgumentError, "size must be greater than 0" if size <= 0
|
|
110
|
+
raise ArgumentError, "timeout must be positive" if timeout <= 0
|
|
111
|
+
raise ArgumentError, "initializer block required" unless block_given?
|
|
112
|
+
|
|
113
|
+
@size = size
|
|
114
|
+
@timeout = timeout
|
|
115
|
+
@lazy = lazy
|
|
116
|
+
@pool_state = pool_state
|
|
117
|
+
@initializer = block
|
|
118
|
+
@shutdown = Concurrent::AtomicBoolean.new(false)
|
|
119
|
+
|
|
120
|
+
# Thread-safe collections
|
|
121
|
+
@available = Concurrent::Array.new
|
|
122
|
+
@in_use = Concurrent::Hash.new
|
|
123
|
+
@mutex = Concurrent::ReentrantReadWriteLock.new
|
|
124
|
+
|
|
125
|
+
# Initialize resources if not lazy
|
|
126
|
+
initialize_resources unless lazy
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Checkout a resource from the pool and execute a block with it.
|
|
130
|
+
#
|
|
131
|
+
# Retrieves an available resource from the pool (or creates a new one if the pool
|
|
132
|
+
# is not full and lazy initialization is enabled), executes the provided block
|
|
133
|
+
# with the resource, and then returns the resource to the pool (or removes it
|
|
134
|
+
# if indicated by the block's return value).
|
|
135
|
+
#
|
|
136
|
+
# If the pool is full and no resources are available, this method will wait up
|
|
137
|
+
# to the specified timeout for a resource to become available.
|
|
138
|
+
#
|
|
139
|
+
# The resource is automatically returned to the pool after the block executes,
|
|
140
|
+
# unless the block returns a hash with `remove: true` or `error: true`, in which
|
|
141
|
+
# case the resource is removed from the pool.
|
|
142
|
+
#
|
|
143
|
+
# @param timeout [Float, nil] Optional timeout override in seconds. If nil, uses
|
|
144
|
+
# the pool's default timeout.
|
|
145
|
+
# @yield [resource] Block to execute with the checked out resource
|
|
146
|
+
# @yieldparam resource [Object] The checked out resource from the pool
|
|
147
|
+
# @yieldreturn [Object] Return value from the block. If a Hash with `remove: true`
|
|
148
|
+
# or `error: true` is returned, the resource will be removed from the pool.
|
|
149
|
+
# @return [Object] The return value from the block
|
|
150
|
+
# @raise [TimeoutError] If checkout times out waiting for an available resource
|
|
151
|
+
# @raise [ShutdownError] If the pool has been shutdown
|
|
152
|
+
# @raise [ArgumentError] If no block is provided
|
|
153
|
+
# @raise [ResourceError] If resource creation fails
|
|
154
|
+
#
|
|
155
|
+
# @example Basic checkout
|
|
156
|
+
# result = pool.checkout do |resource|
|
|
157
|
+
# resource.perform_operation
|
|
158
|
+
# end
|
|
159
|
+
#
|
|
160
|
+
# @example With custom timeout
|
|
161
|
+
# pool.checkout(timeout: 2.0) do |resource|
|
|
162
|
+
# resource.slow_operation
|
|
163
|
+
# end
|
|
164
|
+
#
|
|
165
|
+
# @example Removing unhealthy resources
|
|
166
|
+
# pool.checkout do |resource|
|
|
167
|
+
# if resource.healthy?
|
|
168
|
+
# resource.use
|
|
169
|
+
# else
|
|
170
|
+
# { remove: true } # Resource will be removed
|
|
171
|
+
# end
|
|
172
|
+
# end
|
|
173
|
+
def checkout(timeout: nil, &block)
|
|
174
|
+
raise ShutdownError, "Pool is shutdown" if @shutdown.true?
|
|
175
|
+
raise ArgumentError, "block required" unless block_given?
|
|
176
|
+
|
|
177
|
+
timeout ||= @timeout
|
|
178
|
+
deadline = Time.now + timeout
|
|
179
|
+
|
|
180
|
+
resource = nil
|
|
181
|
+
thread_id = Thread.current.object_id
|
|
182
|
+
|
|
183
|
+
begin
|
|
184
|
+
# Try to get an available resource
|
|
185
|
+
@mutex.with_write_lock do
|
|
186
|
+
resource = @available.pop
|
|
187
|
+
|
|
188
|
+
# Create new resource if none available and pool not full
|
|
189
|
+
if resource.nil? && @in_use.size < @size
|
|
190
|
+
resource = create_resource
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Wait for resource if pool is full
|
|
195
|
+
while resource.nil? && Time.now < deadline
|
|
196
|
+
sleep(0.01) # Small sleep to avoid busy waiting
|
|
197
|
+
|
|
198
|
+
@mutex.with_write_lock do
|
|
199
|
+
resource = @available.pop
|
|
200
|
+
|
|
201
|
+
# Try to create new resource if none available and pool not full
|
|
202
|
+
if resource.nil? && @in_use.size < @size
|
|
203
|
+
resource = create_resource
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
raise TimeoutError, "Timeout waiting for resource" if resource.nil?
|
|
209
|
+
|
|
210
|
+
# Mark resource as in use
|
|
211
|
+
@mutex.with_write_lock do
|
|
212
|
+
@in_use[thread_id] = resource
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# Execute block with resource
|
|
216
|
+
result = block.call(resource)
|
|
217
|
+
|
|
218
|
+
# Check resource state and return to pool or remove
|
|
219
|
+
checkin_resource(resource, result)
|
|
220
|
+
|
|
221
|
+
result
|
|
222
|
+
rescue => e
|
|
223
|
+
# Remove resource if error occurred
|
|
224
|
+
discard_resource(resource) if resource
|
|
225
|
+
raise
|
|
226
|
+
ensure
|
|
227
|
+
# Always remove from in_use
|
|
228
|
+
@mutex.with_write_lock do
|
|
229
|
+
@in_use.delete(thread_id)
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# Checkout a resource, raising on error (alias for checkout).
|
|
235
|
+
#
|
|
236
|
+
# This is an alias for {#checkout} that provides a more explicit name
|
|
237
|
+
# for operations that should raise exceptions on errors.
|
|
238
|
+
#
|
|
239
|
+
# @param timeout [Float, nil] Optional timeout override in seconds
|
|
240
|
+
# @yield [resource] Block to execute with the checked out resource
|
|
241
|
+
# @return [Object] The return value from the block
|
|
242
|
+
# @raise [TimeoutError] If checkout times out
|
|
243
|
+
# @raise [ShutdownError] If pool is shutdown
|
|
244
|
+
# @see #checkout
|
|
245
|
+
def checkout!(timeout: nil, &block)
|
|
246
|
+
checkout(timeout: timeout, &block)
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# Get current pool statistics.
|
|
250
|
+
#
|
|
251
|
+
# Returns a hash containing information about the current state of the pool,
|
|
252
|
+
# including the maximum size, number of available resources, number of resources
|
|
253
|
+
# currently in use, and total number of resources.
|
|
254
|
+
#
|
|
255
|
+
# @return [Hash] Hash with the following keys:
|
|
256
|
+
# - `:size` [Integer] Maximum number of resources in the pool
|
|
257
|
+
# - `:available` [Integer] Number of resources currently available for checkout
|
|
258
|
+
# - `:in_use` [Integer] Number of resources currently checked out
|
|
259
|
+
# - `:total` [Integer] Total number of resources (available + in_use)
|
|
260
|
+
#
|
|
261
|
+
# @example
|
|
262
|
+
# stats = pool.stats
|
|
263
|
+
# # => { size: 5, available: 3, in_use: 2, total: 5 }
|
|
264
|
+
#
|
|
265
|
+
# @note This method is thread-safe and uses a read lock for minimal contention.
|
|
266
|
+
def stats
|
|
267
|
+
@mutex.with_read_lock do
|
|
268
|
+
{
|
|
269
|
+
size: @size,
|
|
270
|
+
available: @available.size,
|
|
271
|
+
in_use: @in_use.size,
|
|
272
|
+
total: @available.size + @in_use.size
|
|
273
|
+
}
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
# Shutdown the pool gracefully.
|
|
278
|
+
#
|
|
279
|
+
# Marks the pool as shutdown, preventing new checkouts, and cleans up all
|
|
280
|
+
# resources. The method will wait for in-use resources to be returned (up to
|
|
281
|
+
# the pool's timeout), then force cleanup of any remaining resources.
|
|
282
|
+
#
|
|
283
|
+
# After shutdown, all subsequent checkout attempts will raise {ShutdownError}.
|
|
284
|
+
#
|
|
285
|
+
# @param cleanup [Proc, nil] Optional block to cleanup each resource. If provided,
|
|
286
|
+
# this block will be called for each resource. If not provided, the pool will
|
|
287
|
+
# attempt to call common cleanup methods (`close`, `quit`, `disconnect`) if
|
|
288
|
+
# they exist on the resource.
|
|
289
|
+
# @yield [resource] Cleanup block for each resource in the pool
|
|
290
|
+
# @return [void]
|
|
291
|
+
#
|
|
292
|
+
# @example With custom cleanup
|
|
293
|
+
# pool.shutdown do |resource|
|
|
294
|
+
# resource.close if resource.respond_to?(:close)
|
|
295
|
+
# resource.cleanup if resource.respond_to?(:cleanup)
|
|
296
|
+
# end
|
|
297
|
+
#
|
|
298
|
+
# @example Without cleanup block (uses default cleanup)
|
|
299
|
+
# pool.shutdown
|
|
300
|
+
#
|
|
301
|
+
# @note Cleanup errors are silently ignored to ensure all resources are processed.
|
|
302
|
+
def shutdown(&cleanup)
|
|
303
|
+
@shutdown.make_true
|
|
304
|
+
|
|
305
|
+
@mutex.with_write_lock do
|
|
306
|
+
# Cleanup all available resources
|
|
307
|
+
while resource = @available.pop
|
|
308
|
+
cleanup_resource(resource, cleanup)
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
# Wait for in-use resources (with timeout)
|
|
312
|
+
deadline = Time.now + @timeout
|
|
313
|
+
while !@in_use.empty? && Time.now < deadline
|
|
314
|
+
sleep(0.1)
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
# Force cleanup remaining in-use resources
|
|
318
|
+
@in_use.each_value do |resource|
|
|
319
|
+
cleanup_resource(resource, cleanup)
|
|
320
|
+
end
|
|
321
|
+
@in_use.clear
|
|
322
|
+
end
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
# Reload the pool by shutting down and reinitializing.
|
|
326
|
+
#
|
|
327
|
+
# Shuts down the pool (cleaning up all existing resources), then re-enables
|
|
328
|
+
# the pool for new checkouts. If the pool was initialized with `lazy: false`,
|
|
329
|
+
# resources will be recreated immediately. Otherwise, resources will be created
|
|
330
|
+
# on-demand as before.
|
|
331
|
+
#
|
|
332
|
+
# This is useful after forking processes or when you need to reset the pool
|
|
333
|
+
# state without creating a new pool instance.
|
|
334
|
+
#
|
|
335
|
+
# @param cleanup [Proc, nil] Optional block to cleanup resources during shutdown
|
|
336
|
+
# @yield [resource] Cleanup block for each resource (passed to shutdown)
|
|
337
|
+
# @return [void]
|
|
338
|
+
#
|
|
339
|
+
# @example Reload after fork
|
|
340
|
+
# if fork
|
|
341
|
+
# pool.reload # Reinitialize pool in child process
|
|
342
|
+
# end
|
|
343
|
+
#
|
|
344
|
+
# @see #shutdown
|
|
345
|
+
def reload(&cleanup)
|
|
346
|
+
shutdown(&cleanup)
|
|
347
|
+
@shutdown.make_false
|
|
348
|
+
initialize_resources unless @lazy
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
private
|
|
352
|
+
|
|
353
|
+
# Initialize all resources for eager initialization.
|
|
354
|
+
#
|
|
355
|
+
# Creates the maximum number of resources and adds them to the available pool.
|
|
356
|
+
# This is called during initialization if `lazy: false`.
|
|
357
|
+
#
|
|
358
|
+
# @return [void]
|
|
359
|
+
def initialize_resources
|
|
360
|
+
@size.times do
|
|
361
|
+
resource = create_resource
|
|
362
|
+
@available << resource if resource
|
|
363
|
+
end
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
# Create a new resource using the initializer block.
|
|
367
|
+
#
|
|
368
|
+
# Calls the initializer block with the pool state and returns the created resource.
|
|
369
|
+
# If resource creation fails, raises a ResourceError with the original error
|
|
370
|
+
# information.
|
|
371
|
+
#
|
|
372
|
+
# @return [Object] The newly created resource
|
|
373
|
+
# @raise [ResourceError] If resource creation fails
|
|
374
|
+
def create_resource
|
|
375
|
+
begin
|
|
376
|
+
resource = @initializer.call(@pool_state)
|
|
377
|
+
# Allow nil resources (some use cases may need this)
|
|
378
|
+
resource
|
|
379
|
+
rescue => e
|
|
380
|
+
raise ResourceError, "Failed to create resource: #{e.message}", e.backtrace
|
|
381
|
+
end
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
# Check in a resource after use.
|
|
385
|
+
#
|
|
386
|
+
# Determines whether to return the resource to the pool or remove it based on
|
|
387
|
+
# the block's return value. If the result is a Hash with `remove: true` or
|
|
388
|
+
# `error: true`, the resource is discarded. Otherwise, it's returned to the
|
|
389
|
+
# available pool.
|
|
390
|
+
#
|
|
391
|
+
# @param resource [Object] The resource to check in
|
|
392
|
+
# @param result [Object] The return value from the checkout block
|
|
393
|
+
# @return [void]
|
|
394
|
+
def checkin_resource(resource, result)
|
|
395
|
+
should_remove = if result.is_a?(Hash)
|
|
396
|
+
result[:remove] == true
|
|
397
|
+
else
|
|
398
|
+
false
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
if should_remove
|
|
402
|
+
discard_resource(resource)
|
|
403
|
+
else
|
|
404
|
+
@mutex.with_write_lock do
|
|
405
|
+
@available << resource
|
|
406
|
+
end
|
|
407
|
+
end
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
# Discard a resource from the pool.
|
|
411
|
+
#
|
|
412
|
+
# Removes a resource from the pool. By default, this simply allows the resource
|
|
413
|
+
# to be garbage collected. Override this method in subclasses if you need
|
|
414
|
+
# explicit cleanup before discarding.
|
|
415
|
+
#
|
|
416
|
+
# @param resource [Object] The resource to discard
|
|
417
|
+
# @return [void]
|
|
418
|
+
def discard_resource(resource)
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
# Cleanup a resource during shutdown.
|
|
422
|
+
#
|
|
423
|
+
# Calls the provided cleanup block if given, otherwise attempts to call common
|
|
424
|
+
# cleanup methods on the resource (`close`, `quit`, `disconnect`). Errors during
|
|
425
|
+
# cleanup are silently ignored to ensure all resources are processed.
|
|
426
|
+
#
|
|
427
|
+
# @param resource [Object] The resource to cleanup
|
|
428
|
+
# @param cleanup [Proc, nil] Optional cleanup block
|
|
429
|
+
# @return [void]
|
|
430
|
+
def cleanup_resource(resource, cleanup)
|
|
431
|
+
if cleanup
|
|
432
|
+
cleanup.call(resource)
|
|
433
|
+
else
|
|
434
|
+
resource.close if resource.respond_to?(:close)
|
|
435
|
+
resource.quit if resource.respond_to?(:quit)
|
|
436
|
+
resource.disconnect if resource.respond_to?(:disconnect)
|
|
437
|
+
end
|
|
438
|
+
rescue => e
|
|
439
|
+
end
|
|
440
|
+
end
|
|
441
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LeanPool
|
|
4
|
+
# The current version of LeanPool.
|
|
5
|
+
#
|
|
6
|
+
# Follows semantic versioning (MAJOR.MINOR.PATCH):
|
|
7
|
+
# - MAJOR: Breaking changes
|
|
8
|
+
# - MINOR: New features, backwards compatible
|
|
9
|
+
# - PATCH: Bug fixes, backwards compatible
|
|
10
|
+
#
|
|
11
|
+
# @return [String] The version string (e.g., "0.1.0")
|
|
12
|
+
# @since 0.1.0
|
|
13
|
+
VERSION = "0.1.0"
|
|
14
|
+
end
|