mirror of
https://github.com/featurebasedb/featurebase.git
synced 2026-08-28 10:54:59 +00:00
* Enable linter: stylecheck This enabled the stylecheck linter, but excludes some staticchecks for now. The following are ignored because they will take a bit of time to address, but the intention is to address them and remove them from the exclusion list. ST1000: at least one file in a package should have a package comment ST1003: golang naming standards ST1008: error should be returned as the last argument ST1016: methods on the same type should have the same receiver name ST1020: comment on exported function * Address ST1015 For some reason this failed in CI but not locally. I can't figure out why that check isn't happening locally. This just moves the switch statements around so that the `default` is the first (or last) item. * Adjust error string in test to match case-adjusted error * Remove TestCloseTimeout
821 lines
22 KiB
Go
821 lines
22 KiB
Go
package kafka
|
|
|
|
import (
|
|
"context"
|
|
"encoding/binary"
|
|
"encoding/json"
|
|
"fmt"
|
|
"hash/fnv"
|
|
"io"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
confluent "github.com/confluentinc/confluent-kafka-go/kafka"
|
|
"github.com/featurebasedb/featurebase/v3/idk"
|
|
"github.com/featurebasedb/featurebase/v3/idk/common"
|
|
"github.com/featurebasedb/featurebase/v3/idk/kafka/csrc"
|
|
"github.com/featurebasedb/featurebase/v3/logger"
|
|
"github.com/featurebasedb/featurebase/v3/pql"
|
|
"github.com/go-avro/avro"
|
|
liavro "github.com/linkedin/goavro/v2"
|
|
"github.com/pkg/errors"
|
|
"golang.org/x/sync/errgroup"
|
|
)
|
|
|
|
// PutSource represents a kafka put process that operates on one
|
|
// or more idk.Source.
|
|
type PutSource struct {
|
|
idk.ConfluentCommand
|
|
Topic string `help:"Kafka topic to post to."`
|
|
Subject string `help:"Kafka schema subject."`
|
|
BatchSize int `help:"Size of record batches to submit to Kafka."`
|
|
|
|
// FB specific config for setting partition key efficiently
|
|
FBPrimaryKeyFields []string
|
|
FBIndexName string
|
|
FBIDField string
|
|
|
|
Concurrency int `help:"Number of concurrent sources and indexing routines to launch."`
|
|
|
|
TrackProgress bool `help:"Periodically print status updates on how many records have been sourced." short:""`
|
|
|
|
ReplicationFactor int `help:"set replication factor for kafka cluster"`
|
|
NumPartitions int `help:"set partition for kafka cluster"`
|
|
// NewSource must be set by the user of Main before calling
|
|
// Main.Run. Main.Run will call this function "Concurrency" times. It
|
|
// is the job of this function to ensure that the concurrent
|
|
// sources which are started partition work appropriately. This is
|
|
// typically set up (by convention) in the Source's package in
|
|
// cmd.go
|
|
NewSource func() (idk.Source, error) `flag:"-"`
|
|
|
|
Log logger.Logger
|
|
|
|
progress *idk.ProgressTracker
|
|
|
|
schemaClient *csrc.Client
|
|
ConfigMap *confluent.ConfigMap `flag:"-"`
|
|
Target string
|
|
}
|
|
|
|
// NewPutSource returns a new instance of PutSource.
|
|
func NewPutSource() (*PutSource, error) {
|
|
var err error
|
|
p := PutSource{}
|
|
p.ConfluentCommand = idk.ConfluentCommand{}
|
|
p.KafkaBootstrapServers = []string{"localhost:9092"}
|
|
p.SchemaRegistryURL = "http://localhost:8081"
|
|
p.Topic = "defaulttopic"
|
|
p.Subject = "defaultsubject"
|
|
p.BatchSize = 1000
|
|
p.Log = logger.NewStandardLogger(os.Stderr)
|
|
p.ConfigMap, err = common.SetupConfluent(&p.ConfluentCommand)
|
|
return &p, err
|
|
}
|
|
|
|
// Run sends the source records to kafka based on concurrency.
|
|
func (p *PutSource) Run() error {
|
|
// Set up the schema registry client.
|
|
var auth *csrc.BasicAuth
|
|
if p.SchemaRegistryUsername != "" {
|
|
auth = &csrc.BasicAuth{
|
|
KafkaSchemaApiKey: p.SchemaRegistryUsername,
|
|
KafkaSchemaApiSecret: p.SchemaRegistryPassword,
|
|
}
|
|
}
|
|
p.schemaClient = csrc.NewClient(p.SchemaRegistryURL, nil, auth)
|
|
|
|
if p.TrackProgress {
|
|
p.progress = &idk.ProgressTracker{}
|
|
startTime := time.Now()
|
|
var wg sync.WaitGroup
|
|
defer wg.Wait()
|
|
doneCh := make(chan struct{})
|
|
defer func() { close(doneCh) }()
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
|
|
// Set up a timer to check progress every 10 seconds.
|
|
tick := time.NewTicker(10 * time.Second)
|
|
defer tick.Stop()
|
|
|
|
prev := uint64(0)
|
|
stalled := true
|
|
for {
|
|
progress := p.progress.Check()
|
|
switch {
|
|
case progress != prev:
|
|
// Forward progress continues.
|
|
p.Log.Printf("sourced %d records (%.2f records/minute)", progress, float64(progress)/time.Since(startTime).Minutes())
|
|
stalled = false
|
|
case stalled:
|
|
// We already told the user that it is stalled.
|
|
default:
|
|
// This is the start of a stall.
|
|
// No records have been sourced in the past 5 seconds.
|
|
p.Log.Printf("record sourcing stalled")
|
|
stalled = true
|
|
}
|
|
prev = progress
|
|
|
|
select {
|
|
case <-tick.C:
|
|
case <-doneCh:
|
|
// Generate a final status update.
|
|
p.Log.Printf("sourced %d records in %s", p.progress.Check(), time.Since(startTime))
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
|
|
// Create Producer instance
|
|
pr, err := confluent.NewProducer(p.ConfigMap)
|
|
if err != nil {
|
|
return errors.Wrap(err, "Failed to create producer")
|
|
}
|
|
// Create the topic if it does not exist.
|
|
err = CreateKafkaTopic(context.Background(), p.Topic, pr, p.NumPartitions, p.ReplicationFactor)
|
|
pr.Close()
|
|
|
|
if err != nil {
|
|
return errors.Wrap(err, "creating topic")
|
|
}
|
|
|
|
//Make producer channel buffered
|
|
err = p.ConfigMap.SetKey("go.produce.channel.size", 1)
|
|
if err != nil {
|
|
return errors.Wrap(err, "setting produce channel")
|
|
}
|
|
|
|
// TODO: i don't think we actually want to run these concurrently for now
|
|
// we can just iterate over them serially.
|
|
eg := errgroup.Group{}
|
|
for c := 0; c < p.Concurrency; c++ {
|
|
c := c
|
|
eg.Go(func() error {
|
|
// targets allowed are defined in datagen target, make sure they match
|
|
var err error
|
|
if p.Target == "kafka" {
|
|
err = p.runSource(c)
|
|
} else { // this is for target kafkastatic
|
|
err = p.runSourceJson(c)
|
|
}
|
|
if err != nil && err != io.EOF {
|
|
return err
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
err = eg.Wait()
|
|
if err != nil && err != io.EOF {
|
|
return errors.Wrap(err, "confluent.PutSource.Run")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func convertToJson(schema []idk.Field, record []interface{}) ([]byte, error) {
|
|
if len(schema) != len(record) {
|
|
return []byte{}, fmt.Errorf("length of schema %v and record %v don't match", len(schema), len(record))
|
|
}
|
|
mesg := make(map[string]interface{})
|
|
for i := range schema {
|
|
field, val := schema[i], record[i]
|
|
if valb, ok := val.([]byte); ok {
|
|
val = string(valb)
|
|
}
|
|
mesg[field.Name()] = val
|
|
}
|
|
|
|
return json.Marshal(mesg)
|
|
}
|
|
|
|
func (p *PutSource) getKeyFunc(schema []idk.Field) (func(vals []interface{}) ([]byte, error), error) {
|
|
if len(p.FBPrimaryKeyFields) > 0 {
|
|
indices := make([]int, len(p.FBPrimaryKeyFields))
|
|
for i, fieldName := range p.FBPrimaryKeyFields {
|
|
for j, f := range schema {
|
|
if f.Name() == fieldName {
|
|
indices[i] = j
|
|
break
|
|
}
|
|
}
|
|
}
|
|
return func(vals []interface{}) ([]byte, error) {
|
|
h := fnv.New64a()
|
|
h.Write([]byte(p.FBIndexName))
|
|
for pos, ind := range indices {
|
|
valb, ok := vals[ind].([]byte)
|
|
if !ok {
|
|
valS, ok := vals[ind].(string)
|
|
if !ok {
|
|
return nil, errors.Errorf("primary key value must be string or byte slice, but got %v of %[1]T", vals[ind])
|
|
}
|
|
valb = []byte(valS)
|
|
}
|
|
h.Write(valb)
|
|
if pos < len(indices)-1 {
|
|
h.Write([]byte{'|'})
|
|
}
|
|
}
|
|
partition := h.Sum64() % 256
|
|
ret := make([]byte, 8)
|
|
binary.BigEndian.PutUint64(ret, partition)
|
|
return ret, nil
|
|
}, nil
|
|
} else if p.FBIDField != "" {
|
|
return nil, errors.New("getting partition key for IDField not implemented")
|
|
}
|
|
return nil, nil // autogen doesn't need to set the key at all
|
|
}
|
|
|
|
func (p *PutSource) runSourceJson(c int) error {
|
|
p.Log.Printf("start source %d", c)
|
|
source, err := p.NewSource()
|
|
if err != nil {
|
|
return errors.Wrap(err, "getting source")
|
|
}
|
|
if p.progress != nil {
|
|
source = p.progress.Track(source)
|
|
}
|
|
|
|
// Create Producer instance
|
|
producer, err := confluent.NewProducer(p.ConfigMap)
|
|
if err != nil {
|
|
return errors.Wrap(err, "Failed to create producer")
|
|
}
|
|
|
|
finished := int32(0)
|
|
iter := int64(0)
|
|
doneChan := common.LaunchKafkaEventConfirmer(producer, &finished, &iter)
|
|
defer producer.Close()
|
|
var schema []idk.Field
|
|
var getKeyFunc func(vals []interface{}) ([]byte, error)
|
|
rec, err := source.Record()
|
|
if err == nil {
|
|
err = idk.ErrSchemaChange // always need to fetch the schema the first time
|
|
}
|
|
for ; ; rec, err = source.Record() {
|
|
if err == idk.ErrFlush {
|
|
continue
|
|
}
|
|
if err != nil || iter == 0 {
|
|
if err == idk.ErrSchemaChange || iter == 0 {
|
|
p.Log.Debugf("handle kafka schema\n")
|
|
schema = source.Schema()
|
|
getKeyFunc, err = p.getKeyFunc(schema)
|
|
if err != nil && err.Error() != "getting partition key for IDField not implemented" {
|
|
return errors.Wrap(err, "getting key func")
|
|
}
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
// in custom.go, we return nil when it is EOF
|
|
if rec == nil {
|
|
break
|
|
}
|
|
|
|
// map schema and records to json
|
|
data, err := convertToJson(schema, rec.Data())
|
|
if err != nil {
|
|
return errors.Wrap(err, "converting record to json")
|
|
}
|
|
// maybe we need a message key, but don't know what to do
|
|
p.Log.Debugf("put kafka record json: %v, %s, %s, %v\n", p.Topic, rec.Data()[0], data, err)
|
|
var key []byte
|
|
if getKeyFunc != nil {
|
|
key, err = getKeyFunc(rec.Data())
|
|
if err != nil {
|
|
return errors.Wrap(err, "trying to get partition key")
|
|
}
|
|
}
|
|
producer.ProduceChannel() <- &confluent.Message{
|
|
TopicPartition: confluent.TopicPartition{Topic: &p.Topic, Partition: confluent.PartitionAny},
|
|
Key: key,
|
|
Value: data,
|
|
}
|
|
atomic.AddInt64(&iter, 1)
|
|
}
|
|
atomic.AddInt32(&finished, 1)
|
|
<-doneChan
|
|
recNotFlushed := producer.Flush(15 * 10000) // 10ms
|
|
count := 0
|
|
for recNotFlushed != 0 {
|
|
recNotFlushed = producer.Flush(15 * 10000) // 10ms
|
|
count += 1
|
|
if count > 4 {
|
|
p.Log.Debugf("Tried more than 4 times to flush confluent producer")
|
|
break
|
|
}
|
|
}
|
|
|
|
if err != io.EOF {
|
|
return errors.Wrap(err, "getting record")
|
|
}
|
|
|
|
p.Log.Printf("Put %d generated records\n", iter)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (p *PutSource) runSource(c int) error {
|
|
p.Log.Printf("start source %d", c)
|
|
source, err := p.NewSource()
|
|
if err != nil {
|
|
return errors.Wrap(err, "getting source")
|
|
}
|
|
if p.progress != nil {
|
|
source = p.progress.Track(source)
|
|
}
|
|
|
|
// Create Producer instance
|
|
producer, err := confluent.NewProducer(p.ConfigMap)
|
|
if err != nil {
|
|
return errors.Wrap(err, "Failed to create producer")
|
|
}
|
|
defer producer.Close()
|
|
|
|
finished := int32(0)
|
|
iter := int64(0)
|
|
doneChan := common.LaunchKafkaEventConfirmer(producer, &finished, &iter)
|
|
|
|
var licodec *liavro.Codec
|
|
var resp *csrc.SchemaResponse
|
|
var schema []idk.Field
|
|
rec, err := source.Record()
|
|
if err == nil {
|
|
err = idk.ErrSchemaChange // always need to fetch the schema the first time
|
|
}
|
|
for ; ; rec, err = source.Record() {
|
|
if err == idk.ErrFlush {
|
|
continue
|
|
}
|
|
if err != nil || iter == 0 {
|
|
if err == idk.ErrSchemaChange || iter == 0 {
|
|
p.Log.Debugf("handle kafka schema\n")
|
|
schema = source.Schema()
|
|
|
|
recordSchema, err := idkSchemaToAvroRecordSchema(schema)
|
|
if err != nil {
|
|
return errors.Wrap(err, "converting schema to record schema")
|
|
}
|
|
|
|
b, err := json.Marshal(recordSchema)
|
|
if err != nil {
|
|
return errors.Wrap(err, "converting record schema to json")
|
|
}
|
|
|
|
schemaStr := string(b)
|
|
resp, err = p.schemaClient.PostSubjects(p.Subject, schemaStr)
|
|
if err != nil {
|
|
return errors.Wrap(err, "posting schema")
|
|
}
|
|
p.Log.Printf("Posted schema ID: %d\n", resp.ID)
|
|
|
|
licodec, err = liavro.NewCodec(schemaStr)
|
|
if err != nil {
|
|
return errors.Wrap(err, "li decoding schema")
|
|
}
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
|
|
// Convert record to a data structure suitable for kafka put.
|
|
recordMap, err := p.recordToMap(rec, schema)
|
|
if err != nil {
|
|
return errors.Wrap(err, "converting record to map")
|
|
}
|
|
data, err := endcodeAvro(int(resp.ID), licodec, recordMap)
|
|
if err != nil {
|
|
return errors.Wrap(err, "encoding record")
|
|
}
|
|
producer.ProduceChannel() <- &confluent.Message{
|
|
TopicPartition: confluent.TopicPartition{Topic: &p.Topic, Partition: confluent.PartitionAny},
|
|
//Key: []byte(strconv.Itoa(uid)),
|
|
Value: data,
|
|
}
|
|
atomic.AddInt64(&iter, 1) //increment message produced
|
|
}
|
|
atomic.AddInt32(&finished, 1) //signal no more messages
|
|
<-doneChan //wait till kafka acknowledges
|
|
|
|
recNotFlushed := producer.Flush(15 * 10000) // 10ms
|
|
count := 0
|
|
for recNotFlushed != 0 {
|
|
recNotFlushed = producer.Flush(10000) // 10ms
|
|
count += 1
|
|
if count > 4 {
|
|
p.Log.Debugf("Tried more than 4 times to flush confluent producer")
|
|
break
|
|
}
|
|
}
|
|
if err != io.EOF {
|
|
return errors.Wrap(err, "getting record")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// recordToMap converts an idk.Record to a map of interfaces based on the
|
|
// provided schema.
|
|
func (p *PutSource) recordToMap(rec idk.Record, schema []idk.Field) (map[string]interface{}, error) {
|
|
m := make(map[string]interface{})
|
|
data := rec.Data()
|
|
|
|
if len(schema) != len(data) {
|
|
return nil, errors.Errorf("number of data fields (%d) does not match schema (%d)", len(data), len(schema))
|
|
}
|
|
|
|
// TODO: return errors for cases where casting isn't ok
|
|
for i, field := range schema {
|
|
v := data[i]
|
|
if v == nil {
|
|
m[field.Name()] = nil
|
|
continue
|
|
}
|
|
switch fld := field.(type) {
|
|
case idk.BoolField:
|
|
b, err := toBool(v)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "converting value to bool")
|
|
}
|
|
m[fld.Name()] = map[string]interface{}{"boolean": b}
|
|
case idk.DateIntField:
|
|
if n, err := toInt64(v); err == nil {
|
|
m[fld.Name()] = map[string]interface{}{"long": n}
|
|
} else {
|
|
m[fld.Name()] = map[string]interface{}{"bytes": v}
|
|
}
|
|
|
|
case idk.DecimalField:
|
|
d, err := toDecimal(v, fld.Scale)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "converting value to decimal")
|
|
}
|
|
m[fld.Name()] = map[string]interface{}{"bytes": d}
|
|
case idk.IDArrayField:
|
|
switch vt := v.(type) {
|
|
case []uint64:
|
|
int64s := make([]int64, len(vt))
|
|
for i := range vt {
|
|
int64s[i] = int64(vt[i])
|
|
}
|
|
m[fld.Name()] = map[string]interface{}{"array": int64s}
|
|
case []int64:
|
|
m[fld.Name()] = map[string]interface{}{"array": vt}
|
|
}
|
|
case idk.IDField:
|
|
l, err := toInt64(v)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "converting value to int64")
|
|
}
|
|
m[fld.Name()] = map[string]interface{}{"long": l}
|
|
case idk.IntField:
|
|
l, err := toInt64(v)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "converting value to int64")
|
|
}
|
|
m[fld.Name()] = map[string]interface{}{"long": l}
|
|
case idk.RecordTimeField:
|
|
m[fld.Name()] = map[string]interface{}{"bytes": v}
|
|
case idk.TimestampField:
|
|
buf := make([]byte, 8)
|
|
u := uint64(v.(time.Time).Unix())
|
|
binary.BigEndian.PutUint64(buf, u)
|
|
m[fld.Name()] = map[string]interface{}{"bytes": buf}
|
|
case idk.SignedIntBoolKeyField:
|
|
l, err := toInt64(v)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "converting value to int64")
|
|
}
|
|
m[fld.Name()] = map[string]interface{}{"long": l}
|
|
case idk.StringArrayField:
|
|
m[fld.Name()] = map[string]interface{}{"array": v}
|
|
case idk.StringField:
|
|
s, err := toString(v)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "converting value to string")
|
|
}
|
|
m[fld.Name()] = map[string]interface{}{"string": s}
|
|
default:
|
|
m[fld.Name()] = v
|
|
}
|
|
}
|
|
|
|
return m, nil
|
|
}
|
|
|
|
// idkSchemaToAvroRecordSchema converts an []idk.Field to a RecordSchema.
|
|
func idkSchemaToAvroRecordSchema(fields []idk.Field) (*avro.RecordSchema, error) {
|
|
record := &avro.RecordSchema{
|
|
Name: "idk_datagen",
|
|
Namespace: "idk.datagen",
|
|
Doc: "idk-datagen",
|
|
Fields: make([]*avro.SchemaField, len(fields)),
|
|
}
|
|
|
|
for n, fld := range fields {
|
|
var schemaField = &avro.SchemaField{}
|
|
quantum := idk.QuantumOf(fld)
|
|
var ttl time.Duration = 0
|
|
if quantum != "" {
|
|
ttlTemp, err := idk.TTLOf(fld)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ttl = ttlTemp
|
|
}
|
|
ttlString := ttl.String()
|
|
cacheConfig := idk.CacheConfigOf(fld)
|
|
hasMutex := idk.HasMutex(fld)
|
|
|
|
switch typ := fld.(type) {
|
|
case idk.BoolField:
|
|
schemaField.Type = &avro.UnionSchema{
|
|
Types: []avro.Schema{
|
|
&avro.NullSchema{},
|
|
&avro.BooleanSchema{},
|
|
},
|
|
}
|
|
schemaField.Default = nil
|
|
|
|
case idk.DateIntField:
|
|
props := map[string]interface{}{
|
|
"fieldType": "dateInt", // TODO: namespace molecula custom properties?
|
|
"epoch": typ.Epoch.Format(typ.Layout),
|
|
"unit": typ.Unit,
|
|
"customUnit": typ.CustomUnit,
|
|
"layout": typ.Layout,
|
|
}
|
|
schemaField.Type = &avro.UnionSchema{
|
|
Types: []avro.Schema{
|
|
&avro.NullSchema{},
|
|
&avro.BytesSchema{Properties: props},
|
|
&avro.LongSchema{Properties: props},
|
|
},
|
|
}
|
|
schemaField.Default = nil
|
|
|
|
case idk.TimestampField:
|
|
props := map[string]interface{}{
|
|
"fieldType": "timestamp",
|
|
"granularity": typ.Granularity,
|
|
"layout": typ.Layout,
|
|
"epoch": typ.Epoch.Format(typ.Layout),
|
|
"unit": typ.Unit,
|
|
}
|
|
schemaField.Type = &avro.UnionSchema{
|
|
Types: []avro.Schema{
|
|
&avro.NullSchema{},
|
|
&avro.BytesSchema{Properties: props},
|
|
},
|
|
}
|
|
schemaField.Default = nil
|
|
|
|
case idk.DecimalField:
|
|
props := map[string]interface{}{
|
|
"fieldType": "decimal",
|
|
"scale": typ.Scale,
|
|
"precision": idk.DecimalPrecision,
|
|
}
|
|
schemaField.Type = &avro.UnionSchema{
|
|
Types: []avro.Schema{
|
|
&avro.NullSchema{},
|
|
&avro.BytesSchema{Properties: props},
|
|
},
|
|
}
|
|
schemaField.Default = nil
|
|
|
|
case idk.IDArrayField:
|
|
schemaField.Type = &avro.UnionSchema{
|
|
Types: []avro.Schema{
|
|
&avro.NullSchema{},
|
|
&avro.ArraySchema{
|
|
Items: &avro.LongSchema{
|
|
Properties: map[string]interface{}{
|
|
"quantum": quantum,
|
|
"ttl": ttlString,
|
|
"cacheType": string(cacheConfig.CacheType),
|
|
"cacheSize": strconv.Itoa(cacheConfig.CacheSize),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
schemaField.Default = nil
|
|
|
|
case idk.IDField:
|
|
schemaField.Type = &avro.UnionSchema{
|
|
Types: []avro.Schema{
|
|
&avro.NullSchema{},
|
|
&avro.LongSchema{
|
|
Properties: map[string]interface{}{
|
|
"fieldType": "id",
|
|
"mutex": hasMutex,
|
|
"quantum": quantum,
|
|
"ttl": ttlString,
|
|
"cacheType": string(cacheConfig.CacheType),
|
|
"cacheSize": strconv.Itoa(cacheConfig.CacheSize),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
schemaField.Default = nil
|
|
|
|
case idk.IgnoreField:
|
|
continue
|
|
|
|
case idk.IntField:
|
|
props := map[string]interface{}{
|
|
"fieldType": "int",
|
|
}
|
|
|
|
// JSON decodes numeric values into float64,
|
|
// so reading them back from kafka gives us float64 value,
|
|
// which after convertion overflows int64.
|
|
// That's why we convert Min and Max properties to strings,
|
|
// so while unmarshaling (func intProp(p propper, s string)),
|
|
// we can parse it as int value.
|
|
if typ.Min != nil {
|
|
props["min"] = strconv.FormatInt(*typ.Min, 10)
|
|
}
|
|
if typ.Max != nil {
|
|
props["max"] = strconv.FormatInt(*typ.Max, 10)
|
|
}
|
|
schemaField.Type = &avro.UnionSchema{
|
|
Types: []avro.Schema{
|
|
&avro.NullSchema{},
|
|
&avro.LongSchema{Properties: props},
|
|
},
|
|
}
|
|
schemaField.Default = nil
|
|
|
|
case idk.RecordTimeField:
|
|
schemaField.Type = &avro.UnionSchema{
|
|
Types: []avro.Schema{
|
|
&avro.NullSchema{},
|
|
&avro.BytesSchema{
|
|
Properties: map[string]interface{}{
|
|
"fieldType": "recordTime",
|
|
"layout": typ.Layout,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
schemaField.Default = nil
|
|
|
|
case idk.SignedIntBoolKeyField:
|
|
schemaField.Type = &avro.UnionSchema{
|
|
Types: []avro.Schema{
|
|
&avro.NullSchema{},
|
|
&avro.LongSchema{
|
|
Properties: map[string]interface{}{
|
|
"fieldType": "signedIntBoolKey",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
schemaField.Default = nil
|
|
|
|
case idk.StringArrayField:
|
|
schemaField.Type = &avro.UnionSchema{
|
|
Types: []avro.Schema{
|
|
&avro.NullSchema{},
|
|
&avro.ArraySchema{
|
|
Items: &avro.StringSchema{
|
|
Properties: map[string]interface{}{
|
|
"quantum": quantum,
|
|
"ttl": ttlString,
|
|
"cacheType": string(cacheConfig.CacheType),
|
|
"cacheSize": strconv.Itoa(cacheConfig.CacheSize),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
schemaField.Default = nil
|
|
|
|
case idk.StringField:
|
|
schemaField.Type = &avro.UnionSchema{
|
|
Types: []avro.Schema{
|
|
&avro.NullSchema{},
|
|
&avro.StringSchema{
|
|
Properties: map[string]interface{}{
|
|
"mutex": hasMutex,
|
|
"quantum": quantum,
|
|
"ttl": ttlString,
|
|
"cacheType": string(cacheConfig.CacheType),
|
|
"cacheSize": strconv.Itoa(cacheConfig.CacheSize),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
schemaField.Default = nil
|
|
|
|
default:
|
|
return nil, errors.Errorf("unsupported idk fieldtype %T for schema", typ)
|
|
}
|
|
schemaField.Name = fields[n].Name()
|
|
record.Fields[n] = schemaField
|
|
}
|
|
|
|
return record, nil
|
|
}
|
|
|
|
// helper functions below were all copied from idk/interfaces.go
|
|
func toInt64(val interface{}) (int64, error) {
|
|
switch vt := val.(type) {
|
|
case uint:
|
|
return int64(vt), nil
|
|
case uint8:
|
|
return int64(vt), nil
|
|
case uint16:
|
|
return int64(vt), nil
|
|
case uint32:
|
|
return int64(vt), nil
|
|
case uint64:
|
|
return int64(vt), nil
|
|
case int:
|
|
return int64(vt), nil
|
|
case int8:
|
|
return int64(vt), nil
|
|
case int16:
|
|
return int64(vt), nil
|
|
case int32:
|
|
return int64(vt), nil
|
|
case int64:
|
|
return vt, nil
|
|
case string: // added this case because of mysql driver sending the ids as strings
|
|
v, err := strconv.ParseInt(strings.TrimSpace(vt), 10, 64)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return v, nil
|
|
default:
|
|
return 0, errors.Errorf("couldn't convert %v of %[1]T to int64", vt)
|
|
}
|
|
}
|
|
|
|
func toBool(val interface{}) (bool, error) {
|
|
switch vt := val.(type) {
|
|
case bool:
|
|
return vt, nil
|
|
case byte:
|
|
if vt == '0' || vt == 'f' || vt == 'F' {
|
|
return false, nil
|
|
}
|
|
return vt != 0, nil
|
|
case string:
|
|
switch strings.ToLower(vt) {
|
|
case "", "0", "f", "false":
|
|
return false, nil
|
|
}
|
|
return true, nil
|
|
default:
|
|
if vint, err := toInt64(val); err == nil {
|
|
return vint != 0, nil
|
|
}
|
|
return false, errors.Errorf("couldn't convert %v of %[1]T to bool", vt)
|
|
}
|
|
}
|
|
|
|
func toString(val interface{}) (string, error) {
|
|
switch vt := val.(type) {
|
|
case string:
|
|
return vt, nil
|
|
case []byte:
|
|
return string(vt), nil
|
|
default:
|
|
if vt == nil {
|
|
return "", nil
|
|
}
|
|
return fmt.Sprintf("%v", val), nil
|
|
}
|
|
}
|
|
|
|
func toDecimal(val interface{}, scale int64) ([]byte, error) {
|
|
var value uint64
|
|
|
|
switch vt := val.(type) {
|
|
case pql.Decimal:
|
|
v := vt.Value()
|
|
value, scale = uint64(v.Int64()), vt.Scale
|
|
case float32:
|
|
value = uint64(vt) * uint64(pql.Pow10(scale))
|
|
case float64:
|
|
value = uint64(vt) * uint64(pql.Pow10(scale))
|
|
default:
|
|
return nil, errors.Errorf("couldn't convert %v of %[1]T to decimal", vt)
|
|
}
|
|
|
|
b := make([]byte, 16)
|
|
binary.BigEndian.PutUint64(b[0:8], value)
|
|
binary.BigEndian.PutUint64(b[8:16], uint64(scale))
|
|
return b, nil
|
|
}
|