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.
@@ -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