mirror of
https://github.com/featurebasedb/featurebase.git
synced 2026-10-10 21:07:53 +00:00
* unifying idk and featurebase: first pass * resolved conflict with master for gitignore & dockerignore * deleted binaries that were accidentally pushed to git * combined gitlab jobs for idk & featurebase * run go fmt for idk * updated ssh env variable, and made docker password variable in gitlab env variables * fixed typo assigning variable name * trying to fix docker login error * trying a different solution for docker password * pass registry * fixed docker login * updated paths for idk * exclude idk tests from featurebase test run * fix vendor error * update certificates * grpc needs to be in version 1.38 genproto, which is imported by big query updates the grpc version to 1.47.0 grpc 1.47.0 causes etcd to deadlock when calling etcd.Close() the fix is to have a replace in go.mod to specify a specific grpc version * run go mod tidy * go mod * run go mod tidy * exclude bigquery since it is causing issues and undo grpc replace in go.mod * fix grpc version * fix formatting error * update formatting * attempt to fix formatting * update path for code coverage * update to use current branch binaries, not master * fix for building idk - path updates * udpate path for binaries * update job dependecies * update docker idk tests to use the current branch registry * update stages for jobs * updated job dependencies * not allow idk s3 dump to fail since it is a dependency for integration tests * update dependecy for idk tests * update paths for idk build and code coverage * download featurebase binary from s3 * pass branch name to all setup scripts * change to current branch instead of master * updated sonarcloud * sonarcloud fix and branch name fix * trying to speed up pipeline run time * update stage * branch name fix + sonar cloud * sonarcloud
315 lines
8.4 KiB
Go
315 lines
8.4 KiB
Go
package bankgen
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"fmt"
|
|
"io"
|
|
"math/rand"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"sync/atomic"
|
|
|
|
liavro "github.com/linkedin/goavro/v2"
|
|
"github.com/molecula/featurebase/v3/idk"
|
|
"github.com/molecula/featurebase/v3/idk/common"
|
|
"github.com/molecula/featurebase/v3/idk/kafka/csrc"
|
|
"github.com/molecula/featurebase/v3/logger"
|
|
"github.com/pkg/errors"
|
|
|
|
confluent "github.com/confluentinc/confluent-kafka-go/kafka"
|
|
)
|
|
|
|
// PutCmd represents a command to put data into Kafka
|
|
type PutCmd struct {
|
|
idk.ConfluentCommand `flag:"!embed"`
|
|
|
|
Topic string `help:"Kafka topic to post to."`
|
|
Subject string
|
|
Seed int64 `help:"Random seed for data generation"`
|
|
Save string `help:"Filename of record log. '-' for stdout."`
|
|
BatchSize int `help:"Size of record batches to submit to Kafka."`
|
|
|
|
NumRecords int `help:"Number of records to generate"`
|
|
|
|
DryRun bool `help:"Dry run - just flag parsing."`
|
|
|
|
log logger.Logger
|
|
rand *rand.Rand
|
|
configMap *confluent.ConfigMap
|
|
}
|
|
|
|
// NewPutCmd creates a new PutCmd
|
|
func NewPutCmd() (*PutCmd, error) {
|
|
var err error
|
|
p := &PutCmd{}
|
|
p.ConfluentCommand = idk.ConfluentCommand{
|
|
KafkaBootstrapServers: []string{"localhost:9092"},
|
|
SchemaRegistryURL: "localhost:8081",
|
|
}
|
|
|
|
p.Topic = "banktest"
|
|
p.Subject = "test"
|
|
p.Save = ""
|
|
p.BatchSize = 1000
|
|
p.NumRecords = 10
|
|
p.log = logger.NewVerboseLogger(os.Stderr)
|
|
return p, err
|
|
}
|
|
|
|
func (p *PutCmd) Log() logger.Logger { return p.log }
|
|
|
|
// Run is the entry point for PutCmd
|
|
func (p *PutCmd) Run() (err error) {
|
|
p.configMap, err = common.SetupConfluent(&p.ConfluentCommand)
|
|
if err != nil {
|
|
return
|
|
}
|
|
var auth *csrc.BasicAuth
|
|
if len(p.SchemaRegistryUsername) > 0 {
|
|
auth = &csrc.BasicAuth{
|
|
KafkaSchemaApiKey: p.SchemaRegistryUsername,
|
|
KafkaSchemaApiSecret: p.SchemaRegistryPassword,
|
|
}
|
|
}
|
|
client := csrc.NewClient(p.SchemaRegistryURL, nil, auth)
|
|
|
|
schemaStr := getBankSchema()
|
|
|
|
resp, err := client.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")
|
|
}
|
|
|
|
p.rand = rand.New(rand.NewSource(p.Seed))
|
|
|
|
var w io.Writer = os.Stdout
|
|
if p.Save != "" && p.Save != "-" {
|
|
f, err := os.Create(p.Save)
|
|
if err != nil {
|
|
return errors.Wrap(err, "creating record log file")
|
|
}
|
|
defer f.Close()
|
|
|
|
w = f
|
|
}
|
|
|
|
// librdkafka has a large (1000000) default buffer.
|
|
// limit the go buffer size to reduce memory consumption
|
|
// but allows for the intended buffered channel behavior
|
|
err = p.configMap.SetKey("go.produce.channel.size", 1)
|
|
if err != nil {
|
|
return errors.Wrap(err, "setting go.produce.channel.size")
|
|
}
|
|
producer, err := confluent.NewProducer(p.configMap)
|
|
if err != nil {
|
|
fmt.Printf("Failed to create producer: %s\n", err)
|
|
os.Exit(1)
|
|
}
|
|
defer producer.Close()
|
|
|
|
finished := int32(0)
|
|
iter := int64(0)
|
|
doneChan := common.LaunchKafkaEventConfirmer(producer, &finished, &iter)
|
|
for n := 0; n < p.NumRecords; n++ {
|
|
data := p.generateRecord()
|
|
rec, _, err := licodec.NativeFromTextual([]byte(data))
|
|
if err != nil {
|
|
return errors.Wrap(err, "decoding generated data")
|
|
}
|
|
|
|
buf := make([]byte, 5, 1000)
|
|
buf[0] = 0
|
|
binary.BigEndian.PutUint32(buf[1:], uint32(resp.ID))
|
|
buf, err = licodec.BinaryFromNative(buf, rec.(map[string]interface{}))
|
|
if err != nil {
|
|
return errors.Errorf("encoding:\n%+v\nerr: %v", rec, err)
|
|
}
|
|
|
|
var uid int
|
|
switch v := rec.(map[string]interface{})["user_id"].(type) {
|
|
case int:
|
|
uid = v
|
|
case int32:
|
|
uid = int(v)
|
|
}
|
|
producer.ProduceChannel() <- &confluent.Message{
|
|
TopicPartition: confluent.TopicPartition{Topic: &p.Topic, Partition: confluent.PartitionAny},
|
|
Key: []byte(strconv.Itoa(uid)),
|
|
Value: buf,
|
|
}
|
|
atomic.AddInt64(&iter, 1)
|
|
|
|
if p.Save != "" {
|
|
fmt.Fprintf(w, "%v\n", rec.(map[string]interface{}))
|
|
}
|
|
|
|
}
|
|
atomic.AddInt32(&finished, 1)
|
|
<-doneChan //wait till all messages are acked
|
|
producer.Flush(10 * 1000)
|
|
p.log.Printf("Put %d generated records\n", iter)
|
|
|
|
return nil
|
|
}
|
|
|
|
var (
|
|
floatMeta = `"type": ["null", {"type": "float", "scale": 3}], "default": null`
|
|
boolMeta = `"type": ["null", "boolean"], "default": null`
|
|
stringMeta = `"type": ["null", "string"], "default": null, "mutex": true`
|
|
intMeta = `"type": ["null", "int"], "default": null`
|
|
|
|
floatNames = []string{
|
|
"pfm_category_total_current_balance__personal_loan",
|
|
}
|
|
|
|
boolNames = []string{
|
|
"pfm_boolean__personal_loan",
|
|
"pfm_boolean__mortgage",
|
|
}
|
|
|
|
stringNames = []string{
|
|
"survey_5dfe1a89_29fa_4505_ac00_4cd055b758ab",
|
|
}
|
|
|
|
intNames = []string{
|
|
"product_recency__consumer_loan",
|
|
}
|
|
|
|
floatCount = 1
|
|
boolCount = 2
|
|
stringCount = 1
|
|
intCount = 1
|
|
|
|
floatNullChance = 0.1
|
|
boolNullChance = 0.1
|
|
stringNullChance = 0.1
|
|
intNullChance = 0.1
|
|
)
|
|
|
|
func getBankSchema() string {
|
|
meta := `"type": "record",
|
|
"name": "bank_user_data",
|
|
"namespace": "bank.user.data",
|
|
"doc": "Per-user bank data"`
|
|
|
|
fields := []string{
|
|
`{"name": "aba", "type": "string"}`,
|
|
`{"name": "db", "type": "string"}`,
|
|
`{"name": "user_id", "type": "int"}`,
|
|
`{"name": "custom_audiences", "type": ["null", "string"], "default": null}`,
|
|
}
|
|
|
|
for n := 0; n < floatCount; n++ {
|
|
fields = append(fields, `{"name": "float_`+floatNames[n]+`", `+floatMeta+`}`)
|
|
}
|
|
for n := 0; n < boolCount; n++ {
|
|
fields = append(fields, `{"name": "bool_`+boolNames[n]+`", `+boolMeta+`}`)
|
|
}
|
|
for n := 0; n < stringCount; n++ {
|
|
fields = append(fields, `{"name": "string_`+stringNames[n]+`", `+stringMeta+`}`)
|
|
}
|
|
for n := 0; n < intCount; n++ {
|
|
fields = append(fields, `{"name": "int_`+intNames[n]+`", `+intMeta+`}`)
|
|
}
|
|
schema := fmt.Sprintf(`{%s, "fields": [%s]}`, meta, strings.Join(fields, ", "))
|
|
|
|
return schema
|
|
}
|
|
|
|
func (p *PutCmd) generateRecord() string {
|
|
fields := []string{
|
|
`"aba": "` + p.ABA() + `"`,
|
|
`"db": "` + p.Db() + `"`,
|
|
`"user_id": ` + strconv.FormatInt(int64(p.UserID()), 10),
|
|
`"custom_audiences": {"string": "` + p.CustomAudiences() + `"}`,
|
|
}
|
|
|
|
for n := 0; n < floatCount; n++ {
|
|
if floatNullChance < p.rand.Float64() {
|
|
floatStr := strconv.FormatFloat(p.rand.Float64(), 'f', 3, 64)
|
|
fields = append(fields, `"float_`+floatNames[n]+`": {"float": `+floatStr+`}`)
|
|
}
|
|
}
|
|
for n := 0; n < boolCount; n++ {
|
|
if boolNullChance < p.rand.Float64() {
|
|
boolStr := strconv.FormatBool(p.rand.Intn(2) == 1)
|
|
fields = append(fields, `"bool_`+boolNames[n]+`": {"boolean": `+boolStr+`}`)
|
|
}
|
|
}
|
|
|
|
for n := 0; n < stringCount; n++ {
|
|
if stringNullChance < p.rand.Float64() {
|
|
stringStr := text(p.rand, 1, 6, true, true, true, false)
|
|
fields = append(fields, `"string_`+stringNames[n]+`": {"string": "`+stringStr+`"}`)
|
|
}
|
|
}
|
|
|
|
for n := 0; n < intCount; n++ {
|
|
if intNullChance < p.rand.Float64() {
|
|
intStr := strconv.FormatInt(int64(p.rand.Intn(1000000)), 10)
|
|
fields = append(fields, `"int_`+intNames[n]+`": {"int": `+intStr+`}`)
|
|
}
|
|
}
|
|
|
|
return `{` + strings.Join(fields, ", ") + `}`
|
|
}
|
|
|
|
// ABA returns a random 9 numeric digit string with about 27000 possible values.
|
|
func (p *PutCmd) ABA() string {
|
|
num := p.rand.Intn(27000) + 22213
|
|
num2 := num/10 - 1213
|
|
numstr := strconv.Itoa(num)
|
|
num2str := strconv.Itoa(num2)
|
|
numstrbytes := append([]byte(numstr), num2str[3], numstr[0], numstr[1], numstr[2])
|
|
return string(numstrbytes)
|
|
}
|
|
|
|
// Db returns a db
|
|
func (p *PutCmd) Db() string {
|
|
return text(p.rand, 1, 6, true, true, true, false)
|
|
}
|
|
|
|
// UserID returns a user ID
|
|
func (p *PutCmd) UserID() int {
|
|
return p.rand.Intn(10000000) // 10 mil
|
|
}
|
|
|
|
// CustomAudiences returns a fake Custom Audience string
|
|
func (p *PutCmd) CustomAudiences() string {
|
|
return text(p.rand, 1, 6, true, true, true, false)
|
|
}
|
|
|
|
var lowerLetters = []rune("abcdefghijklmnopqrstuvwxyz")
|
|
var upperLetters = []rune("ABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
|
var numeric = []rune("0123456789")
|
|
var specialChars = []rune(`!'@#$%^&*()_+-=[]{};:",./?`)
|
|
|
|
func text(rand *rand.Rand, atLeast, atMost int, allowLower, allowUpper, allowNumeric, allowSpecial bool) string {
|
|
allowedChars := []rune{}
|
|
if allowLower {
|
|
allowedChars = append(allowedChars, lowerLetters...)
|
|
}
|
|
if allowUpper {
|
|
allowedChars = append(allowedChars, upperLetters...)
|
|
}
|
|
if allowNumeric {
|
|
allowedChars = append(allowedChars, numeric...)
|
|
}
|
|
if allowSpecial {
|
|
allowedChars = append(allowedChars, specialChars...)
|
|
}
|
|
|
|
result := []rune{}
|
|
nTimes := rand.Intn(atMost-atLeast+1) + atLeast
|
|
for i := 0; i < nTimes; i++ {
|
|
result = append(result, allowedChars[rand.Intn(len(allowedChars))])
|
|
}
|
|
return string(result)
|
|
}
|