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.
472 lines
12 KiB
Go
472 lines
12 KiB
Go
package datastore
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"errors"
|
|
"fmt"
|
|
"github.com/hanzo-ds/go/lib/column"
|
|
"io"
|
|
"log/slog"
|
|
"net"
|
|
"sync"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/hanzo-ds/go/resources"
|
|
|
|
"github.com/hanzo-ds/go/lib/proto"
|
|
"github.com/hanzo-ds/native/compress"
|
|
nativeproto "github.com/hanzo-ds/native/proto"
|
|
)
|
|
|
|
func dial(ctx context.Context, addr string, num int, opt *Options) (*connect, error) {
|
|
var (
|
|
err error
|
|
conn net.Conn
|
|
)
|
|
|
|
switch {
|
|
case opt.DialContext != nil:
|
|
conn, err = opt.DialContext(ctx, addr)
|
|
default:
|
|
switch {
|
|
case opt.TLS != nil:
|
|
conn, err = tls.DialWithDialer(&net.Dialer{Timeout: opt.DialTimeout}, "tcp", addr, opt.TLS)
|
|
default:
|
|
conn, err = net.DialTimeout("tcp", addr, opt.DialTimeout)
|
|
}
|
|
}
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Get base logger and enrich with connection-specific context
|
|
baseLogger := opt.logger()
|
|
logger := prepareConnLogger(baseLogger, num, conn.RemoteAddr().String(), "native")
|
|
|
|
var (
|
|
compression CompressionMethod
|
|
compressor *compress.Writer
|
|
)
|
|
if opt.Compression != nil {
|
|
switch opt.Compression.Method {
|
|
case CompressionLZ4, CompressionLZ4HC, CompressionZSTD, CompressionNone:
|
|
compression = opt.Compression.Method
|
|
default:
|
|
return nil, fmt.Errorf("unsupported compression method for native protocol")
|
|
}
|
|
|
|
compressor = compress.NewWriter(compress.Level(opt.Compression.Level), compress.Method(opt.Compression.Method))
|
|
} else {
|
|
compression = CompressionNone
|
|
compressor = compress.NewWriter(compress.LevelZero, compress.None)
|
|
}
|
|
|
|
var (
|
|
connect = &connect{
|
|
id: num,
|
|
opt: opt,
|
|
conn: conn,
|
|
logger: logger,
|
|
buffer: new(nativeproto.Buffer),
|
|
reader: nativeproto.NewReader(conn),
|
|
revision: ClientTCPProtocolVersion,
|
|
structMap: &structMap{},
|
|
compression: compression,
|
|
connectedAt: time.Now(),
|
|
compressor: compressor,
|
|
readTimeout: opt.ReadTimeout,
|
|
blockBufferSize: opt.BlockBufferSize,
|
|
maxCompressionBuffer: opt.MaxCompressionBuffer,
|
|
}
|
|
)
|
|
|
|
auth := opt.Auth
|
|
if useJWTAuth(opt) {
|
|
jwt, err := opt.GetJWT(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get JWT: %w", err)
|
|
}
|
|
|
|
auth.Username = jwtAuthMarker
|
|
auth.Password = jwt
|
|
}
|
|
|
|
if err := connect.handshake(auth); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if connect.revision >= proto.DBMS_MIN_PROTOCOL_VERSION_WITH_ADDENDUM {
|
|
if err := connect.sendAddendum(); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// warn only on the first connection in the pool
|
|
if num == 1 && !resources.ClientMeta.IsSupportedServerVersion(connect.server.Version) {
|
|
connect.logger.Warn("unsupported datastore version",
|
|
slog.String("version", connect.server.Version.String()),
|
|
slog.String("supported_versions", resources.ClientMeta.SupportedVersions()))
|
|
}
|
|
|
|
return connect, nil
|
|
}
|
|
|
|
// https://github.com/Datastore/Datastore/blob/master/src/Client/Connection.cpp
|
|
type connect struct {
|
|
id int
|
|
opt *Options
|
|
conn net.Conn
|
|
logger *slog.Logger
|
|
server ServerVersion
|
|
closed bool
|
|
buffer *nativeproto.Buffer
|
|
reader *nativeproto.Reader
|
|
released bool
|
|
revision uint64
|
|
structMap *structMap
|
|
compression CompressionMethod
|
|
connectedAt time.Time
|
|
compressor *compress.Writer
|
|
readTimeout time.Duration
|
|
blockBufferSize uint8
|
|
maxCompressionBuffer int
|
|
readerMutex sync.Mutex
|
|
closeMutex sync.Mutex
|
|
}
|
|
|
|
func (c *connect) connID() int {
|
|
return c.id
|
|
}
|
|
|
|
func (c *connect) getLogger() *slog.Logger {
|
|
return c.logger
|
|
}
|
|
|
|
func (c *connect) connectedAtTime() time.Time {
|
|
return c.connectedAt
|
|
}
|
|
|
|
func (c *connect) serverVersion() (*ServerVersion, error) {
|
|
return &c.server, nil
|
|
}
|
|
|
|
func (c *connect) settings(querySettings Settings) []proto.Setting {
|
|
settingToProtoSetting := func(k string, v any) proto.Setting {
|
|
isCustom := false
|
|
if cv, ok := v.(CustomSetting); ok {
|
|
v = cv.Value
|
|
isCustom = true
|
|
}
|
|
|
|
return proto.Setting{
|
|
Key: k,
|
|
Value: v,
|
|
Important: !isCustom,
|
|
Custom: isCustom,
|
|
}
|
|
}
|
|
|
|
settings := make([]proto.Setting, 0, len(c.opt.Settings)+len(querySettings))
|
|
for k, v := range c.opt.Settings {
|
|
settings = append(settings, settingToProtoSetting(k, v))
|
|
}
|
|
|
|
for k, v := range querySettings {
|
|
settings = append(settings, settingToProtoSetting(k, v))
|
|
}
|
|
|
|
return settings
|
|
}
|
|
|
|
func (c *connect) isBad() bool {
|
|
if c.isClosed() {
|
|
return true
|
|
}
|
|
|
|
if time.Since(c.connectedAt) >= c.opt.ConnMaxLifetime {
|
|
return true
|
|
}
|
|
|
|
if err := c.connCheck(); err != nil {
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func (c *connect) isReleased() bool {
|
|
return c.released
|
|
}
|
|
|
|
func (c *connect) setReleased(released bool) {
|
|
c.released = released
|
|
}
|
|
|
|
func (c *connect) isClosed() bool {
|
|
c.closeMutex.Lock()
|
|
defer c.closeMutex.Unlock()
|
|
|
|
return c.closed
|
|
}
|
|
|
|
func (c *connect) setClosed() {
|
|
c.closeMutex.Lock()
|
|
defer c.closeMutex.Unlock()
|
|
|
|
c.closed = true
|
|
}
|
|
|
|
func (c *connect) close() error {
|
|
c.closeMutex.Lock()
|
|
if c.closed {
|
|
c.closeMutex.Unlock()
|
|
return nil
|
|
}
|
|
c.closed = true
|
|
c.closeMutex.Unlock()
|
|
|
|
if err := c.conn.Close(); err != nil {
|
|
return err
|
|
}
|
|
|
|
c.buffer = nil
|
|
c.compressor = nil
|
|
|
|
c.readerMutex.Lock()
|
|
c.reader = nil
|
|
c.readerMutex.Unlock()
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *connect) progress() (*Progress, error) {
|
|
var progress proto.Progress
|
|
if err := progress.Decode(c.reader, c.revision); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
c.logger.Debug("query progress",
|
|
slog.Uint64("rows", progress.Rows),
|
|
slog.Uint64("bytes", progress.Bytes),
|
|
slog.Uint64("total_rows", progress.TotalRows))
|
|
return &progress, nil
|
|
}
|
|
|
|
func (c *connect) exception() error {
|
|
var e Exception
|
|
if err := e.Decode(c.reader); err != nil {
|
|
return err
|
|
}
|
|
|
|
c.logger.Warn("server exception received",
|
|
slog.String("error", e.Error()),
|
|
slog.Int("code", int(e.Code)))
|
|
return &e
|
|
}
|
|
|
|
func (c *connect) compressBuffer(start int) error {
|
|
if c.compression != CompressionNone && len(c.buffer.Buf) > 0 {
|
|
data := c.buffer.Buf[start:]
|
|
if err := c.compressor.Compress(data); err != nil {
|
|
return fmt.Errorf("compress: %w", err)
|
|
}
|
|
c.buffer.Buf = append(c.buffer.Buf[:start], c.compressor.Data...)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *connect) sendData(block *proto.Block, name string) error {
|
|
if c.isClosed() {
|
|
err := errors.New("attempted sending on closed connection")
|
|
c.logger.Error("send data failed: connection closed", slog.Any("error", err))
|
|
return err
|
|
}
|
|
|
|
c.logger.Debug("sending data block",
|
|
slog.String("compression", c.compression.String()),
|
|
slog.Int("columns", len(block.Columns)),
|
|
slog.Int("rows", block.Rows()))
|
|
c.buffer.PutByte(proto.ClientData)
|
|
c.buffer.PutString(name)
|
|
|
|
compressionOffset := len(c.buffer.Buf)
|
|
|
|
if err := block.EncodeHeader(c.buffer, c.revision); err != nil {
|
|
return fmt.Errorf("send data: failed to encode block header (conn_id=%d): %w", c.id, err)
|
|
}
|
|
|
|
for i := range block.Columns {
|
|
if err := block.EncodeColumn(c.buffer, c.revision, i); err != nil {
|
|
return fmt.Errorf("send data: failed to encode column %d (conn_id=%d): %w", i, c.id, err)
|
|
}
|
|
if len(c.buffer.Buf) >= c.maxCompressionBuffer {
|
|
if err := c.compressBuffer(compressionOffset); err != nil {
|
|
return err
|
|
}
|
|
c.logger.Debug("buffer compressed",
|
|
slog.Int("buffer_bytes", len(c.buffer.Buf)))
|
|
if err := c.flush(); err != nil {
|
|
return fmt.Errorf("send data: failed to flush partial block (conn_id=%d, col=%d): %w", c.id, i, err)
|
|
}
|
|
compressionOffset = 0
|
|
}
|
|
}
|
|
|
|
if err := c.compressBuffer(compressionOffset); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := c.flush(); err != nil {
|
|
switch {
|
|
case errors.Is(err, syscall.EPIPE):
|
|
c.logger.Error("connection broken: pipe error",
|
|
slog.Any("error", err),
|
|
slog.Int("block_columns", len(block.Columns)),
|
|
slog.Int("block_rows", block.Rows()))
|
|
c.setClosed()
|
|
return fmt.Errorf("send data: connection broken (EPIPE) to %s (conn_id=%d, block_cols=%d, block_rows=%d): %w",
|
|
c.conn.RemoteAddr(), c.id, len(block.Columns), block.Rows(), err)
|
|
case errors.Is(err, io.EOF):
|
|
c.logger.Error("connection closed unexpectedly",
|
|
slog.Any("error", err),
|
|
slog.Int("block_columns", len(block.Columns)),
|
|
slog.Int("block_rows", block.Rows()))
|
|
c.setClosed()
|
|
return fmt.Errorf("send data: unexpected EOF to %s (conn_id=%d, block_cols=%d, block_rows=%d): %w",
|
|
c.conn.RemoteAddr(), c.id, len(block.Columns), block.Rows(), err)
|
|
default:
|
|
c.logger.Error("send data failed",
|
|
slog.Any("error", err),
|
|
slog.Int("block_columns", len(block.Columns)),
|
|
slog.Int("block_rows", block.Rows()))
|
|
return fmt.Errorf("send data: write error to %s (conn_id=%d, block_cols=%d, block_rows=%d): %w",
|
|
c.conn.RemoteAddr(), c.id, len(block.Columns), block.Rows(), err)
|
|
}
|
|
}
|
|
|
|
defer func() {
|
|
c.buffer.Reset()
|
|
}()
|
|
|
|
return nil
|
|
}
|
|
|
|
func serverVersionToContext(v ServerVersion) column.ServerContext {
|
|
return column.ServerContext{
|
|
Revision: v.Revision,
|
|
VersionMajor: v.Version.Major,
|
|
VersionMinor: v.Version.Minor,
|
|
VersionPatch: v.Version.Patch,
|
|
Timezone: v.Timezone,
|
|
}
|
|
}
|
|
|
|
func (c *connect) readData(ctx context.Context, packet byte, compressible bool) (*proto.Block, error) {
|
|
if c.isClosed() {
|
|
err := errors.New("attempted reading on closed connection")
|
|
c.logger.Error("read data failed: connection closed", slog.Any("error", err))
|
|
return nil, err
|
|
}
|
|
|
|
if c.reader == nil {
|
|
err := errors.New("attempted reading on nil reader")
|
|
c.logger.Error("read data failed: nil reader", slog.Any("error", err))
|
|
return nil, err
|
|
}
|
|
|
|
if _, err := c.reader.Str(); err != nil {
|
|
c.logger.Error("read data failed: cannot read block name", slog.Any("error", err))
|
|
return nil, fmt.Errorf("read data: failed to read block name from %s (conn_id=%d): %w",
|
|
c.conn.RemoteAddr(), c.id, err)
|
|
}
|
|
|
|
if compressible && c.compression != CompressionNone {
|
|
c.reader.EnableCompression()
|
|
defer c.reader.DisableCompression()
|
|
}
|
|
|
|
userLocation := queryOptionsUserLocation(ctx)
|
|
location := c.server.Timezone
|
|
if userLocation != nil {
|
|
location = userLocation
|
|
}
|
|
|
|
serverContext := serverVersionToContext(c.server)
|
|
serverContext.Timezone = location
|
|
block := proto.Block{ServerContext: &serverContext}
|
|
if err := block.Decode(c.reader, c.revision); err != nil {
|
|
c.logger.Error("read data failed: decode error",
|
|
slog.Any("error", err),
|
|
slog.String("compression", c.compression.String()))
|
|
return nil, fmt.Errorf("read data: failed to decode block from %s (conn_id=%d, compression=%s): %w",
|
|
c.conn.RemoteAddr(), c.id, c.compression, err)
|
|
}
|
|
|
|
block.Packet = packet
|
|
c.logger.Debug("data block received",
|
|
slog.String("compression", c.compression.String()),
|
|
slog.Int("columns", len(block.Columns)),
|
|
slog.Int("rows", block.Rows()))
|
|
return &block, nil
|
|
}
|
|
|
|
func (c *connect) freeBuffer() {
|
|
c.buffer = new(nativeproto.Buffer)
|
|
c.compressor.Data = nil
|
|
}
|
|
|
|
func (c *connect) flush() error {
|
|
if len(c.buffer.Buf) == 0 {
|
|
// Nothing to flush.
|
|
return nil
|
|
}
|
|
|
|
n, err := c.conn.Write(c.buffer.Buf)
|
|
if err != nil {
|
|
return fmt.Errorf("write: %w", err)
|
|
}
|
|
|
|
if n != len(c.buffer.Buf) {
|
|
return errors.New("wrote less than expected")
|
|
}
|
|
|
|
c.buffer.Reset()
|
|
return nil
|
|
}
|
|
|
|
// startReadWriteTimeout applies the configured read timeout to conn.
|
|
// If a context deadline is provided, a read and write deadline is set.
|
|
// This should be matched with a deferred call to clearReadWriteTimeout.
|
|
func (c *connect) startReadWriteTimeout(ctx context.Context) error {
|
|
err := c.conn.SetReadDeadline(time.Now().Add(c.readTimeout))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// context level deadlines override configured read timeout
|
|
if deadline, ok := ctx.Deadline(); ok {
|
|
return c.conn.SetDeadline(deadline)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// clearReadWriteTimeout removes the read timeout from conn.
|
|
// If a context deadline is provided, the read and write timeout is cleared too.
|
|
func (c *connect) clearReadWriteTimeout(ctx context.Context) error {
|
|
err := c.conn.SetReadDeadline(time.Time{})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// context level deadlines should clear read + write deadlines.
|
|
if _, ok := ctx.Deadline(); ok {
|
|
return c.conn.SetDeadline(time.Time{})
|
|
}
|
|
|
|
return nil
|
|
}
|