featurebase/client/client.go

1740 lines
50 KiB
Go

// Copyright 2017 Pilosa Corp.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// package ctl contains all pilosa subcommands other than 'server'. These are
// generally administration, testing, and debugging tools.
package client
import (
"bytes"
"crypto/tls"
"encoding/json"
"fmt"
"io"
"math/rand"
"net"
"net/http"
"net/url"
"os"
"strconv"
"strings"
"sync"
"time"
"github.com/golang/protobuf/proto" //nolint:staticcheck
"github.com/opentracing/opentracing-go"
"github.com/molecula/featurebase/v2"
"github.com/molecula/featurebase/v2/logger"
pnet "github.com/molecula/featurebase/v2/net"
"github.com/molecula/featurebase/v2/pb"
"github.com/molecula/featurebase/v2/pql"
"github.com/molecula/featurebase/v2/roaring"
"github.com/molecula/featurebase/v2/stats"
"github.com/pkg/errors"
"golang.org/x/sync/errgroup"
)
// PQLVersion is the version of PQL expected by the client
const PQLVersion = "1.0"
// DefaultShardWidth is used if an index doesn't have it defined.
const DefaultShardWidth = 1 << 20
const maxHosts = 10
// Client is the HTTP client for Pilosa server.
type Client struct {
cluster *Cluster
client *http.Client
logger logger.Logger
primaryURI *pnet.URI
primaryLock *sync.RWMutex
manualFragmentNode *fragmentNode
manualServerURI *pnet.URI
tracer opentracing.Tracer
Stats stats.StatsClient
// An exponential backoff algorithm retries requests exponentially (if an HTTP request fails),
// increasing the waiting time between retries up to a maximum backoff time.
maxBackoff time.Duration
maxRetries int
nat map[pnet.URI]pnet.URI
shardNodes shardNodes
tick *time.Ticker
done chan struct{}
}
func (c *Client) getURIsForShard(index string, shard uint64) ([]*pnet.URI, error) {
uris, ok := c.shardNodes.Get(index, shard)
if ok {
return uris, nil
}
fragmentNodes, err := c.fetchFragmentNodes(index, shard)
if err != nil {
return nil, errors.Wrap(err, "trying to look up nodes for shard")
}
uris = make([]*pnet.URI, 0, len(fragmentNodes))
for _, fn := range fragmentNodes {
uris = append(uris, fn.URI())
}
c.shardNodes.Put(index, shard, uris)
return uris, nil
}
func (c *Client) runChangeDetection() {
for {
select {
case <-c.tick.C:
c.detectClusterChanges()
case <-c.done:
return
}
}
}
func (c *Client) Close() error {
c.tick.Stop()
close(c.done)
return nil
}
// detectClusterChanges chooses a random index and shard from the
// shardNodes cache and deletes it. It then looks it up from Pilosa to
// see if it still matches, and if not it drops the whole cache.
func (c *Client) detectClusterChanges() {
c.shardNodes.mu.Lock()
needsUnlock := true
// we rely on Go's random map iteration order to get a random
// element. If it doesn't end up being random, it shouldn't
// actually matter.
for index, shardMap := range c.shardNodes.data {
for shard, uris := range shardMap {
delete(shardMap, shard)
c.shardNodes.data[index] = shardMap
c.shardNodes.mu.Unlock()
needsUnlock = false
newURIs, err := c.getURIsForShard(index, shard) // refetch URIs from server.
if err != nil {
c.logger.Errorf("problem invalidating shard node cache: %v", err)
return
}
if len(uris) != len(newURIs) {
c.logger.Printf("invalidating shard node cache old: %v, new: %v", uris, newURIs)
c.shardNodes.Invalidate()
return
}
for i := range uris {
u1, u2 := uris[i], newURIs[i]
if *u1 != *u2 {
c.logger.Printf("invalidating shard node cache, uri mismatch at %d old: %v, new: %v", i, uris, newURIs)
c.shardNodes.Invalidate()
return
}
}
break
}
break
}
if needsUnlock {
c.shardNodes.mu.Unlock()
}
}
// DefaultClient creates a client with the default address and options.
func DefaultClient() *Client {
return newClientWithCluster(NewClusterWithHost(pnet.DefaultURI()), nil)
}
func newClientFromAddresses(addresses []string, options *ClientOptions) (*Client, error) {
uris := make([]*pnet.URI, len(addresses))
for i, address := range addresses {
uri, err := pnet.NewURIFromAddress(address)
if err != nil {
return nil, err
}
uris[i] = uri
}
cluster := NewClusterWithHost(uris...)
client := newClientWithCluster(cluster, options)
return client, nil
}
func newClientWithCluster(cluster *Cluster, options *ClientOptions) *Client {
client := newClientWithOptions(options)
client.cluster = cluster
return client
}
func newClientWithURI(uri *pnet.URI, options *ClientOptions) *Client {
client := newClientWithOptions(options)
if options.manualServerAddress {
fragmentNode := newFragmentNodeFromURI(uri)
client.manualFragmentNode = &fragmentNode
client.manualServerURI = uri
client.cluster = NewClusterWithHost()
}
client.cluster = NewClusterWithHost(uri)
return client
}
func newClientWithOptions(options *ClientOptions) *Client {
if options == nil {
options = &ClientOptions{}
}
options = options.withDefaults()
c := &Client{
client: newHTTPClient(options.withDefaults()),
logger: logger.NewStandardLogger(os.Stderr),
primaryLock: &sync.RWMutex{},
shardNodes: newShardNodes(),
tick: time.NewTicker(time.Minute),
done: make(chan struct{}),
nat: options.nat,
}
if options.tracer == nil {
c.tracer = NoopTracer{}
} else {
c.tracer = options.tracer
}
if options.stats == nil {
c.Stats = stats.NopStatsClient
} else {
c.Stats = options.stats
}
c.maxRetries = *options.retries
c.maxBackoff = 2 * time.Minute
go c.runChangeDetection()
return c
}
// NewClient creates a client with the given address, URI, or cluster and options.
func NewClient(addrURIOrCluster interface{}, options ...ClientOption) (*Client, error) {
var cluster *Cluster
clientOptions := &ClientOptions{
nat: make(map[pnet.URI]pnet.URI),
}
err := clientOptions.addOptions(options...)
if err != nil {
return nil, err
}
switch u := addrURIOrCluster.(type) {
case string:
uri, err := pnet.NewURIFromAddress(u)
if err != nil {
return nil, err
}
return newClientWithURI(uri, clientOptions), nil
case []string:
if len(u) == 1 {
uri, err := pnet.NewURIFromAddress(u[0])
if err != nil {
return nil, err
}
return newClientWithURI(uri, clientOptions), nil
} else if clientOptions.manualServerAddress {
return nil, ErrSingleServerAddressRequired
}
return newClientFromAddresses(u, clientOptions)
case *pnet.URI:
uriCopy := *u
return newClientWithURI(&uriCopy, clientOptions), nil
case []*pnet.URI:
if len(u) == 1 {
uriCopy := *u[0]
return newClientWithURI(&uriCopy, clientOptions), nil
} else if clientOptions.manualServerAddress {
return nil, ErrSingleServerAddressRequired
}
cluster = NewClusterWithHost(u...)
case *Cluster:
cluster = u
case nil:
cluster = NewClusterWithHost()
default:
return nil, ErrAddrURIClusterExpected
}
return newClientWithCluster(cluster, clientOptions), nil
}
// Query runs the given query against the server with the given options.
// Pass nil for default options.
func (c *Client) Query(query PQLQuery, options ...interface{}) (*QueryResponse, error) {
span := c.tracer.StartSpan("Client.Query")
defer span.Finish()
if err := query.Error(); err != nil {
return nil, err
}
queryOptions := &QueryOptions{}
err := queryOptions.addOptions(options...)
if err != nil {
return nil, err
}
serializedQuery := query.Serialize()
reqData, err := makeRequestData(serializedQuery.String(), queryOptions)
if err != nil {
return nil, errors.Wrap(err, "making request data")
}
path := fmt.Sprintf("/index/%s/query", query.Index().name)
_, respData, err := c.HTTPRequest("POST", path, reqData, defaultProtobufHeaders())
if err != nil {
return nil, err
}
iqr := &pb.QueryResponse{}
err = proto.Unmarshal(respData, iqr)
if err != nil {
return nil, err
}
queryResponse, err := newQueryResponseFromInternal(iqr)
if err != nil {
return nil, err
}
return queryResponse, nil
}
// CreateIndex creates an index on the server using the given Index struct.
func (c *Client) CreateIndex(index *Index) error {
span := c.tracer.StartSpan("Client.CreateIndex")
defer span.Finish()
data := []byte(index.options.String())
path := fmt.Sprintf("/index/%s", index.name)
status, body, err := c.HTTPRequest("POST", path, data, nil)
if err != nil {
return errors.Wrapf(err, "creating index: %s", index.name)
}
var resp struct {
CreatedAt int64 `json:"createdAt,omitempty"`
}
if err := json.Unmarshal(body, &resp); err == nil && resp.CreatedAt != 0 {
index.createdAt = resp.CreatedAt
}
if status == http.StatusConflict {
return ErrIndexExists
}
return nil
}
// CreateField creates a field on the server using the given Field struct.
func (c *Client) CreateField(field *Field) error {
span := c.tracer.StartSpan("Client.CreateField")
defer span.Finish()
data := []byte(field.options.String())
path := fmt.Sprintf("/index/%s/field/%s", field.index.name, field.name)
status, body, err := c.HTTPRequest("POST", path, data, nil)
if err != nil {
return errors.Wrapf(err, "creating field: %s in index: %s", field.name, field.index.name)
}
var resp struct {
CreatedAt int64 `json:"createdAt,omitempty"`
}
if err := json.Unmarshal(body, &resp); err == nil && resp.CreatedAt != 0 {
field.createdAt = resp.CreatedAt
}
if status == http.StatusConflict {
return ErrFieldExists
}
return nil
}
// EnsureIndex creates an index on the server if it does not exist.
func (c *Client) EnsureIndex(index *Index) error {
err := c.CreateIndex(index)
if err == ErrIndexExists {
return nil
}
return errors.Wrap(err, "creating index")
}
func (c *Client) SyncIndex(index *Index) error {
err := c.EnsureIndex(index)
if err != nil {
return errors.Wrapf(err, "ensuring index exists")
}
for name, field := range index.fields {
if name == "_exists" {
continue
}
err = c.EnsureField(field)
if err != nil {
return errors.Wrapf(err, "ensuring field")
}
}
return nil
}
// EnsureField creates a field on the server if it doesn't exists.
func (c *Client) EnsureField(field *Field) error {
err := c.CreateField(field)
if err == ErrFieldExists {
return nil
}
return err
}
// DeleteIndex deletes an index on the server.
func (c *Client) DeleteIndex(index *Index) error {
if index != nil {
return c.DeleteIndexByName(index.Name())
}
return nil
}
// DeleteIndexByName deletes the named index on the server.
func (c *Client) DeleteIndexByName(index string) error {
span := c.tracer.StartSpan("Client.DeleteIndex")
defer span.Finish()
path := fmt.Sprintf("/index/%s", index)
_, _, err := c.HTTPRequest("DELETE", path, nil, nil)
return err
}
// DeleteField deletes a field on the server.
func (c *Client) DeleteField(field *Field) error {
span := c.tracer.StartSpan("Client.DeleteField")
defer span.Finish()
path := fmt.Sprintf("/index/%s/field/%s", field.index.name, field.name)
_, _, err := c.HTTPRequest("DELETE", path, nil, nil)
return err
}
// SyncSchema updates a schema with the indexes and fields on the server and
// creates the indexes and fields in the schema on the server side.
// This function does not delete indexes and the fields on the server side nor in the schema.
func (c *Client) SyncSchema(schema *Schema) error {
span := c.tracer.StartSpan("Client.SyncSchema")
defer span.Finish()
schema.mu.Lock()
defer schema.mu.Unlock()
serverSchema, err := c.Schema()
if err != nil {
return err
}
serverSchema.mu.RLock()
defer serverSchema.mu.RUnlock()
return c.syncSchema(schema, serverSchema)
}
func (c *Client) syncSchema(schema *Schema, serverSchema *Schema) error {
var err error
// find out local - remote schema
diffSchema := schema.diff(serverSchema)
// create the indexes and fields which doesn't exist on the server side
for indexName, index := range diffSchema.indexes {
if _, ok := serverSchema.indexes[indexName]; !ok {
err = c.EnsureIndex(index)
if err != nil {
return errors.Wrap(err, "ensuring index")
}
}
for name, field := range index.fields {
if name == "_exists" {
continue
}
err = c.EnsureField(field)
if err != nil {
return errors.Wrapf(err, "ensuring field")
}
}
}
// find out remote - local schema
diffSchema = serverSchema.diff(schema)
for indexName, index := range diffSchema.indexes {
if localIndex, ok := schema.indexes[indexName]; !ok {
schema.indexes[indexName] = index
} else {
for fieldName, field := range index.fields {
localIndex.fields[fieldName] = field
}
}
}
return nil
}
// Schema returns the indexes and fields on the server.
func (c *Client) Schema() (*Schema, error) {
span := c.tracer.StartSpan("Client.Schema")
defer span.Finish()
var indexes []SchemaIndex
indexes, err := c.readSchema()
if err != nil {
return nil, err
}
schema := NewSchema()
for _, indexInfo := range indexes {
index := schema.indexWithOptions(indexInfo.Name, indexInfo.CreatedAt, indexInfo.ShardWidth, indexInfo.Options.asIndexOptions())
for _, fieldInfo := range indexInfo.Fields {
index.fieldWithOptions(fieldInfo.Name, fieldInfo.CreatedAt, fieldInfo.Options.asFieldOptions())
}
}
return schema, nil
}
// Import imports data for a single shard using the regular import
// endpoint rather than import-roaring. This is good for e.g. mutex or
// bool fields where import-roaring is not supported.
func (c *Client) Import(field *Field, shard uint64, vals, ids []uint64, clear bool) error {
path, data, err := c.EncodeImport(field, shard, vals, ids, clear)
if err != nil {
return errors.Wrap(err, "encoding import request")
}
err = c.DoImport(field.index.Name(), shard, path, data)
return errors.Wrap(err, "doing import")
}
// EncodeImport computes the HTTP path and payload for an import
// request. It is typically followed by a call to DoImport.
func (c *Client) EncodeImport(field *Field, shard uint64, vals, ids []uint64, clear bool) (path string, data []byte, err error) {
msg := &pb.ImportRequest{
Index: field.index.Name(),
IndexCreatedAt: field.index.CreatedAt(),
Field: field.Name(),
FieldCreatedAt: field.CreatedAt(),
Shard: shard,
RowIDs: vals,
ColumnIDs: ids,
}
data, err = proto.Marshal(msg)
if err != nil {
return "", nil, errors.Wrap(err, "marshaling Import to protobuf")
}
path = fmt.Sprintf("/index/%s/field/%s/import?clear=%s&ignoreKeyCheck=true", field.index.Name(), field.Name(), strconv.FormatBool(clear))
return path, data, nil
}
// DoImport takes a path and data payload (normally from EncodeImport
// or EncodeImportValues), logs the import, finds all nodes which own
// this shard, and concurrently imports to those nodes.
func (c *Client) DoImport(index string, shard uint64, path string, data []byte) error {
// Unlike ImportRoaring, Pilosa does not forward requests to the
// .../import endpoint to all replicas, so we must do that
// here. Yes this is odd. To make it worse, if a ../import request
// was made with keys that needed to be translated server side,
// Pilosa would handle sending the translated data to all the
// appropriate nodes and replicas.
uris, err := c.getURIsForShard(index, shard)
if err != nil {
return errors.Wrap(err, "getting uris")
}
eg := errgroup.Group{}
for _, uri := range uris {
uri := uri
eg.Go(func() error {
return c.importData(uri, path, data)
})
}
return errors.Wrap(eg.Wait(), "importing to nodes")
}
// EncodeImportValues computes the HTTP path and payload for an
// import-values request. It is typically followed by a call to
// DoImportValues.
func (c *Client) EncodeImportValues(field *Field, shard uint64, vals []int64, ids []uint64, clear bool) (path string, data []byte, err error) {
msg := &pb.ImportValueRequest{
Index: field.index.Name(),
IndexCreatedAt: field.index.CreatedAt(),
Field: field.Name(),
FieldCreatedAt: field.CreatedAt(),
Shard: shard,
ColumnIDs: ids,
Values: vals,
}
data, err = proto.Marshal(msg)
if err != nil {
return "", nil, errors.Wrap(err, "marshaling ImportValue to protobuf")
}
path = fmt.Sprintf("/index/%s/field/%s/import?clear=%s&ignoreKeyCheck=true", field.index.Name(), field.Name(), strconv.FormatBool(clear))
return path, data, nil
}
// ImportValues takes the given integer values and column ids (which
// must all be in the given shard) and imports them into the given
// index,field,shard on all nodes which should hold that shard. It
// assumes that the ids have been translated from keys if necessary
// and so tells Pilosa to ignore checking if the index uses column
// keys. ImportValues wraps EncodeImportValues and DoImportValues —
// these are broken out and exported so that performance conscious
// users can re-use the same vals and ids byte buffers for local
// encoding, while performing the imports concurrently.
func (c *Client) ImportValues(field *Field, shard uint64, vals []int64, ids []uint64, clear bool) error {
path, data, err := c.EncodeImportValues(field, shard, vals, ids, clear)
if err != nil {
return errors.Wrap(err, "encoding import-values request")
}
err = c.DoImportValues(field.index.Name(), shard, path, data)
return errors.Wrap(err, "doing import values")
}
// DoImportValues is deprecated. Use DoImport.
func (c *Client) DoImportValues(index string, shard uint64, path string, data []byte) error {
return c.DoImport(index, shard, path, data)
}
func (c *Client) fetchFragmentNodes(indexName string, shard uint64) ([]fragmentNode, error) {
if c.manualFragmentNode != nil {
return []fragmentNode{*c.manualFragmentNode}, nil
}
path := fmt.Sprintf("/internal/fragment/nodes?shard=%d&index=%s", shard, indexName)
_, body, err := c.HTTPRequest("GET", path, []byte{}, nil)
if err != nil {
return nil, err
}
fragmentNodes := []fragmentNode{}
var fragmentNodeURIs []fragmentNodeRoot
err = json.Unmarshal(body, &fragmentNodeURIs)
if err != nil {
return nil, errors.Wrap(err, "unmarshaling fragment node URIs")
}
for _, nodeURI := range fragmentNodeURIs {
fragmentNodes = append(fragmentNodes, nodeURI.URI)
}
return fragmentNodes, nil
}
func (c *Client) fetchPrimaryNode() (fragmentNode, error) {
if c.manualFragmentNode != nil {
return *c.manualFragmentNode, nil
}
status, err := c.Status()
if err != nil {
return fragmentNode{}, err
}
for _, node := range status.Nodes {
if node.IsPrimary {
nodeURI := node.URI.URI().Translate(c.nat)
return fragmentNode{ //nolint:gosimple
Scheme: nodeURI.Scheme,
Host: nodeURI.Host,
Port: nodeURI.Port,
}, nil
}
}
return fragmentNode{}, errors.New("Primary node not found")
}
func (c *Client) importData(uri *pnet.URI, path string, data []byte) error {
if status, _, err := c.doRequest(uri, "POST", path, defaultProtobufHeaders(), data); err != nil {
return errors.Wrapf(err, "import to %s", uri.HostPort())
} else if status == http.StatusPreconditionFailed {
return ErrPreconditionFailed
}
return nil
}
// ImportRoaringBitmap can import pre-made bitmaps for a number of
// different views into the given field/shard. If the view name in the
// map is an empty string, the standard view will be used.
func (c *Client) ImportRoaringBitmap(field *Field, shard uint64, views map[string]*roaring.Bitmap, clear bool) error {
uris, err := c.getURIsForShard(field.index.Name(), shard)
if err != nil {
return errors.Wrap(err, "getting URIs for import")
}
err = c.importRoaringBitmap(uris[0], field, shard, views, &ImportOptions{clear: clear})
return errors.Wrap(err, "importing bitmap")
}
func (c *Client) importRoaringBitmap(uri *pnet.URI, field *Field, shard uint64, views viewImports, options *ImportOptions) error {
protoViews := []*pb.ImportRoaringRequestView{}
for name, bmp := range views {
buf := &bytes.Buffer{}
_, err := bmp.WriteTo(buf)
if err != nil {
return errors.Wrap(err, "marshalling bitmap")
}
protoViews = append(protoViews, &pb.ImportRoaringRequestView{
Name: name,
Data: buf.Bytes(),
})
}
params := url.Values{}
params.Add("clear", strconv.FormatBool(options.clear))
path := makeRoaringImportPath(field, shard, params)
req := &pb.ImportRoaringRequest{
Clear: options.clear,
Views: protoViews,
IndexCreatedAt: field.index.CreatedAt(),
FieldCreatedAt: field.CreatedAt(),
}
data, err := proto.Marshal(req)
if err != nil {
return err
}
status, _, err := c.doRequest(uri, "POST", path, defaultProtobufHeaders(), data)
if err != nil {
return errors.Wrapf(err, "roaring import to %s, status: %d", uri.HostPort(), status)
}
if status == http.StatusPreconditionFailed {
return ErrPreconditionFailed
}
return nil
}
// ExportField exports columns for a field.
func (c *Client) ExportField(field *Field) (io.Reader, error) {
span := c.tracer.StartSpan("Client.ExportField")
defer span.Finish()
var shardsMax map[string]uint64
var err error
status, err := c.Status()
if err != nil {
return nil, err
}
shardsMax, err = c.shardsMax()
if err != nil {
return nil, err
}
status.indexMaxShard = shardsMax
shardURIs, err := c.statusToNodeShardsForIndex(status, field.index.Name())
if err != nil {
return nil, err
}
return newExportReader(c, shardURIs, field), nil
}
// Info returns the server's configuration/host information.
func (c *Client) Info() (Info, error) {
span := c.tracer.StartSpan("Client.Info")
defer span.Finish()
_, data, err := c.HTTPRequest("GET", "/info", nil, nil)
if err != nil {
return Info{}, errors.Wrap(err, "requesting /info")
}
info := Info{}
err = json.Unmarshal(data, &info)
if err != nil {
return Info{}, errors.Wrap(err, "unmarshaling /info data")
}
return info, nil
}
// Status returns the server's status.
func (c *Client) Status() (Status, error) {
span := c.tracer.StartSpan("Client.Status")
defer span.Finish()
_, data, err := c.HTTPRequest("GET", "/status", nil, nil)
if err != nil {
return Status{}, errors.Wrap(err, "requesting /status")
}
status := Status{}
err = json.Unmarshal(data, &status)
if err != nil {
return Status{}, errors.Wrap(err, "unmarshaling /status data")
}
return status, nil
}
func (c *Client) readSchema() ([]SchemaIndex, error) {
_, data, err := c.HTTPRequest("GET", "/schema", nil, nil)
if err != nil {
return nil, errors.Wrap(err, "requesting /schema")
}
schemaInfo := SchemaInfo{}
err = json.Unmarshal(data, &schemaInfo)
if err != nil {
return nil, errors.Wrap(err, "unmarshaling /schema data")
}
return schemaInfo.Indexes, nil
}
func (c *Client) shardsMax() (map[string]uint64, error) {
_, data, err := c.HTTPRequest("GET", "/internal/shards/max", nil, nil)
if err != nil {
return nil, errors.Wrap(err, "requesting /internal/shards/max")
}
m := map[string]map[string]uint64{}
err = json.Unmarshal(data, &m)
if err != nil {
return nil, errors.Wrap(err, "unmarshaling /internal/shards/max data")
}
return m["standard"], nil
}
// HTTPRequest sends an HTTP request to the Pilosa server (used by idk)
// nolint: deadcode
func (c *Client) HTTPRequest(method string, path string, data []byte, headers map[string]string) (status int, body []byte, err error) {
span := c.tracer.StartSpan("Client.HTTPRequest")
status, body, err = c.httpRequest(method, path, data, headers, false)
span.Finish()
return
}
// httpRequest makes a request to the cluster - use this when you want the
// client to choose a host, and it doesn't matter if the request goes to a
// specific host
func (c *Client) httpRequest(method string, path string, data []byte, headers map[string]string, usePrimary bool) (int, []byte, error) {
if data == nil {
data = []byte{}
}
var (
status int
body []byte
err error
)
// try at most maxHosts non-failed hosts; protect against broken cluster.removeHost
for i := 0; i < maxHosts; i++ {
host, herr := c.host(usePrimary)
if herr != nil {
return status, nil, errors.Wrapf(herr, "getting host, previous err: %v", err)
}
// doRequest implements expotential backoff
status, body, err = c.doRequest(host, method, path, c.augmentHeaders(headers), data)
if err == nil {
break
}
if c.manualServerURI == nil {
if usePrimary {
c.primaryLock.Lock()
c.primaryURI = nil
c.primaryLock.Unlock()
} else {
c.logger.Printf("removing host (%s) due to '%v'\n", host.Normalize(), err)
c.cluster.RemoveHost(host)
}
}
}
if err != nil {
err = errors.Wrap(err, ErrTriedMaxHosts.Error())
}
return status, body, err
}
// host returns the first URI that applies, in this order:
// - a non-nil manualServerURI
// - primary URI (if usePrimary = true)
// - the next host from the node list (round-robin)
func (c *Client) host(usePrimary bool) (*pnet.URI, error) {
if c.manualServerURI != nil {
return c.manualServerURI, nil
}
var host *pnet.URI
if usePrimary {
c.primaryLock.RLock()
host = c.primaryURI
c.primaryLock.RUnlock()
if host == nil {
c.primaryLock.Lock()
if c.primaryURI == nil {
node, err := c.fetchPrimaryNode()
if err != nil {
c.primaryLock.Unlock()
return nil, errors.Wrap(err, "fetching primary node")
}
if host, err = pnet.NewURIFromAddress(fmt.Sprintf("%s://%s:%d", node.Scheme, node.Host, node.Port)); err != nil {
return nil, errors.Wrap(err, "parsing primary node URL")
}
} else {
host = c.primaryURI
}
c.primaryURI = host
c.primaryLock.Unlock()
}
} else {
// get a host from the cluster
host = c.cluster.Host()
if host == nil {
return nil, ErrEmptyCluster
}
}
return host, nil
}
// doRequest creates and performs an http request.
func (c *Client) doRequest(host *pnet.URI, method, path string, headers map[string]string, data []byte) (int, []byte, error) {
var (
req *http.Request
resp *http.Response
err error
sleepTime time.Duration
rand = rand.New(rand.NewSource(time.Now().UnixNano()))
)
for retry := 0; ; {
if req, err = buildRequest(host, method, path, headers, data); err != nil {
return 0, nil, errors.Wrap(err, "building request")
}
if resp, err = c.client.Do(req); err != nil {
return 0, nil, errors.Wrap(err, "sending request")
}
if warning := resp.Header.Get("warning"); warning != "" {
c.logger.Warnf(warning)
}
buf := bytes.NewBuffer(make([]byte, 0, 1+resp.ContentLength))
_, err = buf.ReadFrom(resp.Body)
_ = resp.Body.Close()
if err != nil {
return resp.StatusCode, nil, errors.Wrap(err, "reading response body")
}
switch {
case resp.StatusCode >= 200 && resp.StatusCode < 300:
// [200, 300): OK
return resp.StatusCode, buf.Bytes(), nil
case resp.StatusCode == 409:
// 409 Conflict
return resp.StatusCode, buf.Bytes(), nil
case resp.StatusCode == 412:
// 412 Precondition Failed
return resp.StatusCode, buf.Bytes(), nil
case resp.StatusCode == 429:
// 429 Too Many Requests
// A Retry-After header might be included to this response indicating how long to wait before making a new request.
if ms, _ := strconv.Atoi(resp.Header.Get("Retry-After")); ms > 0 {
sleepTime = time.Duration(ms) * time.Millisecond
} else {
sleepTime = time.Duration(1<<uint(retry))*time.Second + time.Duration(rand.Intn(1000))*time.Millisecond
}
case resp.StatusCode > 400 && resp.StatusCode < 500:
// Pilosa nodes sometimes return 400, we retry in that case.
// (400, 500): No need to retry in other 4xx cases.
return resp.StatusCode, nil, errors.New(strings.TrimSpace(buf.String()))
case resp.StatusCode == 503:
// This indicates that Pilosa is not ready to service this request,
// typically during startup. In this case, it's ok to give Pilosa
// some time and try again.
sleepTime = time.Duration(1<<uint(retry))*time.Second + time.Duration(rand.Intn(1000))*time.Millisecond
default:
err = errors.Errorf("server error (%d) %s: %s", resp.StatusCode, resp.Status, strings.TrimSpace(buf.String()))
sleepTime = time.Duration(1<<uint(retry))*time.Second + time.Duration(rand.Intn(1000))*time.Millisecond
}
// Retry logic
if retry >= c.maxRetries {
// If the error here is nil, we still want to return an error because
// we've hit the max retries limit. If an error exists, wrap it.
errMsg := fmt.Sprintf("max retries (%d) exceeded", c.maxRetries)
if err == nil {
return resp.StatusCode, nil, errors.New(errMsg)
}
return resp.StatusCode, nil, errors.Wrap(err, errMsg)
}
// The client can continue retrying after it has reached the maxBackoff time.
if sleepTime > c.maxBackoff {
return resp.StatusCode, nil, errors.Wrapf(err, "max backoff (%s) time exceeded", c.maxBackoff)
}
retry++
c.logger.Errorf("request failed with: '%v' status: %d, retrying %d after %v ", err, resp.StatusCode, retry, sleepTime)
time.Sleep(sleepTime)
}
// Unreachable code
}
// statusToNodeShardsForIndex finds the hosts which contains shards for the given index
func (c *Client) statusToNodeShardsForIndex(status Status, indexName string) (map[uint64]*pnet.URI, error) {
result := make(map[uint64]*pnet.URI)
if maxShard, ok := status.indexMaxShard[indexName]; ok {
for shard := 0; shard <= int(maxShard); shard++ {
fragmentNodes, err := c.fetchFragmentNodes(indexName, uint64(shard))
if err != nil {
return nil, err
}
if len(fragmentNodes) == 0 {
return nil, ErrNoFragmentNodes
}
node := fragmentNodes[0]
uri := &pnet.URI{
Host: node.Host,
Port: node.Port,
Scheme: node.Scheme,
}
result[uint64(shard)] = uri
}
} else {
return nil, ErrNoShard
}
return result, nil
}
func (c *Client) augmentHeaders(headers map[string]string) map[string]string {
if headers == nil {
headers = map[string]string{}
}
// TODO: move the following block to NewClient once cluster-resize support branch is merged.
version := strings.TrimPrefix(Version, "v")
headers["User-Agent"] = fmt.Sprintf("pilosa/client/%s", version)
return headers
}
// FindFieldKeys looks up the IDs associated with specified keys in a field.
// If a key does not exist, the result will not include it.
func (c *Client) FindFieldKeys(field *Field, keys ...string) (map[string]uint64, error) {
path := fmt.Sprintf("/internal/translate/field/%s/%s/keys/find", field.index.name, field.name)
reqData, err := json.Marshal(keys)
if err != nil {
return nil, errors.Wrap(err, "marshalling request")
}
headers := c.augmentHeaders(map[string]string{
"Content-Type": "application/json",
"Accept": "application/json",
})
status, body, err := c.HTTPRequest(http.MethodPost, path, reqData, headers)
if err != nil {
return nil, errors.Wrap(err, "executing request")
}
if status != http.StatusOK {
return nil, errors.Errorf("find field keys request failed (status: %d): %q", status, string(body))
}
var result map[string]uint64
err = json.Unmarshal(body, &result)
if err != nil {
return nil, errors.Wrap(err, "unmarshalling response")
}
return result, nil
}
// CreateFieldKeys looks up the IDs associated with specified keys in a field.
// If a key does not exist, it will be created.
func (c *Client) CreateFieldKeys(field *Field, keys ...string) (map[string]uint64, error) {
path := fmt.Sprintf("/internal/translate/field/%s/%s/keys/create", field.index.name, field.name)
reqData, err := json.Marshal(keys)
if err != nil {
return nil, errors.Wrap(err, "marshalling request")
}
headers := c.augmentHeaders(map[string]string{
"Content-Type": "application/json",
"Accept": "application/json",
})
status, body, err := c.httpRequest(http.MethodPost, path, reqData, headers, field.options.foreignIndex == "")
if err != nil {
return nil, errors.Wrap(err, "executing request")
}
if status != http.StatusOK {
return nil, errors.Errorf("find field keys request failed (status: %d): %q", status, string(body))
}
var result map[string]uint64
err = json.Unmarshal(body, &result)
if err != nil {
return nil, errors.Wrap(err, "unmarshalling response")
}
return result, nil
}
// FindIndexKeys looks up the IDs associated with specified column keys in an index.
// If a key does not exist, the result will not include it.
func (c *Client) FindIndexKeys(idx *Index, keys ...string) (map[string]uint64, error) {
path := fmt.Sprintf("/internal/translate/index/%s/keys/find", idx.name)
reqData, err := json.Marshal(keys)
if err != nil {
return nil, errors.Wrap(err, "marshalling request")
}
headers := c.augmentHeaders(map[string]string{
"Content-Type": "application/json",
"Accept": "application/json",
})
status, body, err := c.HTTPRequest(http.MethodPost, path, reqData, headers)
if err != nil {
return nil, errors.Wrap(err, "executing request")
}
if status != http.StatusOK {
return nil, errors.Errorf("find field keys request failed (status: %d): %q", status, string(body))
}
var result map[string]uint64
err = json.Unmarshal(body, &result)
if err != nil {
return nil, errors.Wrap(err, "unmarshalling response")
}
return result, nil
}
// CreateIndexKeys looks up the IDs associated with specified column keys in an index.
// If a key does not exist, it will be created.
func (c *Client) CreateIndexKeys(idx *Index, keys ...string) (map[string]uint64, error) {
path := fmt.Sprintf("/internal/translate/index/%s/keys/create", idx.name)
reqData, err := json.Marshal(keys)
if err != nil {
return nil, errors.Wrap(err, "marshalling request")
}
headers := c.augmentHeaders(map[string]string{
"Content-Type": "application/json",
"Accept": "application/json",
})
status, body, err := c.HTTPRequest(http.MethodPost, path, reqData, headers)
if err != nil {
return nil, errors.Wrap(err, "executing request")
}
if status != http.StatusOK {
return nil, errors.Errorf("find field keys request failed (status: %d): %q", status, string(body))
}
var result map[string]uint64
err = json.Unmarshal(body, &result)
if err != nil {
return nil, errors.Wrap(err, "unmarshalling response")
}
return result, nil
}
type TransactionResponse struct {
Transaction *pilosa.Transaction `json:"transaction,omitempty"`
Error string `json:"error,omitempty"`
}
// StartTransaction tries to start a new transaction in Pilosa. It
// will continue trying until at least requestTimeout time has
// passed. If it fails due to an exclusive transaction already
// existing, it will return that transaction along with a non-nil
// error.
func (c *Client) StartTransaction(id string, timeout time.Duration, exclusive bool, requestTimeout time.Duration) (*pilosa.Transaction, error) {
return c.startTransaction(id, timeout, exclusive, time.Now().Add(requestTimeout))
}
func (c *Client) startTransaction(id string, timeout time.Duration, exclusive bool, deadline time.Time) (*pilosa.Transaction, error) {
trns := pilosa.Transaction{
ID: id,
Timeout: timeout,
Exclusive: exclusive,
}
bod, err := json.Marshal(&trns)
if err != nil {
return nil, errors.Wrap(err, "marshalling transaction")
}
status, data, err := c.httpRequest("POST", "/transaction", bod, defaultJSONHeaders(), true)
if status == http.StatusConflict && time.Now().Before(deadline) {
// if we're getting StatusConflict after all the usual timeouts/retries, keep retrying until the deadline
time.Sleep(time.Second)
return c.startTransaction(id, timeout, exclusive, deadline)
}
if err != nil {
return nil, err
}
tr := &TransactionResponse{}
uerr := json.Unmarshal(data, &tr)
if uerr != nil {
if err != nil {
return nil, errors.Wrap(err, "unmarshal failed after")
}
return nil, errors.Wrap(uerr, "couldn't decode body")
}
if tr.Error != "" {
err = errors.New(tr.Error)
}
return tr.Transaction, err
}
func (c *Client) FinishTransaction(id string) (*pilosa.Transaction, error) {
_, data, err := c.httpRequest("POST", "/transaction/"+id+"/finish", nil, defaultJSONHeaders(), true)
if err != nil && len(data) == 0 {
return nil, err
}
tr := &TransactionResponse{}
uerr := json.Unmarshal(data, &tr)
if uerr != nil {
if err != nil {
return nil, errors.Wrap(err, "unmarshal failed after")
}
return nil, errors.Wrap(uerr, "couldn't decode body")
}
if tr.Error != "" {
err = errors.New(tr.Error)
}
return tr.Transaction, err
}
func (c *Client) Transactions() (map[string]*pilosa.Transaction, error) {
_, respData, err := c.httpRequest("GET", "/transactions", nil, defaultJSONHeaders(), true)
if err != nil {
return nil, errors.Wrap(err, "getting transactions")
}
trnsMap := make(map[string]*pilosa.Transaction)
err = json.Unmarshal(respData, &trnsMap)
if err != nil {
return nil, errors.Wrap(err, "unmarshalling transactions")
}
return trnsMap, nil
}
func (c *Client) GetTransaction(id string) (*pilosa.Transaction, error) {
_, data, err := c.httpRequest("GET", "/transaction/"+id, nil, defaultJSONHeaders(), true)
if err != nil {
return nil, err
}
tr := &TransactionResponse{}
uerr := json.Unmarshal(data, &tr)
if uerr != nil {
if err != nil {
return nil, errors.Wrap(err, "unmarshal failed after")
}
return nil, errors.Wrap(uerr, "couldn't decode body")
}
if tr.Error != "" {
err = errors.New(tr.Error)
}
return tr.Transaction, err
}
func defaultProtobufHeaders() map[string]string {
return map[string]string{
"Content-Type": "application/x-protobuf",
"Accept": "application/x-protobuf",
"PQL-Version": PQLVersion,
}
}
func defaultJSONHeaders() map[string]string {
return map[string]string{
"Content-Type": "application/json",
"Accept": "application/json",
"PQL-Version": PQLVersion,
}
}
func buildRequest(host *pnet.URI, method, path string, headers map[string]string, data []byte) (*http.Request, error) {
request, err := http.NewRequest(method, host.Normalize()+path, bytes.NewReader(data))
if err != nil {
return nil, err
}
for k, v := range headers {
request.Header.Set(k, v)
}
return request, nil
}
func newHTTPClient(options *ClientOptions) *http.Client {
transport := &http.Transport{
Dial: (&net.Dialer{
Timeout: options.ConnectTimeout,
}).Dial,
TLSClientConfig: options.TLSConfig,
MaxIdleConnsPerHost: options.PoolSizePerRoute,
MaxIdleConns: options.TotalPoolSize,
}
return &http.Client{
Transport: transport,
Timeout: options.SocketTimeout,
}
}
func makeRequestData(query string, options *QueryOptions) ([]byte, error) {
request := &pb.QueryRequest{
Query: query,
Shards: options.Shards,
}
r, err := proto.Marshal(request)
if err != nil {
return nil, errors.Wrap(err, "marshaling request to protobuf")
}
return r, nil
}
func makeRoaringImportPath(field *Field, shard uint64, params url.Values) string {
return fmt.Sprintf("/index/%s/field/%s/import-roaring/%d?%s",
field.index.name, field.name, shard, params.Encode())
}
type viewImports map[string]*roaring.Bitmap
// ClientOptions control the properties of client connection to the server.
type ClientOptions struct {
SocketTimeout time.Duration
ConnectTimeout time.Duration
PoolSizePerRoute int
TotalPoolSize int
TLSConfig *tls.Config
manualServerAddress bool
tracer opentracing.Tracer
retries *int
stats stats.StatsClient
nat map[pnet.URI]pnet.URI
}
func (co *ClientOptions) addOptions(options ...ClientOption) error {
for _, option := range options {
err := option(co)
if err != nil {
return err
}
}
return nil
}
// ClientOption is used when creating a PilosaClient struct.
type ClientOption func(options *ClientOptions) error
// OptClientSocketTimeout is the maximum idle socket time in nanoseconds
func OptClientSocketTimeout(timeout time.Duration) ClientOption {
return func(options *ClientOptions) error {
options.SocketTimeout = timeout
return nil
}
}
// OptClientConnectTimeout is the maximum time to connect in nanoseconds.
func OptClientConnectTimeout(timeout time.Duration) ClientOption {
return func(options *ClientOptions) error {
options.ConnectTimeout = timeout
return nil
}
}
// OptClientPoolSizePerRoute is the maximum number of active connections in the pool to a host.
func OptClientPoolSizePerRoute(size int) ClientOption {
return func(options *ClientOptions) error {
options.PoolSizePerRoute = size
return nil
}
}
// OptClientTotalPoolSize is the maximum number of connections in the pool.
func OptClientTotalPoolSize(size int) ClientOption {
return func(options *ClientOptions) error {
options.TotalPoolSize = size
return nil
}
}
// OptClientTLSConfig contains the TLS configuration.
func OptClientTLSConfig(config *tls.Config) ClientOption {
return func(options *ClientOptions) error {
options.TLSConfig = config
return nil
}
}
// OptClientManualServerAddress forces the client use only the manual server address
func OptClientManualServerAddress(enabled bool) ClientOption {
return func(options *ClientOptions) error {
options.manualServerAddress = enabled
return nil
}
}
// OptClientTracer sets the Open Tracing tracer
// See: https://opentracing.io
func OptClientTracer(tracer opentracing.Tracer) ClientOption {
return func(options *ClientOptions) error {
options.tracer = tracer
return nil
}
}
// OptClientRetries sets the number of retries on HTTP request failures.
func OptClientRetries(retries int) ClientOption {
return func(options *ClientOptions) error {
if retries < 0 {
return errors.New("retries must be non-negative")
}
options.retries = &retries
return nil
}
}
// OptClientStatsClient sets a stats client, such as Prometheus
func OptClientStatsClient(stats stats.StatsClient) ClientOption {
return func(options *ClientOptions) error {
options.stats = stats
return nil
}
}
// OptClientNAT sets a NAT map used to translate the advertised URI to something
// else (for example, when accessing pilosa running in docker).
func OptClientNAT(nat map[string]string) ClientOption {
return func(options *ClientOptions) error {
// covert the strings to URIs
m := make(map[pnet.URI]pnet.URI)
for k, v := range nat {
if kuri, err := pnet.NewURIFromAddress(k); err != nil {
return errors.Wrapf(err, "converting string to URI: %s", k)
} else if vuri, err := pnet.NewURIFromAddress(v); err != nil {
return errors.Wrapf(err, "converting string to URI: %s", v)
} else {
m[*kuri] = *vuri
}
}
options.nat = m
return nil
}
}
func (co *ClientOptions) withDefaults() (updated *ClientOptions) {
// copy options so the original is not updated
updated = &ClientOptions{}
*updated = *co
// impose defaults
if updated.SocketTimeout <= 0 {
updated.SocketTimeout = time.Second * 300
}
if updated.ConnectTimeout <= 0 {
updated.ConnectTimeout = time.Second * 60
}
if updated.PoolSizePerRoute <= 0 {
updated.PoolSizePerRoute = 50
}
if updated.TotalPoolSize <= 0 {
updated.TotalPoolSize = 500
}
if updated.TLSConfig == nil {
updated.TLSConfig = &tls.Config{}
}
if updated.retries == nil {
retries := 2
updated.retries = &retries
}
return
}
// QueryOptions contains options to customize the Query function.
type QueryOptions struct {
// Shards restricts query to a subset of shards. Queries all shards if nil.
Shards []uint64
}
func (qo *QueryOptions) addOptions(options ...interface{}) error {
for i, option := range options {
switch o := option.(type) {
case nil:
if i != 0 {
return ErrInvalidQueryOption
}
continue
case *QueryOptions:
if i != 0 {
return ErrInvalidQueryOption
}
*qo = *o
case QueryOption:
err := o(qo)
if err != nil {
return err
}
default:
return ErrInvalidQueryOption
}
}
return nil
}
// QueryOption is used when using options with a client.Query,
type QueryOption func(options *QueryOptions) error
// OptQueryShards restricts the set of shards on which a query operates.
func OptQueryShards(shards ...uint64) QueryOption {
return func(options *QueryOptions) error {
options.Shards = append(options.Shards, shards...)
return nil
}
}
// ImportOptions are the options for controlling the importer
type ImportOptions struct {
threadCount int
batchSize int
wantRoaring *bool
clear bool
skipSort bool
}
// ImportOption is used when running imports.
type ImportOption func(options *ImportOptions) error
// OptImportThreadCount is the number of goroutines allocated for import.
func OptImportThreadCount(count int) ImportOption {
return func(options *ImportOptions) error {
options.threadCount = count
return nil
}
}
// OptImportBatchSize is the number of records read before importing them.
func OptImportBatchSize(batchSize int) ImportOption {
return func(options *ImportOptions) error {
options.batchSize = batchSize
return nil
}
}
// OptImportClear sets clear import, which clears bits instead of setting them.
func OptImportClear(clear bool) ImportOption {
return func(options *ImportOptions) error {
options.clear = clear
return nil
}
}
// OptImportRoaring enables importing using roaring bitmaps which is more performant.
func OptImportRoaring(enable bool) ImportOption {
return func(options *ImportOptions) error {
options.wantRoaring = &enable
return nil
}
}
// OptImportSort tells the importer whether or not to sort batches of records, on
// by default. Sorting imposes some performance cost, especially on data that's
// already sorted, but dramatically improves performance in pathological
// cases. It is enabled by default because the pathological cases are awful,
// and the performance hit is comparatively small, but the performance cost can
// be significant if you know your data is sorted.
func OptImportSort(sorting bool) ImportOption {
return func(options *ImportOptions) error {
// skipSort is expressed negatively because we want to
// keep sorting enabled by default, so the zero value should
// be that default behavior. The client option expresses it
// positively because that's easier for API users.
options.skipSort = !sorting
return nil
}
}
type fragmentNodeRoot struct {
URI fragmentNode `json:"uri"`
}
type fragmentNode struct {
Scheme string `json:"scheme"`
Host string `json:"host"`
Port uint16 `json:"port"`
}
func newFragmentNodeFromURI(uri *pnet.URI) fragmentNode {
return fragmentNode{
Scheme: uri.Scheme,
Host: uri.Host,
Port: uri.Port,
}
}
func (node fragmentNode) URI() *pnet.URI {
return &pnet.URI{
Scheme: node.Scheme,
Host: node.Host,
Port: node.Port,
}
}
// Info contains the configuration/host information from a Pilosa server.
type Info struct {
ShardWidth uint64 `json:"shardWidth"` // width of each shard
Memory uint64 `json:"memory"` // approximate host physical memory
CPUType string `json:"cpuType"` // "brand name string" from cpuid
CPUPhysicalCores int `json:"CPUPhysicalCores"` // physical cores (cpuid)
CPULogicalCores int `json:"CPULogicalCores"` // logical cores cpuid
CPUMHz uint64 `json:"CPUMHz"` // estimated clock speed
}
// Status contains the status information from a Pilosa server.
type Status struct {
Nodes []StatusNode `json:"nodes"`
State string `json:"state"`
LocalID string `json:"localID"`
indexMaxShard map[string]uint64
}
// StatusNode contains information about a node in the cluster.
type StatusNode struct {
ID string `json:"id"`
URI StatusURI `json:"uri"`
IsPrimary bool `json:"isPrimary"`
}
// StatusURI contains node information.
type StatusURI struct {
Scheme string `json:"scheme"`
Host string `json:"host"`
Port uint16 `json:"port"`
}
// URI returns the StatusURI as a URI.
func (s StatusURI) URI() pnet.URI {
return pnet.URI{
Scheme: s.Scheme,
Host: s.Host,
Port: s.Port,
}
}
// SchemaInfo contains the indexes.
type SchemaInfo struct {
Indexes []SchemaIndex `json:"indexes"`
}
// SchemaIndex contains index information.
type SchemaIndex struct {
Name string `json:"name"`
CreatedAt int64 `json:"createdAt,omitempty"`
Options SchemaOptions `json:"options"`
Fields []SchemaField `json:"fields"`
Shards []uint64 `json:"shards"`
ShardWidth uint64 `json:"shardWidth"`
}
// SchemaField contains field information.
type SchemaField struct {
Name string `json:"name"`
CreatedAt int64 `json:"createdAt,omitempty"`
Options SchemaOptions `json:"options"`
}
// SchemaOptions contains options for a field or an index.
type SchemaOptions struct {
FieldType FieldType `json:"type"`
CacheType string `json:"cacheType"`
CacheSize uint `json:"cacheSize"`
TimeQuantum string `json:"timeQuantum"`
Min pql.Decimal `json:"min"`
Max pql.Decimal `json:"max"`
Scale int64 `json:"scale"`
Keys bool `json:"keys"`
NoStandardView bool `json:"noStandardView"`
TrackExistence bool `json:"trackExistence"`
TimeUnit string `json:"timeUnit"`
}
func (so SchemaOptions) asIndexOptions() *IndexOptions {
return &IndexOptions{
keys: so.Keys,
keysSet: true,
trackExistence: so.TrackExistence,
trackExistenceSet: true,
}
}
func (so SchemaOptions) asFieldOptions() *FieldOptions {
return &FieldOptions{
fieldType: so.FieldType,
cacheSize: int(so.CacheSize),
cacheType: CacheType(so.CacheType),
timeQuantum: TimeQuantum(so.TimeQuantum),
min: so.Min,
max: so.Max,
scale: so.Scale,
keys: so.Keys,
noStandardView: so.NoStandardView,
timeUnit: so.TimeUnit,
}
}
type exportReader struct {
client *Client
shardURIs map[uint64]*pnet.URI
field *Field
body []byte
bodyIndex int
currentShard uint64
shardCount uint64
}
func newExportReader(client *Client, shardURIs map[uint64]*pnet.URI, field *Field) *exportReader {
return &exportReader{
client: client,
shardURIs: shardURIs,
field: field,
shardCount: uint64(len(shardURIs)),
}
}
// Read updates the passed array with the exported CSV data and returns the number of bytes read
func (r *exportReader) Read(p []byte) (n int, err error) {
if r.currentShard >= r.shardCount {
err = io.EOF
return
}
if r.body == nil {
uri := r.shardURIs[r.currentShard]
headers := map[string]string{
"Accept": "text/csv",
}
path := fmt.Sprintf("/export?index=%s&field=%s&shard=%d",
r.field.index.Name(), r.field.Name(), r.currentShard)
_, respData, err := r.client.doRequest(uri, "GET", path, headers, nil)
if err != nil {
return 0, errors.Wrap(err, "doing export request")
}
r.body = respData
r.bodyIndex = 0
}
n = copy(p, r.body[r.bodyIndex:])
r.bodyIndex += n
if n >= len(r.body) {
r.body = nil
r.currentShard++
}
return
}