mirror of
https://github.com/hanzo-ds/native.git
synced 2026-10-30 20:00:34 +00:00
proto.Name is sent in ClientHello and lands in the server's system.query_log, so it is wire visible; it now reads datastore/native and is covered by a test. Verified against the live server: the handshake succeeds and the new name appears in query_log. goimports' local-prefixes still named the upstream module, so it was not grouping this module's own imports. Doc comments, the deprecated-error markers in proto/error_codes.go and the package prose are swept. Dead references are removed rather than repointed: go-faster.org/docs/clickhouse and the mangled Datastore/clickhouse-cpp link both 404, and the list of other vendors' clients does not belong here. Four things deliberately keep the vendor name, and CI records why: LICENSE and AUTHORS are the copyright notices Apache-2.0 requires; db.system in query.go is an OpenTelemetry enum whose registered value for this DBMS is fixed by the specification, so emitting anything else would be non-conformant; and README names clickhouse-local, which is genuinely the binary our own server image ships. Numeric error codes and protocol revision constants are untouched.
628 lines
15 KiB
Go
628 lines
15 KiB
Go
package native
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/go-faster/errors"
|
|
"go.opentelemetry.io/otel"
|
|
"go.opentelemetry.io/otel/metric"
|
|
semconv "go.opentelemetry.io/otel/semconv/v1.7.0"
|
|
"go.opentelemetry.io/otel/trace"
|
|
"go.uber.org/zap"
|
|
cryptossh "golang.org/x/crypto/ssh"
|
|
|
|
"github.com/hanzo-ds/native/compress"
|
|
pkgVersion "github.com/hanzo-ds/native/internal/version"
|
|
"github.com/hanzo-ds/native/proto"
|
|
"github.com/hanzo-ds/native/tracing"
|
|
)
|
|
|
|
// Client implements Datastore binary protocol client on top of
|
|
// single TCP connection.
|
|
type Client struct {
|
|
lg *zap.Logger
|
|
conn net.Conn
|
|
writer *proto.Writer
|
|
reader *proto.Reader
|
|
info proto.ClientHello
|
|
server proto.ServerHello
|
|
version clientVersion
|
|
quotaKey string
|
|
|
|
mux sync.Mutex
|
|
closed bool
|
|
|
|
// Single packet read timeout.
|
|
readTimeout time.Duration
|
|
|
|
otel bool
|
|
tracer trace.Tracer
|
|
meter metric.Meter
|
|
|
|
// TCP Binary protocol version.
|
|
protocolVersion int
|
|
|
|
// compressor performs block compression,
|
|
// see encodeBlock.
|
|
compressor *compress.Writer
|
|
compression proto.Compression
|
|
|
|
settings []Setting
|
|
|
|
// SSH authentication
|
|
sshSigner cryptossh.Signer
|
|
}
|
|
|
|
// Setting to send to server.
|
|
type Setting struct {
|
|
Key, Value string
|
|
Important bool
|
|
}
|
|
|
|
// SettingInt returns Setting with integer value v.
|
|
func SettingInt(k string, v int) Setting {
|
|
return Setting{
|
|
Key: k,
|
|
Value: strconv.Itoa(v),
|
|
Important: true,
|
|
}
|
|
}
|
|
|
|
// ServerInfo returns server information.
|
|
func (c *Client) ServerInfo() proto.ServerHello { return c.server }
|
|
|
|
// ErrClosed means that client was already closed.
|
|
var ErrClosed = errors.New("client is closed")
|
|
|
|
// Close closes underlying connection and frees all resources,
|
|
// rendering Client to unusable state.
|
|
func (c *Client) Close() error {
|
|
c.mux.Lock()
|
|
defer c.mux.Unlock()
|
|
|
|
if c.closed {
|
|
return ErrClosed
|
|
}
|
|
|
|
c.closed = true
|
|
if err := c.conn.Close(); err != nil {
|
|
return errors.Wrap(err, "conn")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// IsClosed indicates that connection is closed.
|
|
func (c *Client) IsClosed() bool {
|
|
c.mux.Lock()
|
|
defer c.mux.Unlock()
|
|
return c.closed
|
|
}
|
|
|
|
// Exception is server-side error.
|
|
type Exception struct {
|
|
Code proto.Error
|
|
Name string
|
|
Message string
|
|
Stack string
|
|
Next []Exception // non-nil only for top exception
|
|
}
|
|
|
|
func (e *Exception) IsCode(codes ...proto.Error) bool {
|
|
if e == nil || len(codes) == 0 {
|
|
return false
|
|
}
|
|
for _, c := range codes {
|
|
if e.Code == c {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (e *Exception) Error() string {
|
|
msg := strings.TrimPrefix(e.Message, e.Name+":")
|
|
msg = strings.TrimSpace(msg)
|
|
return fmt.Sprintf("%s: %s: %s", e.Code, e.Name, msg)
|
|
}
|
|
|
|
// Unwrap implements errors.Unwrap interface.
|
|
func (e *Exception) Unwrap() []error {
|
|
if e == nil {
|
|
return nil
|
|
}
|
|
// Flatten error tree by collecting all error codes.
|
|
// Only check error codes since only they can be compared using errors.Is.
|
|
// Dynamically created Exceptions are not relevant for this functionality.
|
|
return e.collectCodes(nil)
|
|
}
|
|
|
|
func (e *Exception) collectCodes(codes []error) []error {
|
|
result := append(codes, e.Code)
|
|
for _, next := range e.Next {
|
|
result = next.collectCodes(result)
|
|
}
|
|
return result
|
|
}
|
|
|
|
// AsException finds first *Exception in err chain.
|
|
func AsException(err error) (*Exception, bool) {
|
|
var e *Exception
|
|
if !errors.As(err, &e) {
|
|
return nil, false
|
|
}
|
|
return e, true
|
|
}
|
|
|
|
// IsErr reports whether err is error with provided exception codes.
|
|
func IsErr(err error, codes ...proto.Error) bool {
|
|
if e, ok := AsException(err); ok {
|
|
return e.IsCode(codes...)
|
|
}
|
|
return false
|
|
}
|
|
|
|
// IsException reports whether err is Exception.
|
|
func IsException(err error) bool {
|
|
_, ok := AsException(err)
|
|
return ok
|
|
}
|
|
|
|
// Exception reads exception from server.
|
|
func (c *Client) exception() (*Exception, error) {
|
|
var list []proto.Exception
|
|
for {
|
|
var ex proto.Exception
|
|
if err := c.decode(&ex); err != nil {
|
|
return nil, errors.Wrap(err, "decode")
|
|
}
|
|
|
|
list = append(list, ex)
|
|
if !ex.Nested {
|
|
break
|
|
}
|
|
}
|
|
top := list[0]
|
|
e := &Exception{
|
|
Code: top.Code,
|
|
Name: top.Name,
|
|
Message: top.Message,
|
|
Stack: top.Stack,
|
|
}
|
|
for _, next := range list[1:] {
|
|
e.Next = append(e.Next, Exception{
|
|
Code: next.Code,
|
|
Name: next.Name,
|
|
Message: next.Message,
|
|
Stack: next.Stack,
|
|
})
|
|
}
|
|
return e, nil
|
|
}
|
|
|
|
func (c *Client) decode(v proto.AwareDecoder) error {
|
|
return v.DecodeAware(c.reader, c.protocolVersion)
|
|
}
|
|
|
|
func (c *Client) progress() (proto.Progress, error) {
|
|
var p proto.Progress
|
|
|
|
if err := c.decode(&p); err != nil {
|
|
return proto.Progress{}, errors.Wrap(err, "decode")
|
|
}
|
|
|
|
return p, nil
|
|
}
|
|
|
|
func (c *Client) profile() (proto.Profile, error) {
|
|
var p proto.Profile
|
|
|
|
if err := c.decode(&p); err != nil {
|
|
return proto.Profile{}, errors.Wrap(err, "decode")
|
|
}
|
|
|
|
return p, nil
|
|
}
|
|
|
|
// packet reads server code.
|
|
func (c *Client) packet(ctx context.Context) (proto.ServerCode, error) {
|
|
timeout := c.readTimeout
|
|
var deadline time.Time
|
|
if timeout > 0 {
|
|
deadline = time.Now().Add(timeout)
|
|
}
|
|
if d, ok := ctx.Deadline(); ok && (d.Before(deadline) || deadline.IsZero()) {
|
|
// Use context deadline if it is earlier than default timeout or
|
|
// no timeout is set.
|
|
//
|
|
// Otherwise, we can get stuck for a long time in case of network issues.
|
|
// Ref: https://github.com/hanzo-ds/native/issues/274
|
|
deadline = d
|
|
}
|
|
if !deadline.IsZero() {
|
|
if err := c.conn.SetReadDeadline(deadline); err != nil {
|
|
return 0, errors.Wrap(err, "set read deadline")
|
|
}
|
|
defer func() {
|
|
// Reset deadline.
|
|
_ = c.conn.SetReadDeadline(time.Time{})
|
|
}()
|
|
}
|
|
|
|
n, err := c.reader.UVarInt()
|
|
if err != nil {
|
|
return 0, errors.Wrap(err, "uvarint")
|
|
}
|
|
|
|
code := proto.ServerCode(n)
|
|
if ce := c.lg.Check(zap.DebugLevel, "Packet"); ce != nil {
|
|
ce.Write(
|
|
zap.Uint64("packet_code", n),
|
|
zap.Stringer("packet", code),
|
|
)
|
|
}
|
|
if !code.IsAServerCode() {
|
|
return 0, errors.Errorf("bad server packet type %d", n)
|
|
}
|
|
|
|
return code, nil
|
|
}
|
|
|
|
func (c *Client) flushBuf(ctx context.Context, b *proto.Buffer) error {
|
|
defer b.Reset()
|
|
if err := ctx.Err(); err != nil {
|
|
return errors.Wrap(err, "context")
|
|
}
|
|
if len(b.Buf) == 0 {
|
|
// Nothing to flush.
|
|
return nil
|
|
}
|
|
if deadline, ok := ctx.Deadline(); ok {
|
|
if err := c.conn.SetWriteDeadline(deadline); err != nil {
|
|
return errors.Wrap(err, "set write deadline")
|
|
}
|
|
// Reset deadline.
|
|
defer func() { _ = c.conn.SetWriteDeadline(time.Time{}) }()
|
|
}
|
|
n, err := c.conn.Write(b.Buf)
|
|
if err != nil {
|
|
return errors.Wrap(err, "write")
|
|
}
|
|
if n != len(b.Buf) {
|
|
return errors.Wrap(io.ErrShortWrite, "wrote less than expected")
|
|
}
|
|
if ce := c.lg.Check(zap.DebugLevel, "Buffer flush"); ce != nil {
|
|
ce.Write(zap.Int("bytes", n))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *Client) flush(ctx context.Context) error {
|
|
if err := ctx.Err(); err != nil {
|
|
return errors.Wrap(err, "context")
|
|
}
|
|
if deadline, ok := ctx.Deadline(); ok {
|
|
if err := c.conn.SetWriteDeadline(deadline); err != nil {
|
|
return errors.Wrap(err, "set write deadline")
|
|
}
|
|
// Reset deadline.
|
|
defer func() { _ = c.conn.SetWriteDeadline(time.Time{}) }()
|
|
}
|
|
n, err := c.writer.Flush()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if ce := c.lg.Check(zap.DebugLevel, "Flush"); ce != nil {
|
|
ce.Write(zap.Int64("bytes", n))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *Client) encode(v proto.AwareEncoder) {
|
|
c.writer.ChainBuffer(func(b *proto.Buffer) {
|
|
v.EncodeAware(b, c.protocolVersion)
|
|
})
|
|
}
|
|
|
|
//go:generate go run github.com/dmarkham/enumer -transform upper -type Compression -trimprefix Compression -output compression_enum.go
|
|
|
|
// Compression setting.
|
|
//
|
|
// Trade bandwidth for CPU.
|
|
type Compression byte
|
|
|
|
const (
|
|
// CompressionDisabled disables compression. Lowest CPU overhead.
|
|
CompressionDisabled Compression = iota
|
|
// CompressionLZ4 enables LZ4 compression for data. Medium CPU overhead.
|
|
CompressionLZ4
|
|
// CompressionZSTD enables ZStandard compression. High CPU overhead.
|
|
CompressionZSTD
|
|
// CompressionNone uses no compression but data has checksums.
|
|
CompressionNone
|
|
// CompressionLZ4HC enables LZ4HC compression for data. High CPU overhead.
|
|
CompressionLZ4HC
|
|
)
|
|
|
|
// CompressionLevel setting. A level == 0 is invalid and resolves to the default.
|
|
//
|
|
// Supported by: LZ4HC.
|
|
type CompressionLevel uint32
|
|
|
|
// Options for Client. Zero value is valid.
|
|
type Options struct {
|
|
Logger *zap.Logger // defaults to Nop.
|
|
Address string // 127.0.0.1:9000
|
|
Database string // "default"
|
|
User string // "default"
|
|
Password string // blank string by default
|
|
QuotaKey string // blank string by default
|
|
Compression Compression // disabled by default
|
|
CompressionLevel CompressionLevel // compression algorithm specific default
|
|
ClientName string // blank string by default
|
|
Settings []Setting // none by default
|
|
|
|
// ReadTimeout is a timeout for reading a single packet from the server.
|
|
//
|
|
// Defaults to 3s. No timeout if negative (you can use NoTimeout const).
|
|
ReadTimeout time.Duration
|
|
|
|
Dialer Dialer // defaults to net.Dialer
|
|
DialTimeout time.Duration // defaults to 1s
|
|
TLS *tls.Config // no TLS is used by default
|
|
|
|
ProtocolVersion int // force protocol version, optional
|
|
HandshakeTimeout time.Duration // longer lasting handshake is a case for Datastore cloud idle instances, defaults to 5m
|
|
|
|
// Additional OpenTelemetry instrumentation that will capture query body
|
|
// and other parameters.
|
|
//
|
|
// Note: OpenTelemetry context propagation works without this option too.
|
|
OpenTelemetryInstrumentation bool
|
|
TracerProvider trace.TracerProvider
|
|
MeterProvider metric.MeterProvider
|
|
|
|
// SSH authentication.
|
|
SSHSigner cryptossh.Signer
|
|
|
|
meter metric.Meter
|
|
tracer trace.Tracer
|
|
}
|
|
|
|
// Defaults for connection.
|
|
const (
|
|
DefaultDatabase = "default"
|
|
DefaultUser = "default"
|
|
DefaultHost = "127.0.0.1"
|
|
DefaultPort = 9000
|
|
DefaultDialTimeout = 1 * time.Second
|
|
DefaultHandshakeTimeout = 300 * time.Second
|
|
DefaultReadTimeout = 3 * time.Second
|
|
)
|
|
|
|
// NoTimeout is a value for Options.ReadTimeout that disables timeout.
|
|
const NoTimeout = time.Duration(-1)
|
|
|
|
func (o *Options) setDefaults() {
|
|
if o.ProtocolVersion == 0 {
|
|
o.ProtocolVersion = proto.Version
|
|
}
|
|
if o.HandshakeTimeout == 0 {
|
|
o.HandshakeTimeout = DefaultHandshakeTimeout
|
|
}
|
|
if o.Database == "" {
|
|
o.Database = DefaultDatabase
|
|
}
|
|
if o.User == "" {
|
|
o.User = DefaultUser
|
|
}
|
|
if o.Logger == nil {
|
|
o.Logger = zap.NewNop()
|
|
}
|
|
if o.Address == "" {
|
|
o.Address = net.JoinHostPort(DefaultHost, strconv.Itoa(DefaultPort))
|
|
}
|
|
if o.DialTimeout == 0 {
|
|
o.DialTimeout = DefaultDialTimeout
|
|
}
|
|
if o.Dialer == nil {
|
|
o.Dialer = &net.Dialer{
|
|
Timeout: o.DialTimeout,
|
|
}
|
|
}
|
|
if o.MeterProvider == nil {
|
|
o.MeterProvider = otel.GetMeterProvider()
|
|
}
|
|
if o.TracerProvider == nil {
|
|
o.TracerProvider = otel.GetTracerProvider()
|
|
}
|
|
if o.meter == nil {
|
|
o.meter = o.MeterProvider.Meter(tracing.Name)
|
|
}
|
|
if o.tracer == nil {
|
|
o.tracer = o.TracerProvider.Tracer(tracing.Name,
|
|
trace.WithInstrumentationVersion(tracing.SemVersion()),
|
|
)
|
|
}
|
|
if o.ReadTimeout == 0 {
|
|
o.ReadTimeout = DefaultReadTimeout
|
|
}
|
|
}
|
|
|
|
type clientVersion struct {
|
|
Name string
|
|
Major int
|
|
Minor int
|
|
Patch int
|
|
}
|
|
|
|
// Connect performs handshake with Datastore server and initializes
|
|
// application level connection.
|
|
func Connect(ctx context.Context, conn net.Conn, opt Options) (*Client, error) {
|
|
return ConnectWithBuffer(ctx, conn, opt, new(proto.Buffer))
|
|
}
|
|
|
|
// ConnectWithBuffer performs handshake with Datastore server and initializes
|
|
// application level connection using the provided buffer.
|
|
func ConnectWithBuffer(ctx context.Context, conn net.Conn, opt Options, buf *proto.Buffer) (*Client, error) {
|
|
opt.setDefaults()
|
|
|
|
clientName := proto.Name
|
|
pkg := pkgVersion.Get()
|
|
if opt.ClientName == "" {
|
|
if pkg.Name != "" {
|
|
clientName = fmt.Sprintf("%s (%s)", clientName, pkg.Name)
|
|
}
|
|
} else {
|
|
clientName = fmt.Sprintf("%s %s", clientName, opt.ClientName)
|
|
}
|
|
ver := clientVersion{
|
|
Name: clientName,
|
|
Major: pkg.Major,
|
|
Minor: pkg.Minor,
|
|
Patch: pkg.Patch,
|
|
}
|
|
|
|
if opt.OpenTelemetryInstrumentation {
|
|
newCtx, span := opt.tracer.Start(ctx, "Connect",
|
|
trace.WithSpanKind(trace.SpanKindClient),
|
|
trace.WithAttributes(
|
|
semconv.DBNameKey.String(opt.Database),
|
|
),
|
|
)
|
|
ctx = newCtx
|
|
defer span.End()
|
|
}
|
|
|
|
var (
|
|
compression proto.Compression
|
|
compressionMethod compress.Method
|
|
)
|
|
switch opt.Compression {
|
|
case CompressionLZ4:
|
|
compression = proto.CompressionEnabled
|
|
compressionMethod = compress.LZ4
|
|
case CompressionLZ4HC:
|
|
compression = proto.CompressionEnabled
|
|
compressionMethod = compress.LZ4HC
|
|
case CompressionZSTD:
|
|
compression = proto.CompressionEnabled
|
|
compressionMethod = compress.ZSTD
|
|
case CompressionNone:
|
|
compression = proto.CompressionEnabled
|
|
compressionMethod = compress.None
|
|
default:
|
|
compression = proto.CompressionDisabled
|
|
}
|
|
|
|
user := opt.User
|
|
if opt.SSHSigner != nil {
|
|
user = " SSH KEY AUTHENTICATION " + user
|
|
}
|
|
|
|
c := &Client{
|
|
conn: conn,
|
|
writer: proto.NewWriter(conn, buf),
|
|
reader: proto.NewReader(conn),
|
|
settings: opt.Settings,
|
|
lg: opt.Logger,
|
|
otel: opt.OpenTelemetryInstrumentation,
|
|
tracer: opt.tracer,
|
|
meter: opt.meter,
|
|
quotaKey: opt.QuotaKey,
|
|
|
|
readTimeout: opt.ReadTimeout,
|
|
|
|
compression: compression,
|
|
compressor: compress.NewWriter(compress.Level(opt.CompressionLevel), compressionMethod),
|
|
|
|
version: ver,
|
|
protocolVersion: opt.ProtocolVersion,
|
|
info: proto.ClientHello{
|
|
Name: clientName,
|
|
Major: ver.Major,
|
|
Minor: ver.Minor,
|
|
ProtocolVersion: opt.ProtocolVersion,
|
|
Database: opt.Database,
|
|
User: user,
|
|
Password: opt.Password,
|
|
},
|
|
sshSigner: opt.SSHSigner,
|
|
}
|
|
|
|
handshakeCtx, cancel := context.WithTimeout(ctx, opt.HandshakeTimeout)
|
|
defer cancel()
|
|
if err := c.handshake(handshakeCtx); err != nil {
|
|
return nil, errors.Wrap(err, "handshake")
|
|
}
|
|
|
|
return c, nil
|
|
}
|
|
|
|
// A Dialer dials using a context.
|
|
type Dialer interface {
|
|
DialContext(ctx context.Context, network, address string) (net.Conn, error)
|
|
}
|
|
|
|
// Dial dials requested address and establishes TCP connection to Datastore
|
|
// server, performing handshake.
|
|
func Dial(ctx context.Context, opt Options) (c *Client, err error) {
|
|
return DialWithBuffer(ctx, opt, new(proto.Buffer))
|
|
}
|
|
|
|
// DialWithBuffer dials requested address and establishes TCP connection to Datastore
|
|
// server, performing handshake using the provided buffer.
|
|
func DialWithBuffer(ctx context.Context, opt Options, buf *proto.Buffer) (c *Client, err error) {
|
|
opt.setDefaults()
|
|
|
|
if opt.OpenTelemetryInstrumentation {
|
|
newCtx, span := opt.tracer.Start(ctx, "Dial",
|
|
trace.WithSpanKind(trace.SpanKindClient),
|
|
)
|
|
ctx = newCtx
|
|
defer func() {
|
|
if err != nil {
|
|
span.RecordError(err)
|
|
}
|
|
span.End()
|
|
}()
|
|
}
|
|
|
|
if opt.TLS != nil {
|
|
netDialer := &net.Dialer{
|
|
Timeout: opt.DialTimeout,
|
|
}
|
|
if opt.Dialer != nil {
|
|
d, ok := opt.Dialer.(*net.Dialer)
|
|
if !ok {
|
|
return nil, errors.Errorf("tls dialer should be *net.Dialer, got %T", opt.Dialer)
|
|
}
|
|
netDialer = d
|
|
}
|
|
opt.Dialer = &tls.Dialer{
|
|
NetDialer: netDialer,
|
|
Config: opt.TLS,
|
|
}
|
|
}
|
|
|
|
conn, err := opt.Dialer.DialContext(ctx, "tcp", opt.Address)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "dial")
|
|
}
|
|
|
|
client, err := ConnectWithBuffer(ctx, conn, opt, buf)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "connect")
|
|
}
|
|
|
|
return client, nil
|
|
}
|