mirror of
https://github.com/hanzo-ds/go.git
synced 2026-10-30 19:45:01 +00:00
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.
418 lines
10 KiB
Go
418 lines
10 KiB
Go
package datastore
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"log/slog"
|
|
"math/rand"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
_ "time/tzdata"
|
|
|
|
"github.com/hanzo-ds/go/contributors"
|
|
"github.com/hanzo-ds/go/lib/column"
|
|
"github.com/hanzo-ds/go/lib/driver"
|
|
"github.com/hanzo-ds/go/lib/proto"
|
|
)
|
|
|
|
type Conn = driver.Conn
|
|
|
|
type (
|
|
Progress = proto.Progress
|
|
Exception = proto.Exception
|
|
ProfileInfo = proto.ProfileInfo
|
|
ServerVersion = proto.ServerHandshake
|
|
)
|
|
|
|
var (
|
|
ErrBatchInvalid = errors.New("datastore: batch is invalid. check appended data is correct")
|
|
ErrBatchAlreadySent = errors.New("datastore: batch has already been sent")
|
|
ErrBatchNotSent = errors.New("datastore: invalid retry, batch not sent yet")
|
|
ErrAcquireConnTimeout = errors.New("datastore: acquire conn timeout. you can increase the number of max open conn or the dial timeout")
|
|
ErrUnsupportedServerRevision = errors.New("datastore: unsupported server revision")
|
|
ErrBindMixedParamsFormats = errors.New("datastore [bind]: mixed named, numeric or positional parameters")
|
|
ErrAcquireConnNoAddress = errors.New("datastore: no valid address supplied")
|
|
ErrServerUnexpectedData = errors.New("code: 101, message: Unexpected packet Data received from client")
|
|
ErrConnectionClosed = errors.New("datastore: connection is closed")
|
|
)
|
|
|
|
type OpError struct {
|
|
Op string
|
|
ColumnName string
|
|
Err error
|
|
}
|
|
|
|
func (e *OpError) Error() string {
|
|
switch err := e.Err.(type) {
|
|
case *column.Error:
|
|
return fmt.Sprintf("datastore [%s]: (%s %s) %s", e.Op, e.ColumnName, err.ColumnType, err.Err)
|
|
case *column.ColumnConverterError:
|
|
var hint string
|
|
if len(err.Hint) != 0 {
|
|
hint += ". " + err.Hint
|
|
}
|
|
return fmt.Sprintf("datastore [%s]: (%s) converting %s to %s is unsupported%s",
|
|
err.Op, e.ColumnName,
|
|
err.From, err.To,
|
|
hint,
|
|
)
|
|
}
|
|
return fmt.Sprintf("datastore [%s]: %s", e.Op, e.Err)
|
|
}
|
|
|
|
func Open(opt *Options) (driver.Conn, error) {
|
|
if opt == nil {
|
|
opt = &Options{}
|
|
}
|
|
o := opt.setDefaults()
|
|
|
|
conn := &client{
|
|
opt: o,
|
|
idle: newConnPool(o.ConnMaxLifetime, o.MaxIdleConns),
|
|
open: make(chan struct{}, o.MaxOpenConns),
|
|
closeOnce: &sync.Once{},
|
|
closed: &atomic.Bool{},
|
|
}
|
|
|
|
return conn, nil
|
|
}
|
|
|
|
// nativeTransport represents an implementation (TCP or HTTP) that can be pooled by the main client struct.
|
|
// Implementations are not expected to be thread safe, which is why we provide acquire/release functions.
|
|
type nativeTransport interface {
|
|
serverVersion() (*ServerVersion, error)
|
|
query(ctx context.Context, release nativeTransportRelease, query string, args ...any) (*rows, error)
|
|
queryRow(ctx context.Context, release nativeTransportRelease, query string, args ...any) *row
|
|
prepareBatch(ctx context.Context, release nativeTransportRelease, acquire nativeTransportAcquire, query string, opts driver.PrepareBatchOptions) (driver.Batch, error)
|
|
exec(ctx context.Context, query string, args ...any) error
|
|
asyncInsert(ctx context.Context, query string, wait bool, args ...any) error
|
|
ping(context.Context) error
|
|
isBad() bool
|
|
connID() int
|
|
connectedAtTime() time.Time
|
|
isReleased() bool
|
|
setReleased(released bool)
|
|
getLogger() *slog.Logger
|
|
// freeBuffer is called if Options.FreeBufOnConnRelease is set
|
|
freeBuffer()
|
|
close() error
|
|
}
|
|
type nativeTransportAcquire func(context.Context) (nativeTransport, error)
|
|
type nativeTransportRelease func(nativeTransport, error)
|
|
|
|
// connectionPooler is an connection pool maintain
|
|
// idle connections.
|
|
type connectionPooler interface {
|
|
Get(ctx context.Context) (nativeTransport, error)
|
|
Put(conn nativeTransport)
|
|
Len() int
|
|
Cap() int
|
|
Close() error
|
|
}
|
|
|
|
type client struct {
|
|
opt *Options
|
|
connID int64
|
|
|
|
idle connectionPooler
|
|
open chan struct{}
|
|
|
|
closeOnce *sync.Once
|
|
closed *atomic.Bool
|
|
}
|
|
|
|
func (client) Contributors() []string {
|
|
list := contributors.List
|
|
if len(list[len(list)-1]) == 0 {
|
|
return list[:len(list)-1]
|
|
}
|
|
return list
|
|
}
|
|
|
|
func (c *client) ServerVersion() (*driver.ServerVersion, error) {
|
|
var (
|
|
ctx, cancel = context.WithTimeout(context.Background(), c.opt.DialTimeout)
|
|
conn, err = c.acquire(ctx)
|
|
)
|
|
defer cancel()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer c.release(conn, nil)
|
|
return conn.serverVersion()
|
|
}
|
|
|
|
func (c *client) Query(ctx context.Context, query string, args ...any) (rows driver.Rows, err error) {
|
|
conn, err := c.acquire(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
conn.getLogger().Debug("executing query", slog.String("sql", query))
|
|
return conn.query(ctx, c.release, query, args...)
|
|
}
|
|
|
|
func (c *client) QueryRow(ctx context.Context, query string, args ...any) driver.Row {
|
|
conn, err := c.acquire(ctx)
|
|
if err != nil {
|
|
return &row{
|
|
err: err,
|
|
}
|
|
}
|
|
|
|
conn.getLogger().Debug("executing query row", slog.String("sql", query))
|
|
return conn.queryRow(ctx, c.release, query, args...)
|
|
}
|
|
|
|
func (c *client) Exec(ctx context.Context, query string, args ...any) error {
|
|
conn, err := c.acquire(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
conn.getLogger().Debug("executing statement", slog.String("sql", query))
|
|
|
|
if asyncOpt := queryOptionsAsync(ctx); asyncOpt.ok {
|
|
err = conn.asyncInsert(ctx, query, asyncOpt.wait, args...)
|
|
} else {
|
|
err = conn.exec(ctx, query, args...)
|
|
}
|
|
|
|
if err != nil {
|
|
c.release(conn, err)
|
|
return err
|
|
}
|
|
|
|
c.release(conn, nil)
|
|
return nil
|
|
}
|
|
|
|
func (c *client) PrepareBatch(ctx context.Context, query string, opts ...driver.PrepareBatchOption) (driver.Batch, error) {
|
|
conn, err := c.acquire(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
conn.getLogger().Debug("preparing batch", slog.String("sql", query))
|
|
batch, err := conn.prepareBatch(ctx, c.release, c.acquire, query, getPrepareBatchOptions(opts...))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return batch, nil
|
|
}
|
|
|
|
func getPrepareBatchOptions(opts ...driver.PrepareBatchOption) driver.PrepareBatchOptions {
|
|
var options driver.PrepareBatchOptions
|
|
|
|
for _, opt := range opts {
|
|
opt(&options)
|
|
}
|
|
|
|
return options
|
|
}
|
|
|
|
// Deprecated: use context aware `WithAsync()` for any async operations
|
|
func (c *client) AsyncInsert(ctx context.Context, query string, wait bool, args ...any) error {
|
|
conn, err := c.acquire(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
conn.getLogger().Debug("async insert", slog.String("sql", query), slog.Bool("wait", wait))
|
|
if err := conn.asyncInsert(ctx, query, wait, args...); err != nil {
|
|
c.release(conn, err)
|
|
return err
|
|
}
|
|
c.release(conn, nil)
|
|
return nil
|
|
}
|
|
|
|
func (c *client) Ping(ctx context.Context) (err error) {
|
|
conn, err := c.acquire(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
conn.getLogger().Debug("ping")
|
|
if err := conn.ping(ctx); err != nil {
|
|
c.release(conn, err)
|
|
return err
|
|
}
|
|
c.release(conn, nil)
|
|
return nil
|
|
}
|
|
|
|
func (c *client) Stats() driver.Stats {
|
|
return driver.Stats{
|
|
Open: len(c.open),
|
|
MaxOpenConns: cap(c.open),
|
|
|
|
Idle: c.idle.Len(),
|
|
MaxIdleConns: c.idle.Cap(),
|
|
}
|
|
}
|
|
|
|
func (c *client) dial(ctx context.Context) (conn nativeTransport, err error) {
|
|
if err := ctx.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
connID := int(atomic.AddInt64(&c.connID, 1))
|
|
|
|
dialFunc := func(ctx context.Context, addr string, opt *Options) (DialResult, error) {
|
|
var conn nativeTransport
|
|
var err error
|
|
switch opt.Protocol {
|
|
case HTTP:
|
|
conn, err = dialHttp(ctx, addr, connID, opt)
|
|
default:
|
|
conn, err = dial(ctx, addr, connID, opt)
|
|
}
|
|
|
|
return DialResult{conn}, err
|
|
}
|
|
|
|
dialStrategy := DefaultDialStrategy
|
|
if c.opt.DialStrategy != nil {
|
|
dialStrategy = c.opt.DialStrategy
|
|
}
|
|
|
|
result, err := dialStrategy(ctx, connID, c.opt, dialFunc)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return result.conn, nil
|
|
}
|
|
|
|
func DefaultDialStrategy(ctx context.Context, connID int, opt *Options, dial Dial) (r DialResult, err error) {
|
|
for i := range opt.Addr {
|
|
var num int
|
|
switch opt.ConnOpenStrategy {
|
|
case ConnOpenInOrder:
|
|
num = i
|
|
case ConnOpenRoundRobin:
|
|
num = (connID + i) % len(opt.Addr)
|
|
case ConnOpenRandom:
|
|
random := rand.Int()
|
|
num = (random + i) % len(opt.Addr)
|
|
}
|
|
|
|
if r, err = dial(ctx, opt.Addr[num], opt); err == nil {
|
|
return r, nil
|
|
}
|
|
}
|
|
|
|
if err == nil {
|
|
err = ErrAcquireConnNoAddress
|
|
}
|
|
|
|
return r, err
|
|
}
|
|
|
|
func (c *client) acquire(ctx context.Context) (conn nativeTransport, err error) {
|
|
if c.closed.Load() {
|
|
return nil, ErrConnectionClosed
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeoutCause(ctx, c.opt.DialTimeout, ErrAcquireConnTimeout)
|
|
defer cancel()
|
|
|
|
// If context is already cancelled, just return without any work
|
|
// done this way with single case with default. Otherwise if both ctx is cancelled and c.open is ready,
|
|
// Go would choose one of those at random, thus missing to return deterministically when context is cancelled
|
|
// at this point in time.
|
|
// Known pattern: https://go.dev/ref/spec#Select_statements
|
|
select {
|
|
case <-ctx.Done():
|
|
return nil, context.Cause(ctx)
|
|
default:
|
|
}
|
|
|
|
select {
|
|
case c.open <- struct{}{}:
|
|
case <-ctx.Done():
|
|
return nil, context.Cause(ctx)
|
|
}
|
|
|
|
conn, err = c.idle.Get(ctx)
|
|
if err != nil && !errors.Is(err, errQueueEmpty) {
|
|
select {
|
|
case <-c.open:
|
|
default:
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
if err == nil && conn != nil {
|
|
if !conn.isBad() {
|
|
conn.setReleased(false)
|
|
conn.getLogger().Debug("connection acquired from pool")
|
|
return conn, nil
|
|
}
|
|
|
|
conn.close()
|
|
}
|
|
|
|
if conn, err = c.dial(ctx); err != nil {
|
|
select {
|
|
case <-c.open:
|
|
default:
|
|
}
|
|
|
|
return nil, err
|
|
}
|
|
|
|
conn.getLogger().Debug("new connection established")
|
|
return conn, nil
|
|
|
|
}
|
|
|
|
func (c *client) release(conn nativeTransport, err error) {
|
|
if conn.isReleased() {
|
|
return
|
|
}
|
|
conn.setReleased(true)
|
|
|
|
if err != nil {
|
|
conn.getLogger().Debug("connection released with error", slog.Any("error", err))
|
|
} else {
|
|
conn.getLogger().Debug("connection released to pool")
|
|
}
|
|
|
|
select {
|
|
case <-c.open:
|
|
default:
|
|
}
|
|
|
|
if err != nil {
|
|
conn.getLogger().Debug("connection closed due to error", slog.Any("error", err))
|
|
conn.close()
|
|
return
|
|
} else if time.Since(conn.connectedAtTime()) >= c.opt.ConnMaxLifetime {
|
|
conn.getLogger().Debug("connection closed: lifetime expired",
|
|
slog.Duration("age", time.Since(conn.connectedAtTime())),
|
|
slog.Duration("max_lifetime", c.opt.ConnMaxLifetime))
|
|
conn.close()
|
|
return
|
|
}
|
|
|
|
if c.opt.FreeBufOnConnRelease {
|
|
conn.getLogger().Debug("freeing connection buffer")
|
|
conn.freeBuffer()
|
|
}
|
|
|
|
if c.closed.Load() {
|
|
conn.close()
|
|
return
|
|
}
|
|
|
|
c.idle.Put(conn)
|
|
}
|
|
|
|
func (c *client) Close() (err error) {
|
|
c.closeOnce.Do(func() {
|
|
err = c.idle.Close()
|
|
c.closed.Store(true)
|
|
})
|
|
|
|
return
|
|
}
|