mirror of
https://github.com/featurebasedb/featurebase.git
synced 2026-08-28 10:54:59 +00:00
204 lines
5.5 KiB
Go
204 lines
5.5 KiB
Go
// Copyright 2022 Molecula Corp. (DBA FeatureBase).
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
package client
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"sync"
|
|
|
|
"github.com/featurebasedb/featurebase/v3/logger"
|
|
pb "github.com/featurebasedb/featurebase/v3/proto"
|
|
"github.com/pkg/errors"
|
|
"google.golang.org/grpc"
|
|
"google.golang.org/grpc/connectivity"
|
|
"google.golang.org/grpc/credentials"
|
|
"google.golang.org/grpc/credentials/insecure"
|
|
)
|
|
|
|
const maxMsgSize = 1024 * 1024 * 100 // 100 megs ought to be enough for anybody!
|
|
|
|
// GRPCClient is a client for working with the gRPC server.
|
|
type GRPCClient struct {
|
|
dialTargets []string
|
|
tlsConfig *tls.Config
|
|
logger logger.Logger
|
|
|
|
mu sync.RWMutex
|
|
conn *grpc.ClientConn
|
|
targetIndex int
|
|
}
|
|
|
|
// NewGRPCClient returns a new instance of GRPCClient.
|
|
func NewGRPCClient(dialTargets []string, tlsConfig *tls.Config, logger logger.Logger) (*GRPCClient, error) {
|
|
c := &GRPCClient{
|
|
dialTargets: dialTargets,
|
|
tlsConfig: tlsConfig,
|
|
logger: logger,
|
|
}
|
|
// resetConn sets GRPCClient.conn when it doesn't
|
|
// exist yet.
|
|
if err := c.resetConn(); err != nil {
|
|
return nil, errors.Wrap(err, "setting connection")
|
|
}
|
|
|
|
return c, nil
|
|
}
|
|
|
|
// resetConn resets the gRPC client connection. This method
|
|
// can also be used to initially set the client connection
|
|
// because it only tries to first close the connection if
|
|
// the connection already exists.
|
|
func (c *GRPCClient) resetConn() error {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
|
|
// If an existing connection exists, close it first.
|
|
if c.conn != nil {
|
|
if err := c.conn.Close(); err != nil {
|
|
return errors.Wrap(err, "closing existing connection")
|
|
}
|
|
}
|
|
|
|
var opts []grpc.DialOption
|
|
if c.tlsConfig != nil {
|
|
creds := credentials.NewTLS(c.tlsConfig)
|
|
opts = append(opts, grpc.WithTransportCredentials(creds))
|
|
} else {
|
|
opts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
|
}
|
|
|
|
opts = append(opts, grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(maxMsgSize)))
|
|
|
|
var err error
|
|
if c.conn, err = grpc.Dial(c.dialTargets[c.getTargetIndex()], opts...); err != nil {
|
|
return errors.Wrap(err, "creating new grpc client")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// getTargetIndex gets the current target index, then increments it for
|
|
// next time. Unprotected.
|
|
func (c *GRPCClient) getTargetIndex() int {
|
|
if len(c.dialTargets) == 0 {
|
|
return 0
|
|
}
|
|
ret := c.targetIndex
|
|
c.targetIndex = (c.targetIndex + 1) % len(c.dialTargets) // cycle through dialTargets
|
|
return ret
|
|
}
|
|
|
|
// Close closes any connections the client has opened.
|
|
func (c *GRPCClient) Close() error {
|
|
c.mu.RLock()
|
|
defer c.mu.RUnlock()
|
|
|
|
if c.conn != nil {
|
|
return c.conn.Close()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Conn returns the gRPC client connection. If the connection
|
|
// has gone into state `TransientFailure`, this method tries
|
|
// to reset the connection and return that new connection.
|
|
func (c *GRPCClient) Conn() *grpc.ClientConn {
|
|
c.mu.RLock()
|
|
if c.conn == nil {
|
|
c.mu.RUnlock()
|
|
return nil
|
|
} else if c.conn.GetState() != connectivity.TransientFailure {
|
|
defer c.mu.RUnlock()
|
|
return c.conn
|
|
}
|
|
c.mu.RUnlock()
|
|
|
|
if err := c.resetConn(); err != nil {
|
|
c.logger.Errorf("error resetting connection: %s", err)
|
|
}
|
|
|
|
c.mu.RLock()
|
|
defer c.mu.RUnlock()
|
|
return c.conn
|
|
}
|
|
|
|
// Query returns a stream of RowResponse for the given index and PQL string.
|
|
func (c *GRPCClient) Query(ctx context.Context, index string, pql string) (pb.StreamClient, error) {
|
|
conn := c.Conn()
|
|
|
|
if conn == nil {
|
|
return nil, errors.New("client has not established a grpc connection")
|
|
}
|
|
|
|
grpcClient := pb.NewPilosaClient(conn)
|
|
|
|
stream, err := grpcClient.QueryPQL(ctx, &pb.QueryPQLRequest{
|
|
Index: index,
|
|
Pql: pql,
|
|
})
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "getting stream")
|
|
} else if stream == nil {
|
|
return nil, errors.New("could not create stream")
|
|
}
|
|
|
|
return stream, err
|
|
}
|
|
|
|
// QueryUnary returns a TableResponse for the given index and PQL string.
|
|
func (c *GRPCClient) QueryUnary(ctx context.Context, index string, pql string) (*pb.TableResponse, error) {
|
|
conn := c.Conn()
|
|
|
|
if conn == nil {
|
|
return nil, errors.New("client has not established a grpc connection")
|
|
}
|
|
|
|
grpcClient := pb.NewPilosaClient(conn)
|
|
|
|
return grpcClient.QueryPQLUnary(ctx, &pb.QueryPQLRequest{
|
|
Index: index,
|
|
Pql: pql,
|
|
})
|
|
}
|
|
|
|
// Inspect returns a stream of RowResponse for the given index, columns, and filters.
|
|
// It is intended to mimic something like "select [fields] from table where recordID IN (...)".
|
|
func (c *GRPCClient) Inspect(ctx context.Context, index string, columnIDs []uint64, columnKeys []string, query string, fieldFilters []string, limit, offset uint64) (pb.StreamClient, error) {
|
|
conn := c.Conn()
|
|
|
|
if conn == nil {
|
|
return nil, errors.New("client has not established a grpc connection")
|
|
}
|
|
|
|
if len(columnIDs) > 0 && len(columnKeys) > 0 {
|
|
return nil, errors.New("only provide column ids or keys, not both")
|
|
}
|
|
|
|
// Convert columns to proto type IdsOrKeys.
|
|
idsOrKeys := &pb.IdsOrKeys{}
|
|
if len(columnKeys) > 0 {
|
|
idsOrKeys.Type = &pb.IdsOrKeys_Keys{Keys: &pb.StringArray{Vals: columnKeys}}
|
|
} else {
|
|
idsOrKeys.Type = &pb.IdsOrKeys_Ids{Ids: &pb.Uint64Array{Vals: columnIDs}}
|
|
}
|
|
|
|
grpcClient := pb.NewPilosaClient(conn)
|
|
|
|
stream, err := grpcClient.Inspect(ctx, &pb.InspectRequest{
|
|
Index: index,
|
|
Columns: idsOrKeys,
|
|
FilterFields: fieldFilters,
|
|
Limit: limit,
|
|
Offset: offset,
|
|
Query: query,
|
|
})
|
|
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "getting stream")
|
|
} else if stream == nil {
|
|
return nil, errors.New("could not create stream")
|
|
}
|
|
|
|
return stream, err
|
|
}
|