featurebase/dax/queryer/translator.go
Seebs 244d80753e reuse clients instead of making new clients
Buckle in, this one's a ride.

This is attached to the same PR as a fix for exiting abruptly
during some tests because I ran into that issue, and comprehended
it, while trying to track down weird and sporadic test failures
that were actually this issue.

The actual, underlying, problem: `make test`, by running all the
tests at once, was hitting a bug that was mostly effectively
triggered by running the `dax/test/dax` tests, and the top-level
`featurebase/v3` tests, at the same time. However, the interaction
was nothing as obvious as temporary files, etcd configuration,
or whatever.

We were running out of port numbers.

The tests were using a bit over 30k simultaneous established TCP
connections, each to different ports, because we were creating
new clients for basically every single operation. For instance,
in a single SQL test that did an import and then a read, we
were creating a new client for each field written to, and then
also creating a new client for each field in results that needed
key translation. And none of these clients were closed or
timed out in any way. In fact, Go doesn't really *do* "closing"
of clients; the closest is that an http.Client can be told to
close idle connections that it has been keeping open.

The worst offenders were both named `fbClient`, and were nigh-identical,
except one of them was implemented as a method on `importer` in the
IDK tree, and one was a standalone function.

It may seem surprising that the method on `importer` is using a shared
client pool for all importers, rather than a new pool for each
importer. This is because we potentially make quite a few importers
during tests.

Before this, running either of the dax tests or the top-level
tests would show well over ten thousand simultaneous ESTABLISHED
connections. After this, the dax tests used nearly twenty.

The problem with port consumption like this, while more noticeable
on MacOS, is also something we could hit on the CI runners, especially
if a single runner ended up with more than one test suite running
at the same time. This probably manifests as sporadic very strange
failures of CI, with messages about "cannot assign requested address".
(Note that an outgoing connection to a successfully-created port
requires *another* port to be assigned for the outbound socket.)

This was complicated dramatically by the fact that, for some
utterly cursed reason, it was *especially* common for the point
at which we hit this, in the top-level featurebase tests, to be
running one of the backup tests in TestVariousQueries, and
specifically, to be hitting it on the dataframe part of the
backup... Which is to say, on the *one* path in the backup function
that called log.Fatal, and thus terminated the featurebase process
abruptly without further commentary.
2023-03-06 13:12:22 -06:00

343 lines
9.9 KiB
Go

package queryer
import (
"context"
"net/http"
"sync"
pilosa "github.com/featurebasedb/featurebase/v3"
fbclient "github.com/featurebasedb/featurebase/v3/client"
"github.com/featurebasedb/featurebase/v3/dax"
"github.com/featurebasedb/featurebase/v3/dax/controller/partitioner"
"github.com/featurebasedb/featurebase/v3/disco"
"github.com/featurebasedb/featurebase/v3/encoding/proto"
"github.com/featurebasedb/featurebase/v3/errors"
)
// Ensure type implements interface.
var _ Translator = (*serverlessTranslator)(nil)
type serverlessTranslator struct {
controller dax.Controller
}
func NewServerlessTranslator(controller dax.Controller) *serverlessTranslator {
return &serverlessTranslator{
controller: controller,
}
}
var fbClientCache = map[dax.Address]*fbclient.Client{}
var fbClientCacheMu sync.Mutex
func fbClient(address dax.Address) (*fbclient.Client, error) {
fbClientCacheMu.Lock()
defer fbClientCacheMu.Unlock()
client := fbClientCache[address]
if client != nil {
return client, nil
}
client, err := fbclient.NewClient(address.HostPort(),
fbclient.OptClientRetries(2),
fbclient.OptClientTotalPoolSize(1000),
fbclient.OptClientPoolSizePerRoute(400),
fbclient.OptClientPathPrefix(address.Path()),
//fbclient.OptClientStatsClient(m.stats),
)
if err != nil {
return nil, err
}
fbClientCache[address] = client
return client, nil
}
func (m *serverlessTranslator) CreateIndexKeys(ctx context.Context, table string, keys []string) (map[string]uint64, error) {
tkey := dax.TableKey(table)
qtid := tkey.QualifiedTableID()
qtbl, err := m.controller.TableByID(ctx, qtid)
if err != nil {
return nil, errors.Wrap(err, "getting table")
}
partitioner := partitioner.NewPartitioner()
// Get the partitions (and therefore, nodes) responsible for the keys.
pMap := partitioner.PartitionsForKeys(tkey, qtbl.PartitionN, keys...)
out := make(map[string]uint64)
for pNum := range pMap {
address, err := m.controller.IngestPartition(ctx, qtid, pNum)
if err != nil {
return nil, errors.Wrapf(err, "calling ingest-partition on table: %s, partition: %d", table, pNum)
}
fbClient, err := fbClient(address)
if err != nil {
return nil, errors.Wrap(err, "getting featurebase client")
}
idx := fbclient.NewIndex(table)
m, err := fbClient.CreateIndexKeys(idx, pMap[pNum]...)
if err != nil {
return nil, errors.Wrapf(err, "creating index keys on index: %s, partition: %d", table, pNum)
}
for k, v := range m {
out[k] = v
}
}
return out, nil
}
func (m *serverlessTranslator) CreateFieldKeys(ctx context.Context, table string, field string, keys []string) (map[string]uint64, error) {
qtid := dax.TableKey(table).QualifiedTableID()
address, err := m.controller.IngestPartition(ctx, qtid, dax.PartitionNum(0))
if err != nil {
return nil, errors.Wrapf(err, "calling ingest-partition on table: %s, partition: %d", table, dax.PartitionNum(0))
}
fbClient, err := fbClient(address)
if err != nil {
return nil, errors.Wrap(err, "getting featurebase client")
}
idx := fbclient.NewIndex(table)
fld := idx.Field(field)
return fbClient.CreateFieldKeys(fld, keys...)
}
func (m *serverlessTranslator) FindIndexKeys(ctx context.Context, table string, keys []string) (map[string]uint64, error) {
tkey := dax.TableKey(table)
qtid := tkey.QualifiedTableID()
qtbl, err := m.controller.TableByID(ctx, qtid)
if err != nil {
return nil, errors.Wrap(err, "getting table")
}
partitioner := partitioner.NewPartitioner()
// Get the partitions (and therefore, nodes) responsible for the keys.
pMap := partitioner.PartitionsForKeys(tkey, qtbl.PartitionN, keys...)
pNums := make([]dax.PartitionNum, 0, len(pMap))
for k := range pMap {
pNums = append(pNums, k)
}
translateNodes, err := m.controller.TranslateNodes(ctx, qtid, pNums...)
if err != nil {
return nil, errors.Wrapf(err, "getting translate nodes for partitions on table: %s", table)
}
out := make(map[string]uint64)
for _, tnode := range translateNodes {
address := tnode.Address
fbClient, err := fbClient(address)
if err != nil {
return nil, errors.Wrap(err, "getting featurebase client")
}
idx := fbclient.NewIndex(table)
nodeKeys := []string{}
for _, pNum := range tnode.Partitions {
nodeKeys = append(nodeKeys, pMap[pNum]...)
}
m, err := fbClient.FindIndexKeys(idx, nodeKeys...)
if err != nil {
return nil, errors.Wrapf(err, "finding index keys on index: %s", table)
}
for k, v := range m {
out[k] = v
}
}
return out, nil
}
func (m *serverlessTranslator) FindFieldKeys(ctx context.Context, table, field string, keys []string) (map[string]uint64, error) {
qtid := dax.TableKey(table).QualifiedTableID()
address, err := m.controller.IngestPartition(ctx, qtid, dax.PartitionNum(0))
if err != nil {
return nil, errors.Wrapf(err, "calling ingest-partition on table: %s, partition: %d", table, dax.PartitionNum(0))
}
fbClient, err := fbClient(address)
if err != nil {
return nil, errors.Wrap(err, "getting featurebase client")
}
idx := fbclient.NewIndex(table)
fld := idx.Field(field)
return fbClient.FindFieldKeys(fld, keys...)
}
func (m *serverlessTranslator) TranslateIndexIDs(ctx context.Context, index string, ids []uint64) ([]string, error) {
idsByPartition := splitIDsByPartition(index, ids, 1<<20) // TODO(jaffee), don't hardcode shardwidth...need to get this from index info
daxPartitions := make([]dax.PartitionNum, 0)
for partition := range idsByPartition {
daxPartitions = append(daxPartitions, partition)
}
qtid := dax.TableKey(index).QualifiedTableID()
nodes, err := m.controller.TranslateNodes(ctx, qtid, daxPartitions...)
if err != nil {
return nil, errors.Wrapf(err, "calling translate-nodes on table: %s, partitions: %v", index, daxPartitions)
}
// get translation from each node
idToKey := make(map[uint64]string)
for _, node := range nodes {
fbClient, err := fbClient(node.Address)
if err != nil {
return nil, errors.Wrap(err, "getting featurebase client")
}
reqIDs := make([]uint64, 0)
for _, partition := range node.Partitions {
reqIDs = append(reqIDs, idsByPartition[partition]...)
}
strings, err := makeTranslateIDsRequest(fbClient, index, "", reqIDs)
if err != nil {
return nil, errors.Wrapf(err, "translating on %v", node.Address)
}
for i, s := range strings {
idToKey[reqIDs[i]] = s
}
}
ret := make([]string, len(ids))
for i, id := range ids {
ret[i] = idToKey[id]
}
return ret, nil
}
func (m *serverlessTranslator) TranslateIndexIDSet(ctx context.Context, table string, ids map[uint64]struct{}) (map[uint64]string, error) {
idList := make([]uint64, 0, len(ids))
for id := range ids {
idList = append(idList, id)
}
stringList, err := m.TranslateIndexIDs(ctx, table, idList)
if err != nil {
return nil, errors.Wrapf(err, "translating index ids on table: %s", table)
}
ret := make(map[uint64]string)
for i, id := range idList {
ret[id] = stringList[i]
}
return ret, nil
}
func (m *serverlessTranslator) TranslateFieldIDs(ctx context.Context, tableKeyer dax.TableKeyer, field string, ids map[uint64]struct{}) (map[uint64]string, error) {
idList := make([]uint64, 0, len(ids))
for id := range ids {
idList = append(idList, id)
}
// TODO(tlt): convert TranslateFieldListIDs (and the other Translator
// interface methods) to TableKeyer.
index := string(tableKeyer.Key())
stringList, err := m.TranslateFieldListIDs(ctx, index, field, idList)
if err != nil {
return nil, errors.Wrapf(err, "translating field ids on field: %s, %s", tableKeyer, field)
}
ret := make(map[uint64]string)
for i, id := range idList {
ret[id] = stringList[i]
}
return ret, nil
}
func (m *serverlessTranslator) TranslateFieldListIDs(ctx context.Context, index, field string, ids []uint64) ([]string, error) {
qtid := dax.TableKey(index).QualifiedTableID()
address, err := m.controller.IngestPartition(ctx, qtid, dax.PartitionNum(0))
if err != nil {
return nil, errors.Wrapf(err, "calling ingest-partition on table: %s, partition: %d", index, dax.PartitionNum(0))
}
fbClient, err := fbClient(address)
if err != nil {
return nil, errors.Wrap(err, "getting featurebase client")
}
return makeTranslateIDsRequest(fbClient, index, field, ids)
}
func makeTranslateIDsRequest(fbClient *fbclient.Client, table, field string, ids []uint64) ([]string, error) {
method := "POST"
path := "/internal/translate/ids"
headers := map[string]string{
"Content-Type": "application/x-protobuf",
"Accept": "application/x-protobuf",
}
req := &pilosa.TranslateIDsRequest{
Index: table,
Field: field,
IDs: ids,
}
ser := proto.Serializer{}
data, err := ser.Marshal(req)
if err != nil {
return nil, errors.Wrap(err, "marshaling translate ids request")
}
status, body, err := fbClient.HTTPRequest(method, path, data, headers)
if err != nil {
return nil, errors.Wrap(err, "http request")
} else if status != http.StatusOK {
return nil, errors.Wrapf(err, "http request status code: %d", status)
}
idsResp := &pilosa.TranslateIDsResponse{}
if err := ser.Unmarshal(body, idsResp); err != nil {
return nil, errors.Wrap(err, "unmarshaling translate ids request")
}
return idsResp.Keys, nil
}
func splitIDsByShard(ids []uint64, shardWidth uint64) map[dax.ShardNum][]uint64 {
ret := make(map[dax.ShardNum][]uint64)
for _, id := range ids {
shardIDs, ok := ret[dax.ShardNum(id/shardWidth)]
if !ok {
shardIDs = make([]uint64, 0)
}
ret[dax.ShardNum(id/shardWidth)] = append(shardIDs, id)
}
return ret
}
func splitIDsByPartition(index string, ids []uint64, shardWidth uint64) map[dax.PartitionNum][]uint64 {
idsByShard := splitIDsByShard(ids, shardWidth)
partitioner := partitioner.NewPartitioner()
ret := make(map[dax.PartitionNum][]uint64)
for shard, ids := range idsByShard {
// get partition for shard
// TODO: need to get partitionN from the table, instead of using the default.
partitionNum := partitioner.ShardToPartition(dax.TableKey(index), shard, disco.DefaultPartitionN)
ret[partitionNum] = append(ret[partitionNum], ids...)
}
return ret
}