Files
go/conn_pool.go
zeekay 0c90ebe02e debrand: finish the rename across source, docs and dev infra
Source: the unexported clickhouse struct becomes client, the ch receiver and
chproto import alias follow, error message prefixes read datastore, and the
JSON serializer interface drops the vendor infix. resources' metadata key and
its accessor are renamed together, with a test proving the embedded YAML still
unmarshals — a silent drift there would have made every connection take the
unsupported-server-version branch.

Codegen: all three templates still emitted the upstream module paths, so
regenerating would have quietly undone the rename. Note the templates have
drifted from the files they produce (regenerating today drops QBit and
scanTypeDuration), so the generated files are left as tracked and only the
templates are corrected.

Dead links: an earlier mechanical pass invented URLs that do not resolve —
github.com/ClickHouse/datastore-go, hanzo-ds/driver issue numbers that belong to
upstream, and mangled names of independent projects (mailru, uptrace, nikepan,
EpicStep). Third-party names are not ours to rewrite, so those directories are
removed rather than repaired, and provenance comments now point at real files.

Dev infra: compose runs our own server image at the tag production runs, since
the vendor's alpine tag does not exist in our registry. Cluster hostnames,
nginx upstreams and server configs are renamed as one set.

CHANGELOG.md is upstream's release history for versions this module never
released; rewriting it would fabricate a lineage, so it is dropped and the
upstream base recorded in README instead.

The two long-standing vet findings are fixed by keying the struct literals
rather than suppressed. LICENSE and contributors/list keep the vendor name on
purpose: one is the copyright notice Apache-2.0 requires, the other is real
people's names. CI now enforces build, vet, test, gofmt and a grep gate.
2026-07-26 11:46:39 -07:00

180 lines
3.2 KiB
Go

package datastore
import (
"context"
"errors"
"sync"
"time"
"github.com/hanzo-ds/go/internal/circular"
)
var errQueueEmpty = errors.New("datastore: connection pool queue is empty")
type connPool struct {
mu sync.RWMutex
conns *circular.Queue[nativeTransport]
ticker *time.Ticker
finish chan struct{}
finished chan struct{}
maxConnLifetime time.Duration
}
func newConnPool(lifetime time.Duration, capacity int) *connPool {
pool := &connPool{
conns: circular.New[nativeTransport](capacity),
ticker: time.NewTicker(lifetime),
finish: make(chan struct{}),
finished: make(chan struct{}),
maxConnLifetime: lifetime,
}
go pool.runDrainPool()
return pool
}
func (i *connPool) Len() int {
i.mu.RLock()
defer i.mu.RUnlock()
return i.conns.Len()
}
func (i *connPool) Cap() int {
i.mu.RLock()
defer i.mu.RUnlock()
return i.conns.Cap()
}
func (i *connPool) Get(ctx context.Context) (nativeTransport, error) {
i.mu.Lock()
defer i.mu.Unlock()
// check if pool was closed while we waited on the lock
// return early if pool already closed
// otherwise, pool wont close again while we hold lock
// and so we continue
if i.closed() {
return nil, ErrConnectionClosed
}
// this loop continues until either:
// a) the provided context is cancelled
// b) the underlying circular queue is empty
// c) it finds a non-expired connection
for {
if err := ctx.Err(); err != nil {
// context has been cancelled
return nil, context.Cause(ctx)
}
// Try to pull a connection
conn, ok := i.conns.Pull()
if !ok {
return nil, errQueueEmpty // queue is empty
}
if !i.isExpired(conn) {
return conn, nil
}
conn.close()
}
}
func (i *connPool) Put(conn nativeTransport) {
if i.isExpired(conn) || conn.isBad() {
conn.close()
return
}
i.mu.Lock()
defer i.mu.Unlock()
if i.closed() {
return
}
// Try to push the connection
if !i.conns.Push(conn) {
// Buffer is full, close the connection
conn.close()
}
}
func (i *connPool) Close() error {
i.mu.Lock()
defer i.mu.Unlock()
if i.closed() {
return nil
}
close(i.finish)
<-i.finished
// Drain all remaining connections from the pool
i.drainPool()
return nil
}
func (i *connPool) closed() bool {
select {
case <-i.finished:
return true
default:
return false
}
}
func (i *connPool) runDrainPool() {
defer func() {
i.ticker.Stop()
close(i.finished)
}()
for {
select {
case <-i.ticker.C:
i.mu.Lock()
i.drainPool()
i.mu.Unlock()
case <-i.finish:
return
}
}
}
// drainPool removes connections from the pool.
// If the pool is closed, it removes all connections.
// Otherwise, it only removes expired connections.
// Must be called with i.mu held.
func (i *connPool) drainPool() {
if i.closed() {
// Close all connections
for conn := range i.conns.Clear() {
conn.close()
}
return
}
// Remove only expired connections
for conn := range i.conns.DeleteFunc(func(conn nativeTransport) bool {
return i.isExpired(conn)
}) {
conn.close()
}
}
func (i *connPool) isExpired(conn nativeTransport) bool {
return !time.Now().Before(i.expires(conn))
}
func (i *connPool) expires(conn nativeTransport) time.Time {
return conn.connectedAtTime().Add(i.maxConnLifetime)
}