connectionpool

package module
v0.0.0-...-df03c3d Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 17, 2026 License: BSD-3-Clause Imports: 3 Imported by: 0

README

go-ruby-connection-pool/connection-pool

connection-pool — go-ruby-connection-pool

Docs License Go Coverage

A pure-Go (no cgo) reimplementation of Ruby's connection_pool gem — the generic, thread-safe pool of reusable connections — faithful to the observable behaviour of connection_pool on MRI 4.0.5. It mirrors the gem's lazy creation up to a fixed size, its timeout-bounded checkout that raises ConnectionPool::TimeoutError on exhaustion, its per-thread reentrant checkout, shutdown / reload disposal, and the method-delegating ConnectionPool::Wrapperwithout any Ruby runtime.

It is the connection_pool backend for go-embedded-ruby, but is a standalone, reusable module with no dependency on the Ruby runtime — a sibling of go-ruby-set and go-ruby-bigdecimal.

MRI-faithful, not Composition-Oriented. This is the gem's pool — every semantics decision matches what MRI 4.0.5 + connection_pool does, verified by a differential oracle that runs the real gem side by side with this package.

Connections are opaque

A "connection" is any value the factory returns; the pool never inspects it. Redis clients, database handles, sockets, or plain Go structs all work. The pool only tracks ownership and lifetime — how many exist, who holds one, and when each returns.

type Factory func() any

The factory is called lazily, at most size times over the pool's life, the first time a caller needs a connection that is not already idle.

Reentrancy — the caller-key seam

In MRI the checked-out connection lives in thread-local storage, so a nested with on the same thread reuses the same connection (a depth counter decides when it truly returns to the pool). Go has no thread-locals, so the host identifies the logical caller with a CallerKey:

type CallerKey any

Two Checkout/With calls sharing a key nest — the inner one reuses the outer's connection and the connection returns to the pool only when the outermost Checkin runs — exactly as two nested with blocks on one Ruby thread do. The go-embedded-ruby binding plugs the current Ruby thread/fiber in here, so require "connection_pool" behaves identically to MRI.

Install

go get github.com/go-ruby-connection-pool/connection-pool

Usage

package main

import (
	"fmt"
	"time"

	connectionpool "github.com/go-ruby-connection-pool/connection-pool"
)

func main() {
	// ConnectionPool.new(size: 2, timeout: 5) { Conn.new }
	pool := connectionpool.New(2, 5*time.Second, func() any { return newConn() })

	// pool.with { |conn| conn.query(...) }  — the CallerKey stands in for the thread.
	res, err := pool.With(currentCaller(), func(conn any) (any, error) {
		return conn.(*Conn).Query("SELECT 1"), nil
	})
	fmt.Println(res, err)

	// A Wrapper delegates any method straight through the pool, like the gem's:
	//   ConnectionPool::Wrapper.new(pool: pool)
	w := connectionpool.NewWrapper(pool, currentCaller, dispatch)
	_, _ = w.Call("query", "SELECT 1") // borrows, calls query, returns the conn

	pool.Shutdown(func(conn any) { conn.(*Conn).Close() }) // dispose every idle conn
}

API

type Factory func() any        // creates a connection (any value)
type CallerKey any             // identifies the logical caller (thread/fiber)

// ConnectionPool — models ConnectionPool
func New(size int, timeout time.Duration, factory Factory) *ConnectionPool
func (p *ConnectionPool) With(key CallerKey, fn func(conn any) (any, error)) (any, error) // with
func (p *ConnectionPool) Checkout(key CallerKey, timeout time.Duration) (any, error)      // checkout
func (p *ConnectionPool) Checkin(key CallerKey) error                                     // checkin
func (p *ConnectionPool) Shutdown(block func(conn any))                                    // shutdown
func (p *ConnectionPool) Reload(block func(conn any))                                      // reload
func (p *ConnectionPool) Size() int                                                       // size
func (p *ConnectionPool) Available() int                                                  // available

// Wrapper — models ConnectionPool::Wrapper (method delegation through the pool)
type Dispatch func(conn any, name string, args ...any) any // the host's `send`
type KeyFunc func() CallerKey
func NewWrapper(pool *ConnectionPool, keyFn KeyFunc, dispatch Dispatch) *Wrapper
func Wrap(size int, timeout time.Duration, factory Factory, keyFn KeyFunc, dispatch Dispatch) *Wrapper
func (w *Wrapper) Call(name string, args ...any) (any, error)              // method_missing
func (w *Wrapper) With(fn func(conn any) (any, error)) (any, error)        // with
func (w *Wrapper) RespondTo(name string, respond func(conn any) bool) (bool, error) // respond_to?
func (w *Wrapper) WrappedPool() *ConnectionPool                            // wrapped_pool
func (w *Wrapper) PoolShutdown(block func(conn any))                       // pool_shutdown
func (w *Wrapper) PoolSize() int                                          // pool_size
func (w *Wrapper) PoolAvailable() int                                    // pool_available

// TimedStack — models ConnectionPool::TimedStack (the bounded, timed backing store)
type TimedStack struct{ /* ... */ }
func NewTimedStack(size int, factory Factory) *TimedStack
func (s *TimedStack) Pop(timeout time.Duration) (any, error)
func (s *TimedStack) Push(obj any)
func (s *TimedStack) Shutdown(block func(conn any), reload bool)
func (s *TimedStack) Length() int
func (s *TimedStack) Max() int

// Errors — model the gem's hierarchy
type Error struct{ Msg string }                 // ConnectionPool::Error
type PoolShuttingDownError struct{}             // ConnectionPool::PoolShuttingDownError
type TimeoutError struct{ Msg string }          // ConnectionPool::TimeoutError

Semantics

  • Lazy, bounded creation. The factory runs on demand, never more than size times; Available() counts idle connections plus those not yet created.
  • Timeout. Checkout blocks up to the timeout for a free connection and returns a *TimeoutError once the deadline passes. A zero timeout fails immediately when the pool is exhausted.
  • Reentrancy. A nested Checkout/With on the same CallerKey reuses the same connection and only returns it to the pool on the outermost Checkin.
  • Shutdown / reload. Shutdown disposes every idle connection (LIFO) through the block and makes later checkouts fail with *PoolShuttingDownError; a connection still checked out is disposed when it is returned. Reload disposes the idle connections and resets the pool for reuse.
  • Thread-safe. A sync.Mutex + condition variable back the pool; the timeout machinery never leaks goroutines, and the suite runs under -race.

Tests & coverage

go test -race ./...

The suite holds 100% line coverage — including the timeout, exhaustion, shutdown, reload, and reentrancy paths — and cross-compiles on all six supported 64-bit targets (amd64, arm64, riscv64, loong64, ppc64le, s390x, including the big-endian s390x). A differential MRI oracle runs the real connection_pool gem side by side with this package on the lanes where Ruby and the gem are present; it skips itself elsewhere, so the deterministic, Ruby-free tests alone drive the coverage gate.

License

BSD-3-Clause — see LICENSE. Copyright the go-ruby-connection-pool/connection-pool authors.

WebAssembly

Being pure Go (CGO=0), this library also compiles to WebAssembly — both GOOS=js GOARCH=wasm (browser / Node.js) and GOOS=wasip1 GOARCH=wasm (WASI). CI builds both targets on every push, alongside the six 64-bit native/qemu arches.

GOOS=js     GOARCH=wasm go build ./...   # browser / Node
GOOS=wasip1 GOARCH=wasm go build ./...   # WASI (wasmtime, wasmer, wasmedge, …)

Documentation

Overview

Package connectionpool is a pure-Go (no cgo) reimplementation of Ruby's connection_pool gem (v2, MRI 4.0.5) — a generic, thread-safe pool of reusable connections. It mirrors the gem's observable behaviour — lazy creation up to a fixed size, a timeout-bounded checkout that raises ConnectionPool::TimeoutError on exhaustion, per-caller reentrant checkout, Shutdown/Reload disposal, and the method-delegating Wrapper — without any Ruby runtime. It is the connection_pool backend for go-embedded-ruby but is a standalone, reusable module.

Connections are opaque

A "connection" is any value the Factory returns; the pool never inspects it. Redis clients, database handles, or plain Go structs all work. The pool only tracks ownership and lifetime.

Reentrancy — the caller-key seam

The gem stores the checked-out connection in thread-local storage, so a nested `with` on the same thread reuses the same connection (and a depth counter decides when it truly returns to the pool). Go has no thread-locals, so the host identifies the logical caller with a CallerKey: two checkouts sharing a key nest exactly as two nested `with` blocks on one Ruby thread do. The go-embedded-ruby binding supplies the current Ruby thread/fiber as the key.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type CallerKey

type CallerKey any

CallerKey identifies the logical caller (a Ruby thread or fiber) for reentrant checkout. Any comparable value works. Two Checkout calls with an equal key nest — the second reuses the first's connection — and the connection returns to the pool only when the outermost Checkin runs. It stands in for the gem's per-thread storage.

type ConnectionPool

type ConnectionPool struct {
	// contains filtered or unexported fields
}

ConnectionPool is a thread-safe pool of reusable connections. The zero value is not usable; construct it with New.

func New

func New(size int, timeout time.Duration, factory Factory) *ConnectionPool

New builds a pool that lazily creates at most size connections through factory and blocks up to timeout in Checkout waiting for a free one. It mirrors ConnectionPool.new(size:, timeout:, &factory).

func (*ConnectionPool) Available

func (p *ConnectionPool) Available() int

Available reports how many connections can be checked out right now without waiting (idle plus not-yet-created). Mirrors ConnectionPool#available.

func (*ConnectionPool) Checkin

func (p *ConnectionPool) Checkin(key CallerKey) error

Checkin returns key's connection to the pool. On a nested checkout it only unwinds one level of the reentrancy counter; the connection returns to the pool when the outermost Checkin runs. It returns an *Error if key holds no connection. Mirrors ConnectionPool#checkin.

func (*ConnectionPool) Checkout

func (p *ConnectionPool) Checkout(key CallerKey, timeout time.Duration) (any, error)

Checkout returns a connection for key, blocking up to timeout. If key already holds a connection (a nested checkout on the same caller), it returns that same connection and deepens the reentrancy counter instead of taking another from the pool. Mirrors ConnectionPool#checkout.

func (*ConnectionPool) Reload

func (p *ConnectionPool) Reload(block func(conn any))

Reload disposes every idle connection through block and resets the pool for reuse, so it will create fresh connections on demand again. Mirrors ConnectionPool#reload.

func (*ConnectionPool) Shutdown

func (p *ConnectionPool) Shutdown(block func(conn any))

Shutdown disposes every idle connection through block and marks the pool shut down, so later checkouts fail and connections still checked out are disposed when returned. block must not be nil. Mirrors ConnectionPool#shutdown.

func (*ConnectionPool) Size

func (p *ConnectionPool) Size() int

Size reports the maximum number of connections the pool will create. Mirrors ConnectionPool#size.

func (*ConnectionPool) With

func (p *ConnectionPool) With(key CallerKey, fn func(conn any) (any, error)) (any, error)

With checks out a connection for key, runs fn with it, and checks it back in even if fn panics — the faithful analogue of ConnectionPool#with's ensure. It returns fn's result, or the *TimeoutError / *PoolShuttingDownError from an unsuccessful checkout (in which case fn does not run).

type Dispatch

type Dispatch func(conn any, name string, args ...any) any

Dispatch invokes method name with args on a connection and returns the result. It is the seam through which Wrapper delegates arbitrary calls to a pooled connection: the go-embedded-ruby binding plugs in Ruby's send, so a Wrapper behaves like the connection itself. Mirrors the connection.send(...) inside ConnectionPool::Wrapper#method_missing.

type Error

type Error struct{ Msg string }

Error is the base error type for the pool, mirroring ConnectionPool::Error (a RuntimeError subclass in the gem). Checkin on a caller that holds no connection raises it.

func (*Error) Error

func (e *Error) Error() string

type Factory

type Factory func() any

Factory creates a new connection. It mirrors the block passed to ConnectionPool.new / TimedStack.new: it is invoked lazily, at most Size times over the life of the stack, the first time a caller needs a connection that is not already parked in the stack.

type KeyFunc

type KeyFunc func() CallerKey

KeyFunc yields the CallerKey of the current logical caller. Wrapper calls it per delegation, exactly as the gem reads Thread.current inside pool.with.

type PoolShuttingDownError

type PoolShuttingDownError struct{}

PoolShuttingDownError mirrors ConnectionPool::PoolShuttingDownError. It is returned by Checkout when the pool has been shut down (a ConnectionPool::Error subclass in the gem).

func (*PoolShuttingDownError) Error

func (e *PoolShuttingDownError) Error() string

type TimedStack

type TimedStack struct {
	// contains filtered or unexported fields
}

TimedStack is a bounded, lazily-populated stack of connections with a timeout-bounded Pop. It is a faithful port of ConnectionPool::TimedStack: at most max connections are ever created (created on demand by the factory), Pop blocks up to the timeout waiting for a free connection and returns a *TimeoutError on exhaustion, and Shutdown disposes every connection and makes further Pops return a *PoolShuttingDownError.

func NewTimedStack

func NewTimedStack(size int, factory Factory) *TimedStack

NewTimedStack builds a stack that will create at most size connections through factory. A size of zero means the stack never creates a connection, so every Pop that finds the stack empty times out — matching the gem.

func (*TimedStack) Length

func (s *TimedStack) Length() int

Length reports how many connections could still be handed out without waiting: the parked ones plus those not yet created. Mirrors TimedStack#length, which backs ConnectionPool#available.

func (*TimedStack) Max

func (s *TimedStack) Max() int

Max reports the maximum number of connections the stack will create. Mirrors ConnectionPool#size.

func (*TimedStack) Pop

func (s *TimedStack) Pop(timeout time.Duration) (any, error)

Pop returns a connection, blocking up to timeout for one to become available. It returns a parked connection if any, otherwise creates a fresh one while the stack is below capacity, otherwise waits. It returns a *PoolShuttingDownError if the stack is shutting down and a *TimeoutError once the deadline passes. Mirrors TimedStack#pop.

func (*TimedStack) Push

func (s *TimedStack) Push(obj any)

Push returns obj to the stack, or hands it to the shutdown block if the stack is shutting down, then wakes a waiter. Mirrors TimedStack#push.

func (*TimedStack) Shutdown

func (s *TimedStack) Shutdown(block func(conn any), reload bool)

Shutdown disposes every parked connection through block and marks the stack as shutting down so waiters wake and future Pops fail. When reload is true it instead resets the stack for reuse (created count cleared, not shutting down), mirroring TimedStack#shutdown(reload:). block must not be nil.

type TimeoutError

type TimeoutError struct{ Msg string }

TimeoutError mirrors ConnectionPool::TimeoutError (a Timeout::Error subclass in the gem). It is returned by Checkout when no connection becomes available within the timeout.

func (*TimeoutError) Error

func (e *TimeoutError) Error() string

type Wrapper

type Wrapper struct {
	// contains filtered or unexported fields
}

Wrapper delegates method calls to a connection borrowed from a pool for the duration of each call. It is a faithful port of ConnectionPool::Wrapper: every delegated call checks a connection out, invokes the method, and checks it back in. Because delegation runs through With, nested delegated calls on the same caller reuse one connection, so a Wrapper is safe to treat as if it were a single connection.

func NewWrapper

func NewWrapper(pool *ConnectionPool, keyFn KeyFunc, dispatch Dispatch) *Wrapper

NewWrapper wraps pool so that Call delegates to a pooled connection. keyFn supplies the current caller key (for reentrancy) and dispatch performs the method invocation. Mirrors ConnectionPool::Wrapper.new(pool:).

func Wrap

func Wrap(size int, timeout time.Duration, factory Factory, keyFn KeyFunc, dispatch Dispatch) *Wrapper

Wrap builds a fresh pool and wraps it in one call, mirroring ConnectionPool.wrap(size:, timeout:) { factory }.

func (*Wrapper) Call

func (w *Wrapper) Call(name string, args ...any) (any, error)

Call borrows a connection and invokes method name with args on it, returning the method's result. It is the analogue of ConnectionPool::Wrapper#method_missing: any call the connection understands can be made straight on the Wrapper. It returns the checkout error (a *TimeoutError / *PoolShuttingDownError) if no connection is available.

func (*Wrapper) PoolAvailable

func (w *Wrapper) PoolAvailable() int

PoolAvailable reports the underlying pool's availability. Mirrors ConnectionPool::Wrapper#pool_available.

func (*Wrapper) PoolShutdown

func (w *Wrapper) PoolShutdown(block func(conn any))

PoolShutdown disposes the underlying pool through block. Mirrors ConnectionPool::Wrapper#pool_shutdown.

func (*Wrapper) PoolSize

func (w *Wrapper) PoolSize() int

PoolSize reports the underlying pool's size. Mirrors ConnectionPool::Wrapper#pool_size.

func (*Wrapper) RespondTo

func (w *Wrapper) RespondTo(name string, respond func(conn any) bool) (bool, error)

RespondTo reports whether a delegated call to name would resolve: either it is one of the Wrapper's own methods, or a borrowed connection responds to it (probed through respond). Mirrors ConnectionPool::Wrapper#respond_to?.

func (*Wrapper) With

func (w *Wrapper) With(fn func(conn any) (any, error)) (any, error)

With runs fn with a borrowed connection, exactly like the pool's With but using the Wrapper's caller-key source. Mirrors ConnectionPool::Wrapper#with.

func (*Wrapper) WrappedPool

func (w *Wrapper) WrappedPool() *ConnectionPool

WrappedPool returns the underlying pool. Mirrors ConnectionPool::Wrapper#wrapped_pool.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL