Add depot, attendant, jetfire, optimus, ota services with kustomize overlays

This commit is contained in:
Chris Rai
2026-01-31 15:35:07 -05:00
parent a0ec642ca1
commit 9a5cb2f547
404 changed files with 38817 additions and 16 deletions

View File

@@ -0,0 +1,25 @@
CLICKHOUSE_USER="default"
CLICKHOUSE_PASS=""
CLICKHOUSE_FEATURE_TABLE="feature_table"
CLICKHOUSE_VEHICLE_SIGNAL_TABLE="vehicle_signal"
CLICKHOUSE_HOST="localhost"
KAFKA_HOSTS="localhost:9093"
CLICKHOUSE_MAX_CONNS=5
JETFIRE_VEHICLE_SIGNAL_BATCH_PERIOD_MS=10000
JETFIRE_FEATURE_BATCH_PERIOD_MS=10000
JETFIRE_FEATURE_DOWNSAMPLE_US=1000000
JETFIRE_TRIP_TIMEOUT_MS=300000
JETFIRE_STATE_TIMEOUT_MS=3600000
JETFIRE_FUTURE_THRESHOLD_MS=604800000
JETFIRE_SCHEMA_RESET_PERIOD_MS=3600000
JETFIRE_MAX_BUFFER_ROWS=1000
LOG_LEVEL="debug"

View File

@@ -0,0 +1,32 @@
## Build binaries for event detection using cloud_base_go image
ARG BASE_IMAGE=cloud_base_go
FROM ${BASE_IMAGE} as builder-go
WORKDIR /build/jetfire
COPY ./jetfire/go.mod ./jetfire/go.sum ./
RUN go mod edit -replace fiskerinc.com/modules=../fiskerinc.com/modules \
&& go mod download
COPY ./jetfire ./
RUN go mod edit -replace fiskerinc.com/modules=../fiskerinc.com/modules \
&& go build -tags musl
## Build image for event detection, pulling binaries from builder image
FROM alpine:3.17
RUN apk add --no-cache librdkafka --repository=https://dl-cdn.alpinelinux.org/alpine/edge/community \
&& apk add --no-cache ca-certificates
COPY ./modules_go/logger/log_config .
COPY ./jetfire/default-feature-vars.json .
COPY --from=builder-go /build/jetfire/jetfire .
ENV LOG_LEVEL=log_config
EXPOSE 8077
CMD ./jetfire

View File

@@ -0,0 +1,26 @@
# Jetfire Data Routing Service
Jetfire Service listens to CAN Signals on the vehicle_signals topic on Kafka,
and performs a pivot operation for populating the feature_table on Clickhouse,
and populating the real_time table on Clickhouse.
CAN Signals are batched and inserted into vehicle_signal table without any transform performed on the data.
Feature Table inserts are typically batched at `JETFIRE_FEATURE_BATCH_PERIOD_MS=10000`.
Vehicle Signal Table inserts are typically batched at `JETFIRE_VEHICLE_SIGNAL_BATCH_PERIOD_MS=10000`
The schemas for sink tables are fetched periodically, set to `JETFIRE_SCHEMA_RESET_PERIOD_MS=3600000`
Additionally, a GET request to the `/reset` endpoint on port `8077` will also trigger an immediate schema reset.
For more information about Jetfire service, see https://fiskerinc.atlassian.net/wiki/spaces/COM/pages/1401487522/Jetfire+Service
## Usage
Copy `./.env.tenmplate` to `./.env` file
Secrets in the .env file will need to be filled in manually.
Running jetfire locally
```
source set_envs.sh
go run main.go
```

View File

@@ -0,0 +1,54 @@
package controllers
import (
"github.com/fiskerinc/cloud-services/services/jetfire/services"
"time"
"github.com/fiskerinc/cloud-services/pkg/health"
"github.com/fiskerinc/cloud-services/pkg/logger"
"github.com/pkg/errors"
)
var mismatchTypeError = errors.New("mismatch type error")
func HealthCheck() {
server := health.HealthCheckServer{}
err := server.Serve([]health.Config{
{
Name: "clickhouse",
Check: health.NewClickhouseCheck(getClickhouseConsumer),
Timeout: time.Second * 1,
},
})
if err != nil {
logger.Error().Err(err).Send()
}
}
func getKafkaConsumer() (health.KafkaConnCheckInterface, error) {
client, err := services.GetKafkaConsumer()
if err != nil {
return nil, err
}
conn, ok := client.(health.KafkaConnCheckInterface)
if !ok {
return nil, errors.WithStack(mismatchTypeError)
}
return conn, nil
}
func getClickhouseConsumer() (health.ClickhouseConnCheckInterface, error) {
client, err := services.GetClickhouseConnection()
if err != nil {
return nil, err
}
conn, ok := client.(health.ClickhouseConnCheckInterface)
if !ok {
return nil, errors.WithStack(mismatchTypeError)
}
return conn, nil
}

View File

@@ -0,0 +1,65 @@
[
"BCM_DrFrntDoorSts",
"BCM_FrntDrDoorLockSts",
"BCM_TotMilg_ODO",
"BCM_PwrMod",
"BMS_AccueChrgTotAh",
"BMS_AccueDchaTotAh",
"BMS_Bat_Actual_Pack_Capacity",
"BMS_Bat_HVmeasure_Current",
"BMS_Bat_SoC_usable",
"BMS_BattAvrgT",
"BMS_Bat_measure_Energy",
"BMS_PwrBattChrgDchaCrt1",
"BMS_PwrBattChrgDchaCrt2",
"BMS_PwrBattRmngCpSOC",
"BMS_PwrBattSOH",
"BMS_VehChrgDchgMod",
"BMS_Cell_Volt_max",
"BMS_Cell_Volt_min",
"BMS_Bat_Coolant_in",
"BMS_Bat_Coolant_out",
"ECC_OutdT",
"ESP_TotBrkTqReq",
"ESP_VehSpd",
"IBS_StateOfCharge",
"IBS_StateOfHealth",
"IBS_BatteryVoltage",
"IBS_BatteryCurrent",
"IBS_BatteryTemperature",
"IBS_AvgRi",
"IBS_AvailableCapacity",
"ICC_DispVehSpd",
"ICC_FrntWiprCtrl",
"MCU_F_AlrmLamp_FS",
"MCU_F_CrtTq",
"MCU_F_HVActvDchaSts",
"MCU_R_AlrmLamp_FS",
"MCU_R_CrtTq",
"MCU_R_HVActvDchaSts",
"OBC_DCPosRlyCtrlSts",
"VCU_ACChrgShttrSts",
"VCU_APSPerc",
"VCU_BrkPedlSts_GB",
"VCU_BrkSig",
"VCU_ChrgSts",
"VCU_ChrgSts_GB",
"VCU_ChrgSysOperCmd",
"VCU_DCChrgShttrSts",
"VCU_GearSig_GB",
"VCU_VehChrgDchgMod",
"VCU_VehOperMod",
"TBOX_GPSHei",
"TBOX_GPSLati",
"TBOX_GPSLongi"
]

143
services/jetfire/go.mod Normal file
View File

@@ -0,0 +1,143 @@
module github.com/fiskerinc/cloud-services/services/jetfire
go 1.25
toolchain go1.25.0
require (
github.com/ClickHouse/ch-go v0.58.2
github.com/ClickHouse/clickhouse-go/v2 v2.6.0
github.com/fiskerinc/cloud-services/pkg v0.0.0-00010101000000-000000000000
github.com/intel-go/fastjson v0.0.0-20170329170629-f846ae58a1ab
github.com/julienschmidt/httprouter v1.3.0
github.com/pkg/errors v0.9.1
github.com/rs/zerolog v1.29.1
github.com/sony/gobreaker v0.5.0
github.com/stretchr/testify v1.10.0
google.golang.org/protobuf v1.36.1
gopkg.in/retry.v1 v1.0.3
)
require (
github.com/DataDog/appsec-internal-go v1.4.0 // indirect
github.com/DataDog/datadog-agent/pkg/obfuscate v0.48.0 // indirect
github.com/DataDog/datadog-agent/pkg/remoteconfig/state v0.48.1 // indirect
github.com/DataDog/datadog-go/v5 v5.3.0 // indirect
github.com/DataDog/go-libddwaf/v2 v2.2.3 // indirect
github.com/DataDog/go-tuf v1.0.2-0.5.2 // indirect
github.com/DataDog/sketches-go v1.4.2 // indirect
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/Microsoft/go-winio v0.6.1 // indirect
github.com/ReneKroon/ttlcache/v2 v2.11.0 // indirect
github.com/albenik/bcd v0.0.0-20170831201648-635201416bc7 // indirect
github.com/andybalholm/brotli v1.0.6 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/dmarkham/enumer v1.5.8 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/ebitengine/purego v0.5.2 // indirect
github.com/elliotchance/orderedmap/v2 v2.2.0 // indirect
github.com/fiskerinc/cloud-services/pkg/can-go v0.0.0-00010101000000-000000000000 // indirect
github.com/frankban/quicktest v1.14.6 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/go-faster/city v1.0.1 // indirect
github.com/go-faster/errors v0.6.1 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/jsonreference v0.21.0 // indirect
github.com/go-openapi/spec v0.21.0 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
github.com/go-pg/pg/v10 v10.11.1 // indirect
github.com/go-pg/zerochecker v0.2.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.15.1 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/golang/mock v1.7.0-rc.1 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/gomodule/redigo v1.8.9 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/schema v1.2.0 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/go-version v1.6.0 // indirect
github.com/iancoleman/strcase v0.3.0 // indirect
github.com/jinzhu/copier v0.3.5 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/klauspost/compress v1.18.2 // indirect
github.com/leodido/go-urn v1.2.4 // indirect
github.com/lestrrat-go/backoff/v2 v2.0.8 // indirect
github.com/lestrrat-go/blackmagic v1.0.1 // indirect
github.com/lestrrat-go/httpcc v1.0.1 // indirect
github.com/lestrrat-go/iter v1.0.2 // indirect
github.com/lestrrat-go/jwx v1.2.25 // indirect
github.com/lestrrat-go/option v1.0.1 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/montanaflynn/stats v0.7.1 // indirect
github.com/onsi/ginkgo v1.16.5 // indirect
github.com/onsi/gomega v1.25.0 // indirect
github.com/outcaste-io/ristretto v0.2.3 // indirect
github.com/pascaldekloe/name v1.0.1 // indirect
github.com/paulmach/orb v0.8.0 // indirect
github.com/philhofer/fwd v1.1.2 // indirect
github.com/pierrec/lz4/v4 v4.1.22 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/redis/go-redis/v9 v9.5.1 // indirect
github.com/secure-systems-lab/go-securesystemslib v0.7.0 // indirect
github.com/segmentio/asm v1.2.0 // indirect
github.com/shopspring/decimal v1.3.1 // indirect
github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f // indirect
github.com/swaggo/files v0.0.0-20220728132757-551d4a08d97a // indirect
github.com/swaggo/http-swagger v1.3.3 // indirect
github.com/swaggo/swag v1.8.8 // indirect
github.com/tinylib/msgp v1.1.8 // indirect
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect
github.com/twmb/franz-go v1.20.6 // indirect
github.com/twmb/franz-go/pkg/kadm v1.17.2 // indirect
github.com/twmb/franz-go/pkg/kmsg v1.12.0 // indirect
github.com/vmihailenco/bufpool v0.1.11 // indirect
github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect
github.com/vmihailenco/tagparser v0.1.2 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.1.2 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a // indirect
go.mongodb.org/mongo-driver v1.14.0 // indirect
go.opentelemetry.io/otel v1.29.0 // indirect
go.opentelemetry.io/otel/metric v1.29.0 // indirect
go.opentelemetry.io/otel/trace v1.29.0 // indirect
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.25.0 // indirect
go4.org/intern v0.0.0-20230525184215-6c62f75575cb // indirect
go4.org/unsafe/assume-no-moving-gc v0.0.0-20231121144256-b99613f794b6 // indirect
golang.org/x/crypto v0.45.0 // indirect
golang.org/x/mod v0.29.0 // indirect
golang.org/x/net v0.47.0 // indirect
golang.org/x/sync v0.18.0 // indirect
golang.org/x/sys v0.38.0 // indirect
golang.org/x/text v0.31.0 // indirect
golang.org/x/time v0.8.0 // indirect
golang.org/x/tools v0.38.0 // indirect
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
gopkg.in/DataDog/dd-trace-go.v1 v1.60.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
inet.af/netaddr v0.0.0-20230525184311-b8eac61e914a // indirect
mellium.im/sasl v0.3.1 // indirect
)
replace (
github.com/fiskerinc/cloud-services/pkg => ../../pkg
github.com/fiskerinc/cloud-services/pkg/can-go => ../../pkg/can-go
)

548
services/jetfire/go.sum Normal file
View File

@@ -0,0 +1,548 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/ClickHouse/ch-go v0.58.2 h1:jSm2szHbT9MCAB1rJ3WuCJqmGLi5UTjlNu+f530UTS0=
github.com/ClickHouse/ch-go v0.58.2/go.mod h1:Ap/0bEmiLa14gYjCiRkYGbXvbe8vwdrfTYWhsuQ99aw=
github.com/ClickHouse/clickhouse-go/v2 v2.6.0 h1:NmnPY2Cg4hCqS2ZGBep9EWHfQPAco2Vkpwb02VXtWew=
github.com/ClickHouse/clickhouse-go/v2 v2.6.0/go.mod h1:SvXuWqDsiHJE3VAn2+3+nz9W9exOSigyskcs4DAcxJQ=
github.com/DataDog/appsec-internal-go v1.4.0 h1:KFI8ElxkJOgpw+cUm9TXK/jh5EZvRaWM07sXlxGg9Ck=
github.com/DataDog/appsec-internal-go v1.4.0/go.mod h1:ONW8aV6R7Thgb4g0bB9ZQCm+oRgyz5eWiW7XoQ19wIc=
github.com/DataDog/datadog-agent/pkg/obfuscate v0.48.0 h1:bUMSNsw1iofWiju9yc1f+kBd33E3hMJtq9GuU602Iy8=
github.com/DataDog/datadog-agent/pkg/obfuscate v0.48.0/go.mod h1:HzySONXnAgSmIQfL6gOv9hWprKJkx8CicuXuUbmgWfo=
github.com/DataDog/datadog-agent/pkg/remoteconfig/state v0.48.1 h1:5nE6N3JSs2IG3xzMthNFhXfOaXlrsdgqmJ73lndFf8c=
github.com/DataDog/datadog-agent/pkg/remoteconfig/state v0.48.1/go.mod h1:Vc+snp0Bey4MrrJyiV2tVxxJb6BmLomPvN1RgAvjGaQ=
github.com/DataDog/datadog-go/v5 v5.3.0 h1:2q2qjFOb3RwAZNU+ez27ZVDwErJv5/VpbBPprz7Z+s8=
github.com/DataDog/datadog-go/v5 v5.3.0/go.mod h1:XRDJk1pTc00gm+ZDiBKsjh7oOOtJfYfglVCmFb8C2+Q=
github.com/DataDog/go-libddwaf/v2 v2.2.3 h1:LpKE8AYhVrEhlmlw6FGD41udtDf7zW/aMdLNbCXpegQ=
github.com/DataDog/go-libddwaf/v2 v2.2.3/go.mod h1:8nX0SYJMB62+fbwYmx5J7zuCGEjiC/RxAo3+AuYJuFE=
github.com/DataDog/go-tuf v1.0.2-0.5.2 h1:EeZr937eKAWPxJ26IykAdWA4A0jQXJgkhUjqEI/w7+I=
github.com/DataDog/go-tuf v1.0.2-0.5.2/go.mod h1:zBcq6f654iVqmkk8n2Cx81E1JnNTMOAx1UEO/wZR+P0=
github.com/DataDog/gostackparse v0.7.0 h1:i7dLkXHvYzHV308hnkvVGDL3BR4FWl7IsXNPz/IGQh4=
github.com/DataDog/gostackparse v0.7.0/go.mod h1:lTfqcJKqS9KnXQGnyQMCugq3u1FP6UZMfWR0aitKFMM=
github.com/DataDog/sketches-go v1.4.2 h1:gppNudE9d19cQ98RYABOetxIhpTCl4m7CnbRZjvVA/o=
github.com/DataDog/sketches-go v1.4.2/go.mod h1:xJIXldczJyyjnbDop7ZZcLxJdV3+7Kra7H1KMgpgkLk=
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
github.com/Microsoft/go-winio v0.5.0/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84=
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
github.com/ReneKroon/ttlcache/v2 v2.11.0 h1:OvlcYFYi941SBN3v9dsDcC2N8vRxyHcCmJb3Vl4QMoM=
github.com/ReneKroon/ttlcache/v2 v2.11.0/go.mod h1:mBxvsNY+BT8qLLd6CuAJubbKo6r0jh3nb5et22bbfGY=
github.com/albenik/bcd v0.0.0-20170831201648-635201416bc7 h1:m3Ayfs5OcAlIMEdLIQKubBsVLGee4YMUr14+d1256WE=
github.com/albenik/bcd v0.0.0-20170831201648-635201416bc7/go.mod h1:QIAMbrwsnQZ2ES3G26RubSrDB5SPyzsp9Hts5NJdTrI=
github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI=
github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A=
github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/confluentinc/confluent-kafka-go v1.9.2 h1:gV/GxhMBUb03tFWkN+7kdhg+zf+QUM+wVkI9zwh770Q=
github.com/confluentinc/confluent-kafka-go/v2 v2.3.0 h1:icCHutJouWlQREayFwCc7lxDAhws08td+W3/gdqgZts=
github.com/confluentinc/confluent-kafka-go/v2 v2.3.0/go.mod h1:/VTy8iEpe6mD9pkCH5BhijlUl8ulUXymKv1Qig5Rgb8=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.0-20210816181553-5444fa50b93d/go.mod h1:tmAIfUFEirG/Y8jhZ9M+h36obRZAk/1fcSpXwAVlfqE=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 h1:HbphB4TFFXpv7MNrT52FGrrgVXF1owhMVTHFZIlnvd4=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0/go.mod h1:DZGJHZMqrU4JJqFAWUS2UO1+lbSKsdiOoYi9Zzey7Fc=
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y=
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/dmarkham/enumer v1.5.8 h1:fIF11F9l5jyD++YYvxcSH5WgHfeaSGPaN/T4kOQ4qEM=
github.com/dmarkham/enumer v1.5.8/go.mod h1:d10o8R3t/gROm2p3BXqTkMt2+HMuxEmWCXzorAruYak=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/dvyukov/go-fuzz v0.0.0-20210103155950-6a8e9d1f2415/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw=
github.com/ebitengine/purego v0.5.2 h1:r2MQEtkGzZ4LRtFZVAg5bjYKnUbxxloaeuGxH0t7qfs=
github.com/ebitengine/purego v0.5.2/go.mod h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ=
github.com/elliotchance/orderedmap/v2 v2.2.0 h1:7/2iwO98kYT4XkOjA9mBEIwvi4KpGB4cyHeOFOnj4Vk=
github.com/elliotchance/orderedmap/v2 v2.2.0/go.mod h1:85lZyVbpGaGvHvnKa7Qhx7zncAdBIBq6u56Hb1PRU5Q=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/frankban/quicktest v1.2.2/go.mod h1:Qh/WofXFeiAFII1aEBu529AtJo6Zg2VHscnEsbBnJ20=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
github.com/go-faster/city v1.0.1 h1:4WAxSZ3V2Ws4QRDrscLEDcibJY8uf41H6AhXDrNDcGw=
github.com/go-faster/city v1.0.1/go.mod h1:jKcUJId49qdW3L1qKHH/3wPeUstCVpVSXTM6vO3VcTw=
github.com/go-faster/errors v0.6.1 h1:nNIPOBkprlKzkThvS/0YaX8Zs9KewLCOSFQS5BU06FI=
github.com/go-faster/errors v0.6.1/go.mod h1:5MGV2/2T9yvlrbhe9pD9LO5Z/2zCSq2T8j+Jpi2LAyY=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4=
github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY=
github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk=
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
github.com/go-pg/pg/v10 v10.11.1 h1:vYwbFpqoMpTDphnzIPshPPepdy3VpzD8qo29OFKp4vo=
github.com/go-pg/pg/v10 v10.11.1/go.mod h1:ExJWndhDNNftBdw1Ow83xqpSf4WMSJK8urmXD5VXS1I=
github.com/go-pg/zerochecker v0.2.0 h1:pp7f72c3DobMWOb2ErtZsnrPaSvHd2W4o9//8HtF4mU=
github.com/go-pg/zerochecker v0.2.0/go.mod h1:NJZ4wKL0NmTtz0GKCoJ8kym6Xn/EQzXRl2OnAe7MmDo=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.15.1 h1:BSe8uhN+xQ4r5guV/ywQI4gO59C2raYcGffYWZEjZzM=
github.com/go-playground/validator/v10 v10.15.1/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
github.com/golang/mock v1.7.0-rc.1 h1:YojYx61/OLFsiv6Rw1Z96LpldJIy31o+UHmwAUMJ6/U=
github.com/golang/mock v1.7.0-rc.1/go.mod h1:s42URUywIqd+OcERslBJvOjepvNymP31m3q8d/GkuRs=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/gomodule/redigo v1.8.9 h1:Sl3u+2BI/kk+VEatbj0scLdrFhjPmbxOc1myhDP41ws=
github.com/gomodule/redigo v1.8.9/go.mod h1:7ArFNvsTjH8GMMzB4uy1snslv2BwmginuMs06a1uzZE=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.2.1-0.20190312032427-6f77996f0c42/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20240207164012-fb44976bdcd5 h1:E/LAvt58di64hlYjx7AsNS6C/ysHWYo+2qPCZKTQhRo=
github.com/google/pprof v0.0.0-20240207164012-fb44976bdcd5/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/schema v1.2.0 h1:YufUaxZYCKGFuAq3c96BOhjgd5nmXiOY9NGzF247Tsc=
github.com/gorilla/schema v1.2.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek=
github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI=
github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
github.com/intel-go/fastjson v0.0.0-20170329170629-f846ae58a1ab h1:K7WJJ5AnrQV/6tEh0Qqs19KLzvsq5V15f9CifKii6aU=
github.com/intel-go/fastjson v0.0.0-20170329170629-f846ae58a1ab/go.mod h1:xr9Svf97gkxlW+ZDxs47vReKp7m9EUzNhEGOLyBHR+8=
github.com/jinzhu/copier v0.3.5 h1:GlvfUwHk62RokgqVNvYsku0TATCF7bAHVwEXoBh3iJg=
github.com/jinzhu/copier v0.3.5/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
github.com/lestrrat-go/backoff/v2 v2.0.8 h1:oNb5E5isby2kiro9AgdHLv5N5tint1AnDVVf2E2un5A=
github.com/lestrrat-go/backoff/v2 v2.0.8/go.mod h1:rHP/q/r9aT27n24JQLa7JhSQZCKBBOiM/uP402WwN8Y=
github.com/lestrrat-go/blackmagic v1.0.0/go.mod h1:TNgH//0vYSs8VXDCfkZLgIrVTTXQELZffUV0tz3MtdQ=
github.com/lestrrat-go/blackmagic v1.0.1 h1:lS5Zts+5HIC/8og6cGHb0uCcNCa3OUt1ygh3Qz2Fe80=
github.com/lestrrat-go/blackmagic v1.0.1/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU=
github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E=
github.com/lestrrat-go/iter v1.0.1/go.mod h1:zIdgO1mRKhn8l9vrZJZz9TUMMFbQbLeTsbqPDrJ/OJc=
github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI=
github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4=
github.com/lestrrat-go/jwx v1.2.25 h1:tAx93jN2SdPvFn08fHNAhqFJazn5mBBOB8Zli0g0otA=
github.com/lestrrat-go/jwx v1.2.25/go.mod h1:zoNuZymNl5lgdcu6P7K6ie2QRll5HVfF4xwxBBK1NxY=
github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU=
github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE=
github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.14.2/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc=
github.com/onsi/gomega v1.25.0 h1:Vw7br2PCDYijJHSfBOWhov+8cAnUf8MfMaIOV323l6Y=
github.com/onsi/gomega v1.25.0/go.mod h1:r+zV744Re+DiYCIPRlYOTxn0YkOLcAnW8k1xXdMPGhM=
github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs=
github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
github.com/outcaste-io/ristretto v0.2.3 h1:AK4zt/fJ76kjlYObOeNwh4T3asEuaCmp26pOvUOL9w0=
github.com/outcaste-io/ristretto v0.2.3/go.mod h1:W8HywhmtlopSB1jeMg3JtdIhf+DYkLAr0VN/s4+MHac=
github.com/pascaldekloe/name v1.0.1 h1:9lnXOHeqeHHnWLbKfH6X98+4+ETVqFqxN09UXSjcMb0=
github.com/pascaldekloe/name v1.0.1/go.mod h1:Z//MfYJnH4jVpQ9wkclwu2I2MkHmXTlT9wR5UZScttM=
github.com/paulmach/orb v0.8.0 h1:W5XAt5yNPNnhaMNEf0xNSkBMJ1LzOzdk2MRlB6EN0Vs=
github.com/paulmach/orb v0.8.0/go.mod h1:FWRlTgl88VI1RBx/MkrwWDRhQ96ctqMCh8boXhmqB/A=
github.com/paulmach/protoscan v0.2.1/go.mod h1:SpcSwydNLrxUGSDvXvO0P7g7AuhJ7lcKfDlhJCDw2gY=
github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw=
github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0=
github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=
github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/redis/go-redis/v9 v9.5.1 h1:H1X4D3yHPaYrkL5X06Wh6xNVM/pX0Ft4RV0vMGvLBh8=
github.com/redis/go-redis/v9 v9.5.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
github.com/richardartoul/molecule v1.0.1-0.20221107223329-32cfee06a052 h1:Qp27Idfgi6ACvFQat5+VJvlYToylpM/hcyLBI3WaKPA=
github.com/richardartoul/molecule v1.0.1-0.20221107223329-32cfee06a052/go.mod h1:uvX/8buq8uVeiZiFht+0lqSLBHF+uGV8BrTv8W/SIwk=
github.com/rogpeppe/clock v0.0.0-20190514195947-2896927a307a h1:3QH7VyOaaiUHNrA9Se4YQIRkDTCw1EJls9xTUCaCeRM=
github.com/rogpeppe/clock v0.0.0-20190514195947-2896927a307a/go.mod h1:4r5QyqhjIWCcK8DO4KMclc5Iknq5qVBAlbYYzAbUScQ=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.29.1 h1:cO+d60CHkknCbvzEWxP0S9K6KqyTjrCNUy1LdQLCGPc=
github.com/rs/zerolog v1.29.1/go.mod h1:Le6ESbR7hc+DP6Lt1THiV8CQSdkkNrd3R0XbEgp3ZBU=
github.com/secure-systems-lab/go-securesystemslib v0.7.0 h1:OwvJ5jQf9LnIAS83waAjPbcMsODrTQUpJ02eNLUoxBg=
github.com/secure-systems-lab/go-securesystemslib v0.7.0/go.mod h1:/2gYnlnHVQ6xeGtfIqFy7Do03K4cdCY0A/GlJLDKLHI=
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f h1:1R9KdKjCNSd7F8iGTxIpoID9prlYH8nuNYKt0XvweHA=
github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f/go.mod h1:vQhwQ4meQEDfahT5kd61wLAF5AAeh5ZPLVI4JJ/tYo8=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sony/gobreaker v0.5.0 h1:dRCvqm0P490vZPmy7ppEk2qCnCieBooFJ+YoXGYB+yg=
github.com/sony/gobreaker v0.5.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/swaggo/files v0.0.0-20220728132757-551d4a08d97a h1:kAe4YSu0O0UFn1DowNo2MY5p6xzqtJ/wQ7LZynSvGaY=
github.com/swaggo/files v0.0.0-20220728132757-551d4a08d97a/go.mod h1:lKJPbtWzJ9JhsTN1k1gZgleJWY/cqq0psdoMmaThG3w=
github.com/swaggo/http-swagger v1.3.3 h1:Hu5Z0L9ssyBLofaama21iYaF2VbWyA8jdohaaCGpHsc=
github.com/swaggo/http-swagger v1.3.3/go.mod h1:sE+4PjD89IxMPm77FnkDz0sdO+p5lbXzrVWT6OTVVGo=
github.com/swaggo/swag v1.8.8 h1:/GgJmrJ8/c0z4R4hoEPZ5UeEhVGdvsII4JbVDLbR7Xc=
github.com/swaggo/swag v1.8.8/go.mod h1:ezQVUUhly8dludpVk+/PuwJWvLLanB13ygV5Pr9enSk=
github.com/tinylib/msgp v1.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0=
github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw=
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYmW5DyG0UqvY96Bu5QYsTLvCHdrgo=
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs=
github.com/twmb/franz-go v1.20.6 h1:TpQTt4QcixJ1cHEmQGPOERvTzo99s8jAutmS7rbSD6w=
github.com/twmb/franz-go v1.20.6/go.mod h1:u+FzH2sInp7b9HNVv2cZN8AxdXy6y/AQ1Bkptu4c0FM=
github.com/twmb/franz-go/pkg/kadm v1.17.2 h1:g5f1sAxnTkYC6G96pV5u715HWhxd66hWaDZUAQ8xHY8=
github.com/twmb/franz-go/pkg/kadm v1.17.2/go.mod h1:ST55zUB+sUS+0y+GcKY/Tf1XxgVilaFpB9I19UubLmU=
github.com/twmb/franz-go/pkg/kmsg v1.12.0 h1:CbatD7ers1KzDNgJqPbKOq0Bz/WLBdsTH75wgzeVaPc=
github.com/twmb/franz-go/pkg/kmsg v1.12.0/go.mod h1:+DPt4NC8RmI6hqb8G09+3giKObE6uD2Eya6CfqBpeJY=
github.com/vmihailenco/bufpool v0.1.11 h1:gOq2WmBrq0i2yW5QJ16ykccQ4wH9UyEsgLm6czKAd94=
github.com/vmihailenco/bufpool v0.1.11/go.mod h1:AFf/MOy3l2CFTKbxwt0mp2MwnqjNEs5H/UxrkA5jxTQ=
github.com/vmihailenco/msgpack/v5 v5.3.4/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc=
github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU=
github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc=
github.com/vmihailenco/tagparser v0.1.2 h1:gnjoVuB/kljJ5wICEEOpx98oXMWPLj22G67Vbd1qPqc=
github.com/vmihailenco/tagparser v0.1.2/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI=
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a h1:fZHgsYlfvtyqToslyjUt3VOPF4J7aK/3MPcK7xp3PDk=
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a/go.mod h1:ul22v+Nro/R083muKhosV54bj5niojjWZvU8xrevuH4=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.mongodb.org/mongo-driver v1.14.0 h1:P98w8egYRjYe3XDjxhYJagTokP/H6HzlsnojRgZRd80=
go.mongodb.org/mongo-driver v1.14.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c=
go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw=
go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8=
go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc=
go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8=
go.opentelemetry.io/otel/sdk v1.29.0 h1:vkqKjk7gwhS8VaWb0POZKmIEDimRCMsopNYnriHyryo=
go.opentelemetry.io/otel/sdk v1.29.0/go.mod h1:pM8Dx5WKnvxLCb+8lG1PRNIDxu9g9b9g59Qr7hfAAok=
go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4=
go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.25.0 h1:4Hvk6GtkucQ790dqmj7l1eEnRdKm3k3ZUrUMS2d5+5c=
go.uber.org/zap v1.25.0/go.mod h1:JIAUzQIH94IC4fOJQm7gMmBJP5k7wQfdcnYdPoEXJYk=
go4.org/intern v0.0.0-20211027215823-ae77deb06f29/go.mod h1:cS2ma+47FKrLPdXFpr7CuxiTW3eyJbWew4qx0qtQWDA=
go4.org/intern v0.0.0-20230525184215-6c62f75575cb h1:ae7kzL5Cfdmcecbh22ll7lYP3iuUdnfnhiPcSaDgH/8=
go4.org/intern v0.0.0-20230525184215-6c62f75575cb/go.mod h1:Ycrt6raEcnF5FTsLiLKkhBTO6DPX3RCUCUVnks3gFJU=
go4.org/unsafe/assume-no-moving-gc v0.0.0-20211027215541-db492cf91b37/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
go4.org/unsafe/assume-no-moving-gc v0.0.0-20230525183740-e7c30c78aeb2/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
go4.org/unsafe/assume-no-moving-gc v0.0.0-20231121144256-b99613f794b6 h1:lGdhQUN/cnWdSH3291CUuxSEqc+AsGTiDxPP3r2J0l4=
go4.org/unsafe/assume-no-moving-gc v0.0.0-20231121144256-b99613f794b6/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190320223903-b7391e95e576/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220627191245-f75cf1eec38b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210112230658-8b4aab62c064/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ=
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY=
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.67.3 h1:OgPcDAFKHnH8X3O4WcO4XUc8GRDeKsKReqbQtiCj7N8=
google.golang.org/grpc v1.67.3/go.mod h1:YGaHCc6Oap+FzBJTZLBzkGSYt/cvGPFTPxkn7QfSU8s=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk=
google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/DataDog/dd-trace-go.v1 v1.60.1 h1:Sqkq62MxQW/RD+sgZsQuUdHWHyXI4JS5x0lxlxrv2Hk=
gopkg.in/DataDog/dd-trace-go.v1 v1.60.1/go.mod h1:6aArYrAHjnuaofJ3lKuSRQbhrBx1LcSpiEYCIScJE5Y=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/retry.v1 v1.0.3 h1:a9CArYczAVv6Qs6VGoLMio99GEs7kY9UzSF9+LD+iGs=
gopkg.in/retry.v1 v1.0.3/go.mod h1:FJkXmWiMaAo7xB+xhvDF59zhfjDWyzmyAxiT4dB688g=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU=
gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=
honnef.co/go/gotraceui v0.2.0 h1:dmNsfQ9Vl3GwbiVD7Z8d/osC6WtGGrasyrC2suc4ZIQ=
honnef.co/go/gotraceui v0.2.0/go.mod h1:qHo4/W75cA3bX0QQoSvDjbJa4R8mAyyFjbWAj63XElc=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
inet.af/netaddr v0.0.0-20230525184311-b8eac61e914a h1:1XCVEdxrvL6c0TGOhecLuB7U9zYNdxZEjvOqJreKZiM=
inet.af/netaddr v0.0.0-20230525184311-b8eac61e914a/go.mod h1:e83i32mAQOW1LAqEIweALsuK2Uw4mhQadA5r7b0Wobo=
mellium.im/sasl v0.3.1 h1:wE0LW6g7U83vhvxjC1IY8DnXM+EU095yeo8XClvCdfo=
mellium.im/sasl v0.3.1/go.mod h1:xm59PUYpZHhgQ9ZqoJ5QaCqzWMi8IeS49dhp6plPCzw=

View File

@@ -0,0 +1,97 @@
package handlers
import (
"github.com/fiskerinc/cloud-services/services/jetfire/models"
"github.com/fiskerinc/cloud-services/services/jetfire/services"
"github.com/fiskerinc/cloud-services/services/jetfire/utils"
"time"
"github.com/fiskerinc/cloud-services/pkg/grpc/kafka_grpc"
"github.com/fiskerinc/cloud-services/pkg/logger"
"github.com/fiskerinc/cloud-services/pkg/utils/envtool"
)
var (
//downsampling timers and delays
clearCacheTimer = time.Now().Truncate(time.Second)
featureDelay = envtool.GetEnvDuration("JETFIRE_FEATURE_DOWNSAMPLE_US", 1000000) * time.Microsecond
clearCacheDelay = envtool.GetEnvDuration("JETFIRE_STATE_TIMEOUT_MS", 3600000) * time.Millisecond
futureTimeThreshold = envtool.GetEnvDuration("JETFIRE_FUTURE_THRESHOLD_MS", 2*24*60*60*1000) * time.Millisecond
)
// Handles the batch of Signals. returns batchFlag, error
// where batchFlag corresponds to FeatureUpdateFlag for which buffers were updated with this batch
func HandleSignalBatch(batchData []*kafka_grpc.GRPC_CANSignal, vehicleCache *models.VehicleCache, producerChannel chan models.InsertCommand) (uint, error) {
start := time.Now()
batchFlag := uint(0)
//Iterate through received can signals and add to cache
futureWarning := false //only one future warning per batch of data
//sets of pointers to insert into insertion buffers
featureUpdates := make(map[*models.VehicleState]bool)
logger.Debug().Msgf("Processing batch %d signals...", len(batchData))
skipLog := false
for _, signal := range batchData {
signal.Timestamp = utils.FixFloatTimestampScale(signal.Timestamp)
if start.Add(futureTimeThreshold).Before(utils.FloatToTime(signal.Timestamp)) && !futureWarning {
// Throw out any signals that are from the future.
futureWarning = true
logger.Warn().Msgf("ignoring signal(s) from %s from future: %f (currently %f)", signal.Vin, signal.Timestamp, utils.TimeToFloat(start))
continue
}
err := vehicleCache.UpdateSignal(signal, utils.FeatureUpdateFlag)
if err != nil {
logger.Error().Err(err).Send()
//do not continue yet; attempt to add to vehicle signal buffer
}
//Check vehicle state for update
// if signal is not in tracked vars, then skip.
// if VIN is not in vehicle cache, then log and skip
state, ok := vehicleCache.Cache[signal.Vin]
if !ok || state == nil {
if len(*vehicleCache.SignalsSet) > 0 {
_, ok = (*vehicleCache.SignalsSet)[signal.Name]
if ok && !skipLog {
// no vin state but signal update should have created a vin state... log warning
logger.Error().Msgf("unexpected missing VIN state from cache for VIN {%s} and signal {%s}", signal.Vin, signal.Name)
skipLog = true
}
}
continue
}
if state.TimeSincePolled(utils.FeatureUpdateFlag) > featureDelay {
featureUpdates[state] = true
}
}
services.SchemaLock.Lock()
defer services.SchemaLock.Unlock()
//TODO: investigate optimizing selecting VINs that need to be appended to buffer
// iterating through maps is slow!
//batch rows for insertion for feature
for vinState := range featureUpdates {
err := services.GetFeatureBatch().AppendRow(services.GetFeatureVars(), vinState, producerChannel)
if err != nil {
logger.Error().Err(err).Send()
} else {
vinState.SetPollTime(utils.FeatureUpdateFlag)
batchFlag |= utils.FeatureUpdateFlag
}
err = services.GetFeatureLastBatch().AppendRow(services.GetFeatureVars(), vinState, producerChannel)
if err != nil {
logger.Error().Err(err).Send()
}
}
return batchFlag, nil
}

View File

@@ -0,0 +1,21 @@
package handlers
import (
"net/http"
"time"
"github.com/fiskerinc/cloud-services/services/jetfire/services"
"github.com/fiskerinc/cloud-services/pkg/logger"
)
func ResetSchemaDefinitions(w http.ResponseWriter, r *http.Request) {
startTime := time.Now()
logger.Info().Msg("resetting tracked variables and schemas")
services.ResetCacheVars()
endTime := time.Now()
logger.Debug().Msgf("reset tracked variables and schemas, time taken: %fms; %d variables",
float64(endTime.UnixMilli()-startTime.UnixMilli()),
len(services.GetFeatureVars()),
)
}

45
services/jetfire/main.go Normal file
View File

@@ -0,0 +1,45 @@
package main
import (
"context"
"github.com/fiskerinc/cloud-services/services/jetfire/controllers"
"github.com/fiskerinc/cloud-services/services/jetfire/server"
"github.com/fiskerinc/cloud-services/services/jetfire/services"
"github.com/fiskerinc/cloud-services/pkg/kafka"
"github.com/fiskerinc/cloud-services/pkg/logger"
"github.com/fiskerinc/cloud-services/pkg/tracer"
"github.com/fiskerinc/cloud-services/pkg/utils/app"
)
var (
SERVICE_NAME = "jetfire"
)
func init() {
app.Setup(SERVICE_NAME, cleanup)
}
func main() {
defer cleanup()
tracer.Start()
defer tracer.Stop()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go controllers.HealthCheck()
services.ResetCacheVars()
go server.StartHTTPServer() //listen for reset requests
go server.StartConsumer(ctx, kafka.VehicleSignal)
select {}
}
func cleanup() {
logger.Close()
}

View File

@@ -0,0 +1,933 @@
package models
import (
"context"
"github.com/fiskerinc/cloud-services/services/jetfire/utils"
"math"
"sync"
"time"
"github.com/fiskerinc/cloud-services/pkg/grpc/kafka_grpc"
"github.com/fiskerinc/cloud-services/pkg/logger"
"github.com/fiskerinc/cloud-services/pkg/utils/envtool"
"github.com/ClickHouse/ch-go"
"github.com/ClickHouse/ch-go/proto"
"github.com/pkg/errors"
"github.com/rs/zerolog"
"github.com/sony/gobreaker"
)
type InsertBlockType int
const (
NilBlockType InsertBlockType = -1 //insert buffers using this block type will have a nil block.
PivotBlockType InsertBlockType = iota
SignalBlockType
VinLastBlockType
)
var (
initialBlockPoolSize = envtool.GetEnvInt("JETFIRE_MIN_BLOCKS", 2)
maxBlockPoolSize = envtool.GetEnvInt("JETFIRE_MAX_BLOCKS", 4)
maxBufferBytes = envtool.GetEnvInt("JETFIRE_BUFFER_MAX_BYTES", 128<<20) / maxBlockPoolSize
)
const NO_TIMESTAMP_LOG_SAMPLE_RATE = 10000
var logSampler zerolog.Logger
// Creating a log sampler
func init(){
logSampler = logger.Sample(zerolog.RandomSampler(NO_TIMESTAMP_LOG_SAMPLE_RATE))
}
// InsertBuffer abstracts buffer appends and insertions to clickhouse, even for different underlying schemas.
// A pool of blocks are allocated for each InsertBuffer.
//
// These pools do not implement leaky buffer pattern to ensure data order and timeliness.
type InsertBuffer struct {
InsertTime time.Time
insertDelay time.Duration
blockPool []insertBlock //pool of all blocks, including busy blocks.
//linked list of available blocks in the pool
freeLock sync.Mutex //mutex for linked list
freeHead *BlockNode
freeTail *BlockNode
blockType InsertBlockType
tripInfo bool
TableName string
}
// linked list node for insertBlock
type BlockNode struct {
block insertBlock
next *BlockNode
}
// Command struct for inserter goroutine
type InsertCommand struct {
Buffer *InsertBuffer
Block insertBlock
}
func NewInsertBuffer(tripInfo bool, signalNames []string, tableName string, insertDelay time.Duration, blockType InsertBlockType) *InsertBuffer {
newBlockPool := []insertBlock{}
for i := 0; i < initialBlockPoolSize; i++ {
var newBlock insertBlock = AllocateNewBlock(signalNames, maxBufferBytes, blockType, tripInfo)
if newBlock != nil {
newBlockPool = append(newBlockPool, newBlock)
}
}
newBuffer := new(InsertBuffer)
newBuffer.insertDelay = insertDelay
newBuffer.blockPool = newBlockPool
newBuffer.TableName = tableName
newBuffer.blockType = blockType
newBuffer.tripInfo = tripInfo
newBuffer.InitFreeBlocksList()
return newBuffer
}
// memory block linked list functions. These are for available blocks that are ready to accept data.
func (buffer *InsertBuffer) AppendFreeBlock(block insertBlock) {
buffer.freeLock.Lock()
defer buffer.freeLock.Unlock()
if buffer.freeHead == nil {
buffer.freeHead = &BlockNode{block: block}
buffer.freeTail = buffer.freeHead
return
}
if buffer.freeTail == nil { //this should never occur!
err := errors.Errorf("Unexpected nil tail for InsertBuffer blocks with non nil head!")
logger.Error().Err(err).Send()
return
}
buffer.freeTail.next = &BlockNode{block: block}
buffer.freeTail = buffer.freeTail.next
}
func (buffer *InsertBuffer) PopFreeBlock() insertBlock {
buffer.freeLock.Lock()
defer buffer.freeLock.Unlock()
blockNode := buffer.freeHead
if blockNode == buffer.freeTail {
buffer.freeTail = nil
}
if blockNode == nil {
return nil
}
buffer.freeHead = blockNode.next
return blockNode.block
}
func (buffer *InsertBuffer) PeekFreeBlock() insertBlock {
buffer.freeLock.Lock()
defer buffer.freeLock.Unlock()
blockNode := buffer.freeHead
if blockNode == nil {
return nil
}
return blockNode.block
}
// allocates a new block with given params and returns it
func AllocateNewBlock(signalNames []string, maxBufferBytes int, blockType InsertBlockType, tripInfo bool) insertBlock {
var newBlock insertBlock = nil
if blockType == PivotBlockType {
newBlock = NewProtoPivotBlock(signalNames, maxBufferBytes, tripInfo)
} else if blockType == SignalBlockType {
newBlock = NewProtoSignalBlock(signalNames, maxBufferBytes)
} else if blockType == VinLastBlockType {
newBlock = NewProtoVinLastBlock(signalNames, maxVinCount, tripInfo)
}
return newBlock
}
// appends a row to the buffer.
// Row must match the expected type by the underlying proto block.
func (buffer *InsertBuffer) AppendRow(signalNames []string, row interface{}, producerChannel chan InsertCommand) error {
block := buffer.PeekFreeBlock()
loggedWaiting := false
for block != nil && block.IsFull() {
//if the block is full before append, then pop it and put it on the producer channel.
buffer.ProduceBlock(producerChannel)
block = buffer.PeekFreeBlock()
}
for block == nil {
//no more free blocks are available...
// if the buffer is able to allocate more blocks, then do so. otherwise, wait for a block...
if len(buffer.blockPool) == maxBlockPoolSize {
if !loggedWaiting {
loggedWaiting = true
logger.Warn().Msgf("no available %s blocks for appending, waiting for available block...", buffer.TableName)
}
//wait
time.Sleep(100 * time.Millisecond)
block = buffer.PeekFreeBlock()
} else {
logger.Info().Msgf("%s no available block, allocating new block", buffer.TableName)
block = AllocateNewBlock(signalNames, maxBufferBytes, buffer.blockType, buffer.tripInfo)
buffer.blockPool = append(buffer.blockPool, block)
buffer.AppendFreeBlock(block)
}
}
err := block.AppendRow(signalNames, row)
//if block is full after append, pop it from linked list and put onto producer channel
if block.IsFull() || block.TimeSinceThreshold(buffer.insertDelay) {
buffer.ProduceBlock(producerChannel)
}
return err
}
func (buffer *InsertBuffer) ProduceBlock(producerChannel chan InsertCommand) {
if producerChannel != nil {
logger.Debug().Msgf("ProduceBlock...")
producerChannel <- InsertCommand{
Buffer: buffer,
Block: buffer.PopFreeBlock(),
}
}
}
// searches blockpool for nonEmpty blocks
func (buffer *InsertBuffer) getNonEmptyBlock() insertBlock {
for _, block := range buffer.blockPool {
if block.Len() > 0 {
return block
}
}
return buffer.blockPool[0]
}
// returns any nonempty input from buffer
func (buffer *InsertBuffer) GetInput() proto.Input {
return buffer.getNonEmptyBlock().GetProtoInput(true, nil)
}
// reinitializes the blocks list, assuming any empty block in the pool is free and can be added to list.
// returns size of blocks list
func (buffer *InsertBuffer) InitFreeBlocksList() int {
buffer.freeHead = nil
buffer.freeTail = nil
count := 0
for _, block := range buffer.blockPool {
if block.Len() != 0 {
continue
}
buffer.AppendFreeBlock(block)
count++
}
return count
}
// Inserts and flushes only the head block.
func InsertAndFlushHead(buffer *InsertBuffer, ctx context.Context, conn *ch.Client, breaker *gobreaker.CircuitBreaker, signalsSet map[string]bool) (int, error) {
block := buffer.PopFreeBlock()
if buffer == nil || block == nil {
return 0, errors.Errorf("attempted to insert with nil buffer")
}
block.Lock()
defer block.Unlock()
rows := block.Len()
protoInput := block.GetProtoInput(false, signalsSet)
if protoInput == nil {
// empty buffer, just return
return 0, nil
}
queryContext, cancel := context.WithTimeout(ctx, 60*time.Second)
defer cancel()
insertFunc := func() (interface{}, error) {
return nil, conn.Do(queryContext, ch.Query{
Body: protoInput.Into(buffer.TableName),
Input: protoInput,
})
}
_, err := breaker.Execute(insertFunc)
if err != nil {
return 0, err
}
block.Flush(false)
buffer.AppendFreeBlock(block)
return rows, err
}
// Inserts a block and flushes it, does not readd to free blocks list if error occurred
func InsertAndFlush(command InsertCommand, ctx context.Context, conn *ch.Client, breaker *gobreaker.CircuitBreaker, signalsSet map[string]bool) (int, error) {
buffer := command.Buffer
block := command.Block
if buffer == nil || block == nil {
return 0, errors.Errorf("attempted to insert with nil buffer")
}
block.Lock()
defer block.Unlock()
rows := block.Len()
protoInput := block.GetProtoInput(false, signalsSet)
if protoInput == nil {
// empty buffer, just return
return 0, nil
}
queryContext, cancel := context.WithTimeout(ctx, 5*time.Minute)
defer cancel()
logger.Debug().Msgf("Inserting %d rows to %s...", rows, conn.ServerInfo().DisplayName)
insertFunc := func() (interface{}, error) {
return nil, conn.Do(queryContext, ch.Query{
Body: protoInput.Into(buffer.TableName),
Input: protoInput,
})
}
_, err := breaker.Execute(insertFunc)
if err != nil {
return 0, err
}
block.Flush(false)
buffer.AppendFreeBlock(block)
return rows, err
}
func (buffer *InsertBuffer) Len() int {
length := 0
for _, block := range buffer.blockPool {
length += block.Len()
}
return length
}
func (buffer *InsertBuffer) Cap() int {
length := 0
for _, block := range buffer.blockPool {
length += block.Cap()
}
return length
}
func (buffer *InsertBuffer) Resize(signals []string) {
for _, block := range buffer.blockPool {
block.Resize(signals, maxBufferBytes)
}
}
type insertBlock interface {
AppendRow(signalNames []string, row interface{}) error
GetProtoInput(useLock bool, signalsSet map[string]bool) proto.Input
Flush(useLock bool)
IsFull() bool
GetInsertBlockType() InsertBlockType
Resize(signals []string, maxBytes int)
Lock()
Unlock()
Len() int
Cap() int
IsBusy() bool
TimeSinceThreshold(time.Duration) bool
}
type protoPivotBlock struct {
lock sync.Mutex
busy bool
length int
capacity int
maxBytes int
vin proto.ColStr
timestamp proto.ColDateTime64
tripStart proto.ColDateTime64
tripID proto.ColStr
data []proto.ColFloat64
startTime *time.Time
columnNames []string
appendTripInfo bool
}
// block type with issue
func NewProtoPivotBlock(signalNames []string, maxBufferBytes int, tripInfo bool) *protoPivotBlock {
newBlock := protoPivotBlock{
columnNames: signalNames,
appendTripInfo: tripInfo,
}
logger.Debug().Msgf("NEW PIVOT BLOCK: %d %d", len(signalNames), maxBufferBytes)
newBlock.Resize(signalNames, maxBufferBytes)
return &newBlock
}
func (block *protoPivotBlock) Lock() {
block.lock.Lock()
block.busy = true
}
func (block *protoPivotBlock) Unlock() {
block.lock.Unlock()
block.busy = false
}
func (block *protoPivotBlock) IsBusy() bool {
return block.busy
}
func (block *protoPivotBlock) Len() int {
return block.length
}
func (block *protoPivotBlock) Cap() int {
return block.capacity
}
func (block *protoPivotBlock) TimeSinceThreshold(threshold time.Duration) bool {
return block.startTime == nil || time.Since(*block.startTime) > threshold
}
// Resizes this pivotBlock to the desired number of signal columns, and scales rows to not exceed maxBytes
func (block *protoPivotBlock) Resize(signals []string, maxBytes int) {
block.Lock()
defer block.Unlock()
if len(signals) == len(block.columnNames) && maxBytes == block.maxBytes {
block.Flush(false)
return
}
oldCapacity := block.capacity
oldWidth := len(block.columnNames)
block.columnNames = signals
block.maxBytes = maxBytes
block.capacity = block.estimateMaxRows(len(signals), maxBytes)
block.length = 0
logger.Debug().Msgf("resizing block to %d rows, %d columns", block.capacity, len(signals))
//reallocate memory only if block dimensions have changed
if oldCapacity != block.capacity || oldWidth != len(signals) {
block.vin.Buf = make([]byte, 0, utils.MaxVinLength*block.capacity)
block.vin.Pos = make([]proto.Position, 0, block.capacity)
block.timestamp.Data = make([]proto.DateTime64, 0, block.capacity)
block.tripStart.Data = make([]proto.DateTime64, 0, block.capacity)
block.timestamp.WithPrecision(proto.PrecisionNano)
block.tripStart.WithPrecision(proto.PrecisionNano)
block.tripID.Buf = make([]byte, 0, (utils.MaxVinLength+utils.MaxTimestampLength+1)*block.capacity)
block.tripID.Pos = make([]proto.Position, 0, block.capacity)
block.data = make([]proto.ColFloat64, len(signals))
for i := range block.data {
block.data[i] = make([]float64, 0, block.capacity)
}
}
block.Flush(false)
}
func (block *protoPivotBlock) GetInsertBlockType() InsertBlockType {
return PivotBlockType
}
func (block *protoPivotBlock) IsFull() bool {
block.Lock()
defer block.Unlock()
return block.length >= block.capacity
}
// Given upper bound for bytes, estimate the maximum number of rows to allocate
func (block *protoPivotBlock) estimateMaxRows(numDataColumns int, maxBytes int) int {
rowBytes := 20 + 2 + 8 + 8*numDataColumns //vin, timestamp, data columns per row
if block.appendTripInfo {
rowBytes += 8 + 44 + 2 //tripstart and tripid
}
return maxBytes / rowBytes
}
// Appends a VehicleState as a row to this Block.
func (block *protoPivotBlock) AppendRow(signalNames []string, row interface{}) error {
block.Lock()
defer block.Unlock()
if block.length >= block.capacity {
return errors.WithStack(utils.ErrInsertFullBlock)
}
if len(signalNames) != len(block.data) {
return errors.WithStack(utils.ErrInsertWrongColumns)
}
state, valid := row.(*VehicleState)
if !valid {
return errors.WithStack(utils.ErrInvalidAppendType)
}
block.length++
if block.startTime == nil {
time := time.Now()
block.startTime = &time
}
block.vin.Append(state.VIN)
block.timestamp.Append(state.Timestamp)
if block.appendTripInfo {
block.tripStart.Append(state.TripStart)
block.tripID.Append(state.TripID)
}
// 3 hour leeway
// Saw a lot of signals with a delay around 3 hours, going to now only check for signals over a day old
catchTime := state.Timestamp.Add(-time.Hour * 24)
for i, signal := range signalNames {
value, valid := state.StateValues[signal]
if !valid {
value = math.NaN()
} else {
// If the signal is not included, I'm not going to check its timing.
// should no linger see the !ok case
// Block that should have my issue
signalTime, ok := state.StateTimes[signal]
if !ok {
// So a lot of signals do not have a timestamp on them, not sure why
logSampler.Warn().Str("Location", "protoPivotBlock").Str("VIN", state.VIN).Str("Signal", signal).
Str("Location", "protoPivotBlock").Float64("Value", value).Int("Sample Rate", NO_TIMESTAMP_LOG_SAMPLE_RATE).Msg("AppendRow No Timestamp")
} else {
// If the signal is from 3 hours before the suggested time of the signal
if signalTime.Before(catchTime) {
logger.Warn().Str("VIN", state.VIN).
Str("Signal", signal).
Float64("Value", value).
Time("timestamp.state", state.Timestamp).
Time("timestamp.signal", signalTime).
Str("Location", "protoPivotBlock").
Msg("AppendRow Timestamp Old")
}
}
}
block.data[i].Append(value)
}
return nil
}
// Clears and Resets the buffers in the Block.
func (block *protoPivotBlock) Flush(useLock bool) {
if useLock {
block.Lock()
defer block.Unlock()
}
block.length = 0
block.startTime = nil
block.vin.Reset()
block.timestamp.Reset()
block.tripStart.Reset()
block.tripID.Reset()
for i := range block.data {
block.data[i].Reset()
}
}
// Gets protocol input struct for ch-go batch insertion
func (block *protoPivotBlock) GetProtoInput(useLock bool, signalsSet map[string]bool) proto.Input {
if useLock {
block.Lock()
defer block.Unlock()
}
if block.length == 0 {
return nil
}
input := proto.Input{
{Name: "VIN", Data: block.vin},
{Name: "Timestamp", Data: block.timestamp},
}
if block.appendTripInfo {
input = append(input, proto.InputColumn{Name: "TripStart", Data: block.tripStart})
input = append(input, proto.InputColumn{Name: "TripID", Data: block.tripID})
}
for i := range block.data {
columnName := block.columnNames[i]
ok := true
if len(signalsSet) > 0 {
_, ok = signalsSet[columnName]
}
if !ok {
continue
}
input = append(input, proto.InputColumn{Name: columnName, Data: block.data[i]})
}
return input
}
// Block struct for feature_table_last type of schema
// This block aggregates only 1 row per VIN. vinIndexMap maps the VIN to a row index.
type protoVinLastBlock struct {
protoPivotBlock
vinIndexMap map[string]int
}
func NewProtoVinLastBlock(signalNames []string, numVINs int, tripInfo bool) *protoVinLastBlock {
newBlock := protoVinLastBlock{}
newBlock.columnNames = append(newBlock.columnNames, signalNames...)
newBlock.vinIndexMap = make(map[string]int)
newBlock.appendTripInfo = tripInfo
logger.Debug().Msgf("NEW VIN LAST BLOCK: %d %d", len(signalNames), maxBufferBytes)
bytesPerVIN := 20 + 16 + 16 + 20 //VIN (str), Timestamp, TripStart (timestamp), TripID (str)
bytesPerVIN += 8 * len(signalNames)
newBlock.Resize(signalNames, bytesPerVIN*numVINs)
return &newBlock
}
func (block *protoVinLastBlock) Flush(useLock bool) {
if useLock {
block.Lock()
defer block.Unlock()
}
block.protoPivotBlock.Flush(false)
block.vinIndexMap = make(map[string]int)
}
// Appends a VehicleState as a row to this Block.
// Investigate this code vs protoPivotBlock. WHy the differences, why two of the same thing?
func (block *protoVinLastBlock) AppendRow(signalNames []string, row interface{}) error {
block.Lock()
defer block.Unlock()
if block.length >= block.capacity {
return errors.WithStack(utils.ErrInsertFullBlock)
}
if len(signalNames) != len(block.data) {
return errors.WithStack(utils.ErrInsertWrongColumns)
}
state, valid := row.(*VehicleState)
if !valid {
return errors.WithStack(utils.ErrInvalidAppendType)
}
vinIndex, ok := block.vinIndexMap[state.VIN]
if block.startTime == nil {
time := time.Now()
block.startTime = &time
}
if !ok {
// append state to the end of the buffer
block.length++
block.vin.Append(state.VIN)
block.timestamp.Append(state.Timestamp)
if block.appendTripInfo {
block.tripStart.Append(state.TripStart)
block.tripID.Append(state.TripID)
}
// catchTime := state.Timestamp.Add(-time.Hour * 3)
for i, signal := range signalNames {
value, valid := state.StateValues[signal]
if !valid {
value = math.NaN()
} else {
// If the signal is not included, I'm not going to check its timing.
// should no linger see the !ok case
// signalTime, ok := state.StateTimes[signal]
// if !ok {
// logger.Warn().Str("Location", "protoVinLastBlock, !ok").Msgf("AppendRow no timestamp for %s", signal)
// } else {
// // If the signal is from 3 hours before the suggested time of the signal
// if signalTime.Before(catchTime) {
// logger.Warn().Str("VIN", state.VIN).
// Str("Signal", signal).
// Time("timestamp.state", state.Timestamp).
// Time("timestamp.signal", signalTime).
// Str("Location", "protoVinLastBlock, !ok").
// Msg("AppendRow Timestamp Old")
// }
// }
}
block.data[i].Append(value)
}
block.vinIndexMap[state.VIN] = block.length - 1
} else {
// override only the row mapped to the vin
block.timestamp.Data[vinIndex] = proto.ToDateTime64(state.Timestamp, block.timestamp.Precision)
block.tripStart.Data[vinIndex] = proto.ToDateTime64(state.TripStart, block.timestamp.Precision)
SetColStr(&block.tripID, state.TripID, vinIndex)
//catchTime := state.Timestamp.Add(-time.Hour * 3)
for i, signal := range signalNames {
value, valid := state.StateValues[signal]
if !valid {
value = math.NaN()
} else {
// If the signal is not included, I'm not going to check its timing.
// should no linger see the !ok case
// signalTime, ok := state.StateTimes[signal]
// if !ok {
// logger.Warn().Str("Location", "protoVinLastBlock, ok").Msgf("AppendRow no timestamp for %s", signal)
// } else {
// // If the signal is from 3 hours before the suggested time of the signal
// if signalTime.Before(catchTime) {
// logger.Warn().Str("VIN", state.VIN).
// Str("Signal", signal).
// Time("timestamp.state", state.Timestamp).
// Time("timestamp.signal", signalTime).
// Str("Location", "protoVinLastBlock, ok").
// Msg("AppendRow Timestamp Old")
// }
// }
}
block.data[i][vinIndex] = value
}
}
return nil
}
// Block struct for vehicle_signal type of schema
type protoSignalBlock struct {
lock sync.Mutex
busy bool
length int
capacity int
maxBytes int
startTime *time.Time
vin proto.ColStr
timestamp proto.ColDateTime64
id proto.ColInt16
name proto.ColStr
value proto.ColFloat64
}
func NewProtoSignalBlock(signalNames []string, maxBufferBytes int) *protoSignalBlock {
newBlock := protoSignalBlock{}
newBlock.Resize(signalNames, maxBufferBytes)
return &newBlock
}
func (block *protoSignalBlock) Lock() {
block.lock.Lock()
block.busy = true
}
func (block *protoSignalBlock) Unlock() {
block.lock.Unlock()
block.busy = false
}
func (block *protoSignalBlock) IsBusy() bool {
return block.busy
}
func (block *protoSignalBlock) Len() int {
return block.length
}
func (block *protoSignalBlock) Cap() int {
return block.capacity
}
func (block *protoSignalBlock) TimeSinceThreshold(threshold time.Duration) bool {
return block.startTime == nil || time.Since(*block.startTime) > threshold
}
// Resizes allocated memory for the Block, given maxBytes as the upper bound for memory size
func (block *protoSignalBlock) Resize(signals []string, maxBytes int) {
block.Lock()
defer block.Unlock()
if maxBytes == block.maxBytes {
block.Flush(false)
return
}
oldCapacity := block.capacity
block.maxBytes = maxBytes
block.capacity = block.estimateMaxRows(maxBytes)
logger.Debug().Msgf("resizing block to %d rows", block.capacity)
//reallocate memory only if block dimensions have changed
if oldCapacity != block.capacity {
block.vin.Buf = make([]byte, 0, utils.MaxVinLength*block.capacity)
block.vin.Pos = make([]proto.Position, 0, block.capacity)
block.timestamp.Data = make([]proto.DateTime64, 0, block.capacity)
block.timestamp.WithPrecision(proto.PrecisionMicro)
block.id = make([]int16, 0, block.capacity)
block.name.Buf = make([]byte, 0, 20*block.capacity)
block.name.Pos = make([]proto.Position, 0, block.capacity)
block.value = make([]float64, 0, block.capacity)
}
block.Flush(false)
}
func (block *protoSignalBlock) GetInsertBlockType() InsertBlockType {
return SignalBlockType
}
func (block *protoSignalBlock) IsFull() bool {
block.Lock()
defer block.Unlock()
return block.length >= block.capacity
}
// Given upper bound for bytes, estimate the maximum number of rows to allocate
func (block *protoSignalBlock) estimateMaxRows(maxBytes int) int {
const rowBytes int = 17 + 8 + 2 + 30 + 8 + 2 + 2 //Each row is ~65 bytes.
return maxBytes / rowBytes
}
// Clears and Resets the buffers in the Block.
func (block *protoSignalBlock) Flush(useLock bool) {
if useLock {
block.Lock()
defer block.Unlock()
}
block.vin.Reset()
block.timestamp.Reset()
block.id.Reset()
block.name.Reset()
block.value.Reset()
block.startTime = nil
block.length = 0
}
// Appends a kafka_grpc.GRPC_CANSignal to this block as a row
func (block *protoSignalBlock) AppendRow(signalNames []string, row interface{}) error {
block.Lock()
defer block.Unlock()
if block.length >= block.capacity {
return errors.WithStack(utils.ErrInsertFullBlock)
}
signal, valid := row.(*kafka_grpc.GRPC_CANSignal)
if !valid {
return errors.WithStack(utils.ErrInvalidAppendType)
}
if block.startTime == nil {
time := time.Now()
block.startTime = &time
}
timestamp := utils.FloatToTime(signal.Timestamp)
block.vin.Append(signal.Vin)
block.timestamp.Append(timestamp)
block.id.Append(int16(signal.Id))
block.name.Append(signal.Name)
block.value.Append(signal.Value)
block.length++
return nil
}
// Gets protocol input struct for ch-go batch insertion
func (block *protoSignalBlock) GetProtoInput(useLock bool, signalsSet map[string]bool) proto.Input {
if useLock {
block.Lock()
defer block.Unlock()
}
if block.length == 0 {
return nil
}
input := proto.Input{
{Name: "VIN", Data: block.vin},
{Name: "Timestamp", Data: block.timestamp},
{Name: "Name", Data: block.name},
{Name: "Value", Data: block.value},
{Name: "ID", Data: block.id},
}
return input
}
/// helper methods ///
// Sets the value of string in-place in a proto colstr.
// This will reallocate memory as needed.
func SetColStr(col *proto.ColStr, value string, index int) {
pos := col.Pos[index]
oldLen := pos.End - pos.Start
newLen := len(value)
offset := newLen - oldLen
newEnd := pos.Start + len(value)
oldBufLen := len(col.Buf)
//need to resize buffers
if offset > 0 {
// grow buffer by appending zero bytes, then truncating the length (not the cap) of slice
col.Buf = append(col.Buf, make([]byte, offset)...)[:oldBufLen]
}
// need to shift EVERYTHING after index. This can be slow.
if offset != 0 {
// copy buffer into itself with offset.
copy(col.Buf[newEnd:len(col.Buf)], col.Buf[pos.End:oldBufLen])
for i := index + 1; i < len(col.Pos); i++ {
col.Pos[i].Start += offset
col.Pos[i].End += offset
}
}
//insert
copy(col.Buf[pos.Start:newEnd], value)
col.Pos[index].End = newEnd
}

View File

@@ -0,0 +1,237 @@
package models
import (
"fmt"
"time"
"github.com/fiskerinc/cloud-services/services/jetfire/utils"
"github.com/fiskerinc/cloud-services/pkg/grpc/kafka_grpc"
"github.com/fiskerinc/cloud-services/pkg/logger"
"github.com/fiskerinc/cloud-services/pkg/utils/envtool"
)
var (
maxVinCount = envtool.GetEnvInt("JETFIRE_MAX_VINS", 10000)
timestampThreshold = 500 * time.Millisecond
)
// Vehicle State stores latest state for a particular VIN.
type VehicleState struct {
VIN string
Timestamp time.Time //data timestamp of last data sample
TripStart time.Time //data timestamp of start of trip
TripID string //trip id
StateValues map[string]float64 //map of values in state. Keys correspond to CAN signal name
StateTimes map[string]time.Time //map of timestamps in state. Keys correspond to CAN signal name
InsertTime time.Time // local timestamp of last received data. Used only for removing old vehicle states from cache
pollingMap map[uint]time.Time //map of last polling times. Key matches update flag
// for linked list
Next *VehicleState
prev *VehicleState
}
func (v *VehicleState) TimeSincePolled(updateIndex uint) time.Duration {
pollTime, ok := v.pollingMap[updateIndex]
if !ok {
pollTime = time.Unix(0, 0)
}
return v.Timestamp.Sub(pollTime)
}
func (v *VehicleState) SetPollTime(updateIndex uint) {
v.pollingMap[updateIndex] = v.Timestamp
}
func (v *VehicleState) Clear(newVIN string) {
v.VIN = newVIN
cacheSize := len(v.StateValues)
v.StateValues = make(map[string]float64, cacheSize)
v.StateTimes = make(map[string]time.Time, cacheSize)
v.Timestamp = time.Unix(0, 0)
v.TripStart = v.Timestamp
v.TripID = ""
v.InsertTime = time.Now().UTC()
v.pollingMap = make(map[uint]time.Time)
}
// VehicleCache is a table of vehicle states.
type VehicleCache struct {
Cache map[string]*VehicleState
SignalsSet *map[string]bool
LastTimestamp time.Time
// linked list LIFO of States in order of update.
StatesListHead *VehicleState //oldest update
StatesListTail *VehicleState //newest update
}
func (cache *VehicleCache) Clear() {
clear(cache.Cache)
// disconnect linked list
node := cache.StatesListHead
for node != nil {
next := node.Next
node.Next = nil
node.prev = nil
node = next
}
cache.StatesListHead = nil
cache.StatesListTail = nil
}
// removes the node from linked list and appends it to the right end
func (cache *VehicleCache) ReinsertRight(node *VehicleState) {
if cache.StatesListTail == node {
// node is already the tail. don't do anything
return
}
// move head ptr if we are moving the first node
if cache.StatesListHead == node {
cache.StatesListHead = node.Next
}
// remove from list
if node.prev != nil && node.Next != nil {
p := node.prev
n := node.Next
n.prev = p
p.Next = n
} else if node.prev != nil {
node.prev.Next = nil
} else if node.Next != nil {
node.Next.prev = nil
}
node.prev = nil
node.Next = nil
// append to tail
if cache.StatesListTail != nil {
cache.StatesListTail.Next = node
}
node.prev = cache.StatesListTail
cache.StatesListTail = node
if cache.StatesListHead == nil {
cache.StatesListHead = node
}
}
// removes left node from the linked list AND from the cache map
func (cache *VehicleCache) PopLeft() *VehicleState {
if cache.StatesListHead == nil {
return nil
}
node := cache.StatesListHead
cache.StatesListHead = node.Next
delete(cache.Cache, node.VIN)
return node
}
// Insert CANSignal into vehicle cache. Allocates new vehicle state as necessary.
func (cache *VehicleCache) UpdateSignal(signal *kafka_grpc.GRPC_CANSignal, updateFlag uint) error {
if len(*cache.SignalsSet) > 0 {
// check if signal is in signals set, skip if not in signals set
_, contains := (*cache.SignalsSet)[signal.Name]
if !contains {
return nil
}
}
// if VIN is not in cache, allocate new vehicle state for VIN and add to cache
_, contains := cache.Cache[signal.Vin]
if !contains {
if len(cache.Cache) > maxVinCount {
oldState := cache.PopLeft()
logger.Debug().Msgf("repurposing state %s -> %s", oldState.VIN, signal.Vin)
oldState.Clear(signal.Vin)
cache.Cache[signal.Vin] = oldState
} else {
cache.Cache[signal.Vin] = NewVehicleState(signal.Vin, len(*cache.SignalsSet))
logger.Debug().Msgf("new vehicle state %s", signal.Vin)
}
}
//update value
cache.Cache[signal.Vin].UpdateSignal(signal, updateFlag)
cache.ReinsertRight(cache.Cache[signal.Vin]) //move state to end of orderly linkedlist
cache.LastTimestamp = time.Now().UTC()
return nil
}
// constructs a new VehicleState
func NewVehicleState(VIN string, cacheSize int) *VehicleState {
newState := new(VehicleState)
newState.VIN = VIN
newState.StateValues = make(map[string]float64, cacheSize)
newState.StateTimes = make(map[string]time.Time, cacheSize)
newState.Timestamp = time.Unix(0, 0)
newState.TripStart = newState.Timestamp
newState.TripID = ""
newState.InsertTime = time.Now().UTC()
newState.pollingMap = make(map[uint]time.Time)
return newState
}
// constructs a new VehicleState
func NewVehicleStateDefault(VIN string) *VehicleState {
return NewVehicleState(VIN, 10)
}
// UpdateSignal() updates the vehicle state cache
func (state *VehicleState) UpdateSignal(signal *kafka_grpc.GRPC_CANSignal, updateFlag uint) {
// Mark start of new trip if too much time has elapsed between updates
signalTime := utils.FloatToTime(signal.Timestamp)
ignitionTriggered := false
// check for vehicle ignition rising edge as a trigger for a new trip.
if signal.Name == "BCM_PwrMod" && signal.Value >= 2 && signal.Value <= 4 && !signalTime.Before(state.Timestamp) {
pwrMod, ok := state.StateValues["BCM_PwrMod"]
ignitionTriggered = !ok || (ok && pwrMod < 2)
}
if signalTime.Sub(state.Timestamp) >= utils.TripTimeout || ignitionTriggered {
logger.Debug().Msgf("%s New TripStart: %d, old %d, delta %d", state.VIN, signalTime.Unix(), state.Timestamp.Unix(), signalTime.Sub(state.Timestamp))
state.TripStart = signalTime
state.TripID = fmt.Sprintf("%s_%d", state.VIN, state.TripStart.Unix())
}
// Update the vehicle timestamp
oldTime, ok := state.StateTimes[signal.Name]
if !ok {
oldTime = state.Timestamp
}
if !signalTime.Add(timestampThreshold).Before(oldTime) {
if !signalTime.Before(state.Timestamp) {
state.Timestamp = signalTime
}
// Insert new signal value
state.StateValues[signal.Name] = signal.Value
state.StateTimes[signal.Name] = signalTime
state.InsertTime = time.Now()
} else {
// if receiving message out of timestamp order, only accept new signal value if
// cached value is empty for signal name
_, hasSignal := state.StateValues[signal.Name]
if !hasSignal {
state.StateValues[signal.Name] = signal.Value
state.StateTimes[signal.Name] = signalTime
state.InsertTime = time.Now()
}
utils.LogOutOfOrderMsg(signal.Name, signal.Vin)
}
}

View File

@@ -0,0 +1,164 @@
package server
import (
"context"
"time"
"github.com/fiskerinc/cloud-services/services/jetfire/handlers"
"github.com/fiskerinc/cloud-services/services/jetfire/models"
"github.com/fiskerinc/cloud-services/services/jetfire/services"
"github.com/fiskerinc/cloud-services/pkg/grpc/kafka_grpc"
"github.com/fiskerinc/cloud-services/pkg/kafka"
"github.com/fiskerinc/cloud-services/pkg/logger"
"github.com/fiskerinc/cloud-services/pkg/loggerdataresp"
"github.com/intel-go/fastjson"
"github.com/pkg/errors"
"google.golang.org/protobuf/proto"
)
// StartConsumer runs consumer and puts vehicle signals into a channel for router
func StartConsumer(ctx context.Context, topic string) {
logger.Debug().Str("StartConsumer", "").Send()
defer func() {
if err := recover(); err != nil {
logger.Error().Msgf("PanicConsumer %v", err)
}
}()
producerChannel := make(chan models.InsertCommand, 100)
signalsChannel := make(chan *kafka.Message)
rebalanceChannel := make(chan struct{})
consumer, err := services.GetKafkaConsumer()
if err != nil {
logger.Fatal().Err(err)
panic(err)
}
logger.Debug().Msgf("Starting routeEvents and InserterLoop...")
go routeEvents(signalsChannel, rebalanceChannel, producerChannel)
go InserterLoop(producerChannel, ctx)
for {
logger.Debug().Msgf("Calling ConsumeOrRebalancedCatch...")
consumer.Subscribe([]string{topic})
err = consumer.ConsumeOrRebalancedCatch([]string{topic}, signalsChannel, rebalanceChannel)
if err != nil {
logger.Error().Err(err).Send()
}
if !loggerdataresp.BadDataError(err, loggerdataresp.EofErrorCheck) {
check := consumer.Check(ctx)
if check != nil {
logger.Error().Err(check).Send()
}
}
time.Sleep(500 * time.Millisecond)
// reset the kafka consumer and gc the old one
consumer = nil
for consumer == nil || err != nil {
consumer, err = services.ResetKafkaConsumer()
if err != nil {
logger.Error().Err(err).Send()
}
}
}
}
// processes signals in batch, handles caching of vehicle state and appending to insertion batch caches
func routeEvents(signalsChannel chan *kafka.Message, rebalanceChannel <-chan struct{}, producerChannel chan models.InsertCommand) {
var jsonErr error
var protoErr error
var batchData []*kafka_grpc.GRPC_CANSignal
initialized := false
//Consumer loop
rebalanceFlag := true
for {
select {
case signal := <-signalsChannel:
if signal == nil {
continue
}
rebalanceFlag = true
if !initialized {
// init cache after rebalancing
logger.Debug().Msgf("INITIALIZING CACHE...")
time.Sleep(time.Second)
services.InitCacheFromClickhouse()
initialized = true
logger.Debug().Msgf("INITIALIZED...")
}
batchData, protoErr = unmarshalProtobufSignal(signal)
if protoErr != nil {
batchData, jsonErr = unmarshalJSONSignal(signal)
if jsonErr != nil {
logger.Error().Err(protoErr).Msg("Failed to unmarshal signal as either Protobuf or JSON")
continue
}
}
_, err := handlers.HandleSignalBatch(batchData, services.GetVehicleCache(), producerChannel)
if err != nil {
logger.Error().Err(errors.WithStack(err)).Send()
}
case <-rebalanceChannel:
if rebalanceFlag {
rebalanceFlag = false
logger.Info().Msgf("kafka rebalancing...")
initialized = false
}
}
}
}
func unmarshalProtobufSignal(event *kafka.Message) ([]*kafka_grpc.GRPC_CANSignal, error) {
if event == nil {
err := errors.Errorf("trying to unmarshall null event ptr")
return nil, err
}
batchData := kafka_grpc.GRPC_CANSignalBatchPayload{}
err := proto.Unmarshal(event.Value, &batchData)
if err != nil {
return nil, err
}
return batchData.Data.Cansignals, nil
}
func unmarshalJSONSignal(event *kafka.Message) ([]*kafka_grpc.GRPC_CANSignal, error) {
if event == nil {
err := errors.Errorf("trying to unmarshall null event ptr")
return nil, err
}
//JSON handling is generally slower;
//not only is payload much larger but character insertions to fix json format from optimus is very slow.
batchData := []kafka_grpc.GRPC_CANSignal{}
dataBuffer := make([]byte, len(event.Value)+2)
copy(dataBuffer[1:], event.Value)
dataBuffer[0] = '['
dataBuffer[len(event.Value)+1] = ']'
err := fastjson.Unmarshal(dataBuffer, &batchData)
if err != nil {
logger.Error().Err(err).Send()
return nil, err
}
ptrs := make([]*kafka_grpc.GRPC_CANSignal, len(batchData))
for i := range batchData {
ptrs[i] = &batchData[i]
}
return ptrs, nil
}

View File

@@ -0,0 +1,25 @@
package server
import (
"net/http"
"github.com/fiskerinc/cloud-services/services/jetfire/handlers"
"github.com/fiskerinc/cloud-services/pkg/httphandlers"
"github.com/fiskerinc/cloud-services/pkg/logger"
"github.com/julienschmidt/httprouter"
)
const port string = ":8077"
func StartHTTPServer() {
router := httprouter.New()
router.PanicHandler = httphandlers.HttpRouterPanicHandler
addHandler(router, http.MethodGet, "/reset", handlers.ResetSchemaDefinitions)
logger.Fatal().AnErr("http.ListenAndServe", http.ListenAndServe(port, router)).Send()
}
func addHandler(router *httprouter.Router, method string, path string, handler http.HandlerFunc) {
router.HandlerFunc(method, httphandlers.HttpRouterHandleBaseURL(path), handler)
}

View File

@@ -0,0 +1,99 @@
package server
import (
"context"
"github.com/fiskerinc/cloud-services/services/jetfire/models"
"github.com/fiskerinc/cloud-services/services/jetfire/services"
"time"
"github.com/fiskerinc/cloud-services/pkg/logger"
"github.com/ClickHouse/ch-go"
"github.com/ClickHouse/ch-go/proto"
"github.com/pkg/errors"
"github.com/fiskerinc/cloud-services/pkg/utils/envtool"
)
var (
flushBufferCmd chan interface{}
schemaResetPeriod = envtool.GetEnvDuration("JETFIRE_SCHEMA_RESET_PERIOD_MS", 3600000*12) * time.Millisecond
)
// loop for Inserter goroutine.
// This goroutine is responsible for inserting data into clickhouse
func InserterLoop(producerChannel chan models.InsertCommand, ctx context.Context) {
resetSchemaTicker := time.NewTicker(schemaResetPeriod)
for {
select {
// reset cache vars every hour
case <-resetSchemaTicker.C:
logger.Debug().Msgf("<-resetSchemaTicker")
services.ResetCacheVars()
// process flush buffer command
case <-flushBufferCmd:
logger.Debug().Msgf("<-flushBufferCmd")
//get clickhouse client
client := services.GetShardClient()
for client == nil || client.IsClosed() {
logger.Error().Err(errors.Errorf("bad chgo client , retrying connection...")).Send()
time.Sleep(time.Second)
services.InitShardClients()
client = services.GetShardClient()
}
logger.Debug().Msgf("InsertFlushAllBuffers??")
services.InsertFlushAllBuffers(client)
// process data to be inserted to clickhouse
case command := <-producerChannel:
logger.Debug().Msgf("<-producerChannel")
// get clickhouse client
client := services.GetShardClient()
for client == nil || client.IsClosed() {
logger.Error().Err(errors.Errorf("bad chgo client , retrying connection...")).Send()
time.Sleep(time.Second)
services.InitShardClients()
client = services.GetShardClient()
}
//insert the queued buffer and block in command
var err error
retryInsert := true
count := 0
insertTime := time.Now()
for retryInsert {
count, err = models.InsertAndFlush(command, ctx, client.GetClient(), client.GetBreaker(), nil)
if err == nil {
break
}
logger.Error().Err(errors.WithStack(err)).Send()
// if the error is a mismatching schema error, then pull new schemas
if ch.IsErr(err,
proto.ErrIncompatibleColumns,
proto.ErrNoSuchColumnInTable,
proto.ErrThereIsNoColumn,
proto.ErrIncorrectNumberOfColumns,
) {
services.ResetCacheVars()
} else {
// client.Connect() //close and restart the clickhouse connection
client = services.GetShardClient() //grab a new shard client to retry insert
}
time.Sleep(time.Second)
}
if count == 0 {
continue
}
//log messages relating to clickhouse insertion
logger.Debug().Msgf("done flush Buffer %s, %dms", command.Buffer.TableName, (time.Since(insertTime))/time.Millisecond)
if time.Since(insertTime) > 10*time.Second {
logger.Warn().Msgf("slow row insertions: took %s to insert %d rows for %s", time.Since(insertTime), count, command.Buffer.TableName)
}
}
}
}

View File

@@ -0,0 +1,303 @@
package services
import (
"context"
"fmt"
"github.com/fiskerinc/cloud-services/services/jetfire/models"
"github.com/fiskerinc/cloud-services/services/jetfire/utils"
"reflect"
"strings"
"sync"
"time"
"github.com/fiskerinc/cloud-services/pkg/logger"
"github.com/pkg/errors"
)
var (
//singleton cache definitions for vehicle states
vehicleCache *models.VehicleCache
// lists of variables needed by destination tables
featureVars []string
// set of all variables needing to be tracked
featureSet map[string]bool
varsSet map[string]bool
SchemaLock sync.Mutex
)
// Initializes feature variables by pulling the table schema for destination tables
func ResetCacheVars() {
SchemaLock.Lock()
defer SchemaLock.Unlock()
conn, err := GetClickhouseConnection()
if err != nil {
logger.Error().Err(err).Send()
return
}
featureVars = GetVarList(conn, FEATURE_TABLE, utils.FeatureVarsDefaults)
logger.Debug().Msgf("FEATURE VARS %d", len(featureVars))
featureSet = make(map[string]bool)
varsSet = make(map[string]bool)
for _, key := range featureVars {
varsSet[key] = true
featureSet[key] = true
}
logger.Debug().Msgf("VARSSET %d", len(varsSet))
vCache := GetVehicleCache()
vCache.SignalsSet = &varsSet
//insert and flush the insertion buffers
insertClient := GetShardClient()
//guarantee initialization of singleton buffer
//GetVehicleSignalBatch()
//only insert signals in the intersection of insert buffer columns and updated sink table columns
fBuffer := GetFeatureBatch()
flBuffer := GetFeatureLastBatch()
if insertClient != nil {
ctx := context.Background()
models.InsertAndFlushHead(fBuffer, ctx, insertClient.GetClient(), insertClient.GetBreaker(), featureSet)
models.InsertAndFlushHead(flBuffer, ctx, insertClient.GetClient(), insertClient.GetBreaker(), featureSet)
}
//now resize the buffers and reset the column names
fBuffer.Resize(featureVars)
//now resize the buffers and reset the column names
flBuffer.Resize(featureVars)
}
// flushes the head node for all buffers.
func InsertFlushAllBuffers(client *ChGoClient) {
ctx := context.Background()
//models.InsertAndFlushHead(GetVehicleSignalBatch(), ctx, client.GetClient(), client.GetBreaker(), featureSet)
models.InsertAndFlushHead(GetFeatureBatch(), ctx, client.GetClient(), client.GetBreaker(), featureSet)
models.InsertAndFlushHead(GetFeatureLastBatch(), ctx, client.GetClient(), client.GetBreaker(), featureSet)
}
func GetFeatureVars() []string {
return featureVars
}
// Initializes the VIN cache from clickhouse feature table. This is used during Kafka rebalance events.
func InitCacheFromClickhouse() {
SchemaLock.Lock()
defer SchemaLock.Unlock()
logger.Debug().Msgf("InitCacheFromClickhouse")
//query feature table
featureColumns := append([]string{"VIN", "Timestamp", "TripStart", "TripID"}, featureVars...)
logger.Debug().Msgf("Querying feature table, %d columns", len(featureColumns))
queryString := fmt.Sprintf("SELECT * FROM %s LIMIT 1 BY VIN", FEATURE_LAST_TABLE)
populateCacheFromTable(GetVehicleCache(), queryString, &featureColumns)
logger.Debug().Msgf("Pulled cache from clickhouse! %d vehicles!", len(GetVehicleCache().Cache))
}
// Performs a query, expecting a dynamic schema from the table. Query results are used to populate cache.
// Columns describes the columns expected in the table
func populateCacheFromTable(cache *models.VehicleCache, query string, columns *[]string) {
ctx := context.Background()
logger.Debug().Msgf("query: %s", query)
conn, err := GetClickhouseConnection()
if err != nil {
logger.Error().Err(err)
return
}
if !HasClickhouseParams() {
logger.Error().Msgf("Could not open clickhouse connection. skipping initialization of vehicle caches")
return
}
rows, err := QueryWithBreaker(ctx, conn, &query)
if err != nil {
logger.Error().Err(err).Send()
panic(errors.WithStack(err))
}
if rows == nil {
return
}
columnTypes := rows.ColumnTypes()
row := make([]interface{}, len(columnTypes))
for i, cType := range columnTypes {
kind := cType.ScanType().Kind()
scanName := cType.ScanType().Name()
if kind == reflect.String || strings.Contains(scanName, "string") {
row[i] = new(string)
} else if kind == reflect.Float64 || kind == reflect.Float32 || strings.Contains(scanName, "float") || strings.Contains(scanName, "decimal") {
row[i] = new(float64)
} else if kind == reflect.Int64 || kind == reflect.Int || strings.Contains(scanName, "int") {
row[i] = new(int64)
} else if strings.Contains(scanName, "Time") {
row[i] = new(time.Time)
} else {
row[i] = new(string)
}
}
defer rows.Close()
for rows.Next() {
err := rows.Scan(row...)
if err != nil {
panic(errors.WithStack(err))
}
vin := *row[0].(*string)
timestamp := *row[1].(*time.Time)
_, contains := cache.Cache[vin]
// initialize new state cache as needed
if !contains {
cache.Cache[vin] = models.NewVehicleState(vin, len(varsSet))
}
state := cache.Cache[vin]
if timestamp.After(state.Timestamp) {
for i, valPtr := range row {
key := (*columns)[i]
if key == "VIN" {
continue
}
if key == "Timestamp" {
continue
}
if key == "TripStart" {
state.TripStart = *valPtr.(*time.Time)
state.TripID = fmt.Sprintf("%s_%d", state.VIN, state.TripStart.Unix())
continue
}
if key == "TripID" {
state.TripID = *valPtr.(*string)
continue
}
myValue, hasValue := state.StateValues[key]
if !hasValue || myValue == 0 || timestamp.After(state.Timestamp) {
state.StateValues[key] = *valPtr.(*float64)
}
}
state.Timestamp = timestamp
}
}
// append states to right
for _, state := range cache.Cache {
cache.ReinsertRight(state)
}
}
// Queries clickhouse and reads into the dest struct
func LoadChState(VIN string, dest *models.VehicleState, columns *[]string) error {
ctx := context.Background()
queryString := fmt.Sprintf("SELECT * FROM %s WHERE VIN='%s' LIMIT 1", FEATURE_LAST_TABLE, VIN)
logger.Debug().Msgf("query: %s", queryString)
conn, err := GetClickhouseConnection()
if !HasClickhouseParams() || err != nil {
return errors.Errorf("Could not open clickhouse connection. Failed to load VIN state %s", VIN)
}
rows, err := QueryWithBreaker(ctx, conn, &queryString)
if err != nil {
logger.Error().Err(err).Send()
panic(errors.WithStack(err))
}
if rows == nil {
return nil
}
columnTypes := rows.ColumnTypes()
row := make([]interface{}, len(columnTypes))
for i, cType := range columnTypes {
kind := cType.ScanType().Kind()
scanName := cType.ScanType().Name()
if kind == reflect.String || strings.Contains(scanName, "string") {
row[i] = new(string)
} else if kind == reflect.Float64 || kind == reflect.Float32 || strings.Contains(scanName, "float") || strings.Contains(scanName, "decimal") {
row[i] = new(float64)
} else if kind == reflect.Int64 || kind == reflect.Int || strings.Contains(scanName, "int") {
row[i] = new(int64)
} else if strings.Contains(scanName, "Time") {
row[i] = new(time.Time)
} else {
row[i] = new(string)
}
}
defer rows.Close()
for rows.Next() {
err := rows.Scan(row...)
if err != nil {
return err
}
timestamp := *row[1].(*time.Time)
for i, valPtr := range row {
key := (*columns)[i]
if key == "VIN" {
dest.VIN = *row[0].(*string)
continue
}
if key == "Timestamp" {
dest.Timestamp = timestamp
continue
}
if key == "TripStart" {
dest.TripStart = *valPtr.(*time.Time)
dest.TripID = fmt.Sprintf("%s_%d", dest.VIN, dest.TripStart.Unix())
continue
}
if key == "TripID" {
dest.TripID = *valPtr.(*string)
continue
}
_, ok := dest.StateValues[key]
if ok {
dest.StateValues[key] = *valPtr.(*float64)
}
}
}
GetVehicleCache().ReinsertRight(dest)
return nil
}
// Returns Singleton Vehicle Cache; only one is needed for Jetfire service
func GetVehicleCache() *models.VehicleCache {
if vehicleCache != nil {
return vehicleCache
}
vehicleCache = new(models.VehicleCache)
vehicleCache.SignalsSet = &varsSet
vehicleCache.Cache = make(map[string]*models.VehicleState)
vehicleCache.LastTimestamp = utils.FloatToTime(0.0)
return vehicleCache
}

View File

@@ -0,0 +1,201 @@
package services
import (
"context"
"fmt"
"math/rand"
"time"
fClickhouse "github.com/fiskerinc/cloud-services/pkg/clickhouse"
"github.com/fiskerinc/cloud-services/pkg/logger"
"github.com/ClickHouse/ch-go"
"github.com/pkg/errors"
"github.com/sony/gobreaker"
"gopkg.in/retry.v1"
)
/*
This file implements an interface for the chgo library.
*/
var (
shardClients []*ChGoClient
)
// Gets a shard client at random
func GetShardClient() *ChGoClient {
if len(shardClients) == 0 {
InitShardClients()
}
if len(shardClients) == 0 {
return nil
}
index := rand.Intn(len(shardClients))
client := shardClients[index]
isInvalid := client.GetBreaker().State() == gobreaker.StateOpen && !client.GetClient().IsClosed()
count := 5
//resample shards N times to look for a shard client that is active and circruit breaker is not open
for isInvalid && count > 0 {
count--
index = rand.Intn(len(shardClients))
client = shardClients[index]
isInvalid = client.GetBreaker().State() == gobreaker.StateOpen && !client.GetClient().IsClosed()
}
if isInvalid {
return nil
}
return client
}
func InitShardClients() {
clear(shardClients)
shardClients = shardClients[:0]
client, err := GetClickhouseConnection()
if err != nil {
logger.Error().Err(err).Send()
}
shards := []ShardInfo{}
err = client.Select(
context.Background(),
&shards,
"SELECT shard_num, replica_num, host_address FROM system.clusters WHERE cluster='default'",
)
if err != nil {
err = errors.WithStack(err)
logger.Error().Err(err).Send()
}
for _, s := range shards {
shardName := fmt.Sprintf("%d-%d", s.ShardNum, s.ReplicaNum)
logger.Debug().Msgf("Creating new shard connection %s, %s", s.HostAddress, shardName)
client, _ := NewChgoClient(
s.HostAddress,
fClickhouse.CLICKHOUSE_PORT,
shardName,
fClickhouse.CLICKHOUSE_DB,
fClickhouse.CLICKHOUSE_USER,
fClickhouse.CLICKHOUSE_PASS,
)
shardClients = append(shardClients, client)
}
logger.Info().Msgf("Connected to %d shards", len(shardClients))
}
type ChGoClient struct {
client *ch.Client
shardName string
retry retry.Strategy //retry is used for connecting and reconnecting
breaker *gobreaker.CircuitBreaker //circuit breaker is only used for insertions
ch_host string
ch_port string
ch_db string
ch_user string
ch_pass string
}
func NewChgoClient(ch_host string, ch_port string, ch_shard string, ch_db string, ch_user string, ch_pass string) (*ChGoClient, error) {
newClient := ChGoClient{
shardName: ch_shard,
ch_host: ch_host,
ch_port: ch_port,
ch_db: ch_db,
ch_user: ch_user,
ch_pass: ch_pass,
}
newClient.retry = retry.LimitTime(
120*time.Second,
retry.Exponential{
Initial: 100 * time.Millisecond,
},
)
err := newClient.Connect()
if err != nil {
return nil, errors.WithStack(err)
}
return &newClient, nil
}
func (client *ChGoClient) Connect() error {
if client.client != nil && !client.client.IsClosed() {
client.client.Close()
}
var err error
var newConn *ch.Client
client.breaker = gobreaker.NewCircuitBreaker(gobreaker.Settings{
Name: "clickhouse",
MaxRequests: 1,
Interval: time.Minute * 1,
Timeout: time.Minute * 10,
})
for a := retry.Start(client.retry, nil); a.Next(); {
newConn, err = ch.Dial(
context.Background(),
ch.Options{
Address: fmt.Sprintf("%s:%s", client.ch_host, client.ch_port),
Database: client.ch_db,
User: client.ch_user,
Password: client.ch_pass,
DialTimeout: 1 * time.Minute,
},
)
if err == nil {
client.client = newConn
return nil
}
}
err = errors.WithStack(err)
return err
}
func (c *ChGoClient) IsClosed() bool {
if c.client == nil {
return true
}
return c.client.IsClosed()
}
func (c *ChGoClient) Close() error {
if c.client == nil {
return nil
}
return c.client.Close()
}
func (c *ChGoClient) GetClient() *ch.Client {
return c.client
}
func (c *ChGoClient) GetShardName() string {
return c.shardName
}
func (c *ChGoClient) GetBreaker() *gobreaker.CircuitBreaker {
return c.breaker
}
// helper struct for querying clickhouse shard info
type ShardInfo struct {
ShardNum uint32 `ch:"shard_num"`
ReplicaNum uint32 `ch:"replica_num"`
HostAddress string `ch:"host_address"`
}

View File

@@ -0,0 +1,213 @@
package services
import (
"context"
"fmt"
"github.com/fiskerinc/cloud-services/services/jetfire/models"
"github.com/fiskerinc/cloud-services/services/jetfire/utils"
"sync"
"time"
fClickhouse "github.com/fiskerinc/cloud-services/pkg/clickhouse"
"github.com/fiskerinc/cloud-services/pkg/logger"
"github.com/fiskerinc/cloud-services/pkg/utils/envtool"
"github.com/ClickHouse/clickhouse-go/v2"
"github.com/ClickHouse/clickhouse-go/v2/lib/driver"
"github.com/pkg/errors"
"github.com/sony/gobreaker"
"gopkg.in/retry.v1"
)
var (
conn fClickhouse.ConnInterface
clickLock sync.Mutex
FEATURE_TABLE = envtool.GetEnv("CLICKHOUSE_FEATURE_TABLE", "feature_table_shard")
FEATURE_LAST_TABLE = envtool.GetEnv("CLICKHOUSE_FEATURE_LAST_TABLE", "feature_table_last_shard")
VEHICLE_SIGNAL_TABLE = envtool.GetEnv("CLICKHOUSE_VEHICLE_SIGNAL_TABLE", "vehicle_signal_shard")
clickhouseBreaker = gobreaker.NewCircuitBreaker(gobreaker.Settings{
Name: "clickhouse",
MaxRequests: 1,
Interval: time.Minute * 1,
Timeout: time.Second * 60,
OnStateChange: utils.BreakerStateChange,
ReadyToTrip: func(counts gobreaker.Counts) bool { return counts.Requests > 0 },
})
featureBuffer *models.InsertBuffer
featureLastBuffer *models.InsertBuffer
vehicleSignalBuffer *models.InsertBuffer
retryStrategy = retry.LimitTime(
120*time.Second,
retry.Exponential{
Initial: 100 * time.Millisecond,
},
)
)
func HasClickhouseParams() bool {
return len(envtool.GetEnv("CLICKHOUSE_USER", "")) > 0
}
// Returns singleton instance of clickhouse connection
func GetClickhouseConnection() (fClickhouse.ConnInterface, error) {
clickLock.Lock()
defer clickLock.Unlock()
if conn != nil {
return conn, nil
}
executeWrapper := func() (interface{}, error) {
return fClickhouse.NewConn()
}
var err error
if conn == nil {
//instantiate singleton
newConn, err := clickhouseBreaker.Execute(executeWrapper)
if err != nil {
panic(errors.WithStack(err))
}
conn = newConn.(clickhouse.Conn)
}
return conn, err
}
func SetClickhouseConn(newConn fClickhouse.ConnInterface) {
clickLock.Lock()
defer clickLock.Unlock()
conn = newConn
}
// Returns the buffer struct for batched inserts into Vehicle Signal table
func GetVehicleSignalBatch() *models.InsertBuffer {
if vehicleSignalBuffer != nil {
return vehicleSignalBuffer
}
vehicleSignalBuffer = models.NewInsertBuffer(
false,
nil,
VEHICLE_SIGNAL_TABLE,
envtool.GetEnvDuration("JETFIRE_VEHICLE_SIGNAL_BATCH_PERIOD_MS", 10000)*time.Millisecond,
models.SignalBlockType,
)
return vehicleSignalBuffer
}
// Returns the buffer struct for batched inserts into Feature table
func GetFeatureBatch() *models.InsertBuffer {
if featureBuffer != nil {
return featureBuffer
}
featureBuffer = models.NewInsertBuffer(
true,
featureVars,
FEATURE_TABLE,
envtool.GetEnvDuration("JETFIRE_FEATURE_BATCH_PERIOD_MS", 10000)*time.Millisecond,
models.PivotBlockType,
)
return featureBuffer
}
func GetFeatureLastBatch() *models.InsertBuffer {
if featureLastBuffer != nil {
return featureLastBuffer
}
featureLastBuffer = models.NewInsertBuffer(
true,
featureVars,
FEATURE_LAST_TABLE,
envtool.GetEnvDuration("JETFIRE_FEATURE_BATCH_PERIOD_MS", 10000)*time.Millisecond,
models.VinLastBlockType,
)
return featureLastBuffer
}
// Queries table schema to get ordered list of can signals
func GetVarList(conn fClickhouse.ConnInterface, table string, defaultVarsFile string) []string {
var result []string
ctx := context.Background()
if !HasClickhouseParams() {
logger.Warn().Msgf("No clickhouse params found, reading default vars instead.")
return utils.ReadVarListFromFile(defaultVarsFile)
}
query := fmt.Sprintf("DESCRIBE %s", table)
logger.Debug().Msgf("%s", query)
var rows []descRow
err := SelectWithBreaker(ctx, conn, &query, &rows)
if err != nil {
logger.Warn().Err(err).Msgf("Failed to select data from clickhouse, reading default vars instead...")
return utils.ReadVarListFromFile(defaultVarsFile)
}
for _, row := range rows {
if row.Name == "VIN" || row.Name == "Timestamp" {
continue
}
if row.Name == "TripID" || row.Name == "TripStart" {
continue
}
result = append(result, row.Name)
}
return result
}
func SelectWithBreaker(ctx context.Context, conn fClickhouse.ConnInterface, query *string, buffer interface{}) error {
var err error
selectWrapper := func() (interface{}, error) {
return nil, conn.Select(ctx, buffer, *query)
}
for a := retry.Start(retryStrategy, nil); a.Next(); {
_, err = clickhouseBreaker.Execute(selectWrapper)
if err == nil {
break
}
logger.Error().Err(err).Send()
}
return err
}
func QueryWithBreaker(ctx context.Context, conn fClickhouse.ConnInterface, query *string) (driver.Rows, error) {
var rows interface{}
var err error
queryWrapper := func() (interface{}, error) {
return conn.Query(ctx, *query)
}
for a := retry.Start(retryStrategy, nil); a.Next(); {
rows, err = clickhouseBreaker.Execute(queryWrapper)
if err == nil && rows != nil {
break
}
}
//exit if rows is nil; do not attempt to convert to driver.Rows
if rows == nil {
return nil, err
}
return rows.(driver.Rows), err
}
type descRow struct {
Name string `ch:"name"`
Type string `ch:"type"`
Default_type string `ch:"default_type"`
Default_expression string `ch:"default_expression"`
Comment string `ch:"comment"`
Codec_expression string `ch:"codec_expression"`
Ttl_expression string `ch:"ttl_expression"`
}

View File

@@ -0,0 +1,72 @@
package services
import (
"sync"
"time"
"github.com/fiskerinc/cloud-services/pkg/kafka"
"github.com/fiskerinc/cloud-services/pkg/logger"
"github.com/pkg/errors"
"gopkg.in/retry.v1"
)
const ServiceName = "jetfire"
var (
consumer kafka.BaseConsumerInterface = nil
consumerLock sync.Mutex
kafkaRetry = retry.LimitTime(
120*time.Second,
retry.Exponential{
Initial: 100 * time.Millisecond,
})
)
// GetKafkaConsumer returns singleton instance of kafka consumer
func GetKafkaConsumer() (kafka.BaseConsumerInterface, error) {
consumerLock.Lock()
defer consumerLock.Unlock()
if consumer != nil {
return consumer, nil
}
var err error
for a := retry.Start(kafkaRetry, nil); a.Next(); {
newConsumer, err := kafka.NewBaseConsumer(ServiceName, -1, -1, true)
if err == nil {
consumer = newConsumer
return consumer, nil
}
logger.Error().Err(err).Send()
}
err = errors.WithStack(err)
return nil, err
}
// stops the kafka consumer, instantiates new one. gc should clean up old one
func ResetKafkaConsumer() (kafka.BaseConsumerInterface, error) {
consumerLock.Lock()
defer consumerLock.Unlock()
logger.Info().Msgf("Resetting Kafka Consumer...")
if consumer != nil {
consumer.Stop()
consumer = nil
}
var err error
for a := retry.Start(kafkaRetry, nil); a.Next(); {
newConsumer, err := kafka.NewBaseConsumer(ServiceName, -1, -1, true)
if err == nil {
consumer = newConsumer
return consumer, nil
}
logger.Error().Err(err).Send()
}
err = errors.WithStack(err)
return nil, err
}

16
services/jetfire/set_envs.sh Executable file
View File

@@ -0,0 +1,16 @@
#!/usr/bin/bash
# This is a tool to just set env vars from .env
# This is used instead of a golang dotenv package,
# because there are some variables in fiskerinc.com/modules/clickhouse
# that are initialized before init() is called.
# Usage:
# Run this first before running go module locally:
#
# source ./set_envs.sh
# go run main.go
#
set -a
source .env
set +a

View File

@@ -0,0 +1,103 @@
package tests
/*
This file implements benchmarks for
1. Batch handling of messages.
The intention is to measure performance of appending rows to InsertBuffer
2. Batch serialization of messages
The intention is to measure performance of preparing data for clickhouse insertion
Because of the size of the input data for this benchmark, it is recommended to set
JETFIRE_BUFFER_MAX_BYTES=1073741824
Or more; otherwise the benchmark may hang while waiting for the nonexistant inserter thread to flush the buffer.
*/
import (
"github.com/fiskerinc/cloud-services/services/jetfire/handlers"
"github.com/fiskerinc/cloud-services/services/jetfire/services"
"os"
"testing"
"github.com/fiskerinc/cloud-services/pkg/grpc/kafka_grpc"
"github.com/ClickHouse/ch-go/proto"
"github.com/intel-go/fastjson"
)
var benchmarkJSONPayload = []byte{}
var benchmarkBatchPayload = []kafka_grpc.GRPC_CANSignal{}
var benchmarkBatchPtrs = []*kafka_grpc.GRPC_CANSignal{}
func benchInit() {
//1176 messages long
jsonPath := "test-batch-msg.json"
os.Chdir("./tests/")
data, err := os.ReadFile(jsonPath)
if err != nil {
panic(err)
}
benchmarkJSONPayload = data
fastjson.Unmarshal(data, &benchmarkBatchPayload)
benchmarkBatchPtrs = make([]*kafka_grpc.GRPC_CANSignal, len(benchmarkBatchPayload))
for i := range benchmarkBatchPayload {
benchmarkBatchPtrs[i] = &benchmarkBatchPayload[i]
}
services.ResetCacheVars()
}
func benchmarkMessageHandler(batchData []*kafka_grpc.GRPC_CANSignal, b *testing.B) {
cache := services.GetVehicleCache()
for i := 0; i < b.N; i++ {
handlers.HandleSignalBatch(batchData, cache, nil)
}
b.StopTimer()
}
func BenchmarkMessageHandler(b *testing.B) {
benchInit()
b.ResetTimer()
benchmarkMessageHandler(benchmarkBatchPtrs, b)
}
func BenchmarkSignalSerialization(b *testing.B) {
benchInit()
cache := services.GetVehicleCache()
handlers.HandleSignalBatch(benchmarkBatchPtrs, cache, nil)
b.ResetTimer()
dummy := proto.Input{}
serializedLength := 0
rowsLength := 0
for i := 0; i < b.N; i++ {
dummy = services.GetVehicleSignalBatch().GetInput()
serializedLength += len(dummy)
rowsLength += services.GetVehicleSignalBatch().Len()
}
b.StopTimer()
}
func BenchmarkFeatureSerialization(b *testing.B) {
benchInit()
cache := services.GetVehicleCache()
handlers.HandleSignalBatch(benchmarkBatchPtrs, cache, nil)
b.ResetTimer()
dummy := proto.Input{}
serializedLength := 0
rowsLength := 0
for i := 0; i < b.N; i++ {
dummy = services.GetFeatureBatch().GetInput()
serializedLength += len(dummy)
rowsLength += services.GetFeatureBatch().Len()
}
b.StopTimer()
}

View File

@@ -0,0 +1,251 @@
package tests
import (
"fmt"
"github.com/fiskerinc/cloud-services/services/jetfire/services"
"github.com/fiskerinc/cloud-services/services/jetfire/utils"
"testing"
"github.com/fiskerinc/cloud-services/pkg/grpc/kafka_grpc"
"github.com/stretchr/testify/assert"
)
const testVIN = "TESTVIN1234567890"
var testCanSignalBatch = []kafka_grpc.GRPC_CANSignal{
{Vin: testVIN, Timestamp: 600.0, Id: 816, Name: "BMS_PwrBattRmngCpSOC", Value: 100},
{Vin: testVIN, Timestamp: 601.0, Id: 792, Name: "ESP_VehSpd", Value: 0},
{Vin: testVIN, Timestamp: 602.0, Id: 816, Name: "BMS_PwrBattRmngCpSOC", Value: 90},
{Vin: testVIN, Timestamp: 603.0, Id: 792, Name: "ESP_VehSpd", Value: 10},
{Vin: testVIN, Timestamp: 604.0, Id: 816, Name: "BMS_PwrBattRmngCpSOC", Value: 80},
{Vin: testVIN, Timestamp: 605.0, Id: 792, Name: "ESP_VehSpd", Value: 20},
{Vin: testVIN, Timestamp: 606.0, Id: 816, Name: "BMS_PwrBattRmngCpSOC", Value: 80},
{Vin: testVIN, Timestamp: 607.0, Id: 792, Name: "ESP_VehSpd", Value: 30},
{Vin: testVIN, Timestamp: 608.0, Id: 816, Name: "BMS_PwrBattRmngCpSOC", Value: 70},
{Vin: testVIN, Timestamp: 609.0, Id: 792, Name: "ESP_VehSpd", Value: 40},
{Vin: testVIN, Timestamp: 608.0, Id: 816, Name: "BMS_PwrBattRmngCpSOC", Value: 60},
{Vin: testVIN, Timestamp: 607.0, Id: 792, Name: "ESP_VehSpd", Value: 50},
{Vin: testVIN, Timestamp: 610.0, Id: 816, Name: "BMS_PwrBattRmngCpSOC", Value: 50},
{Vin: testVIN, Timestamp: 611.0, Id: 792, Name: "ESP_VehSpd", Value: 60},
{Vin: testVIN, Timestamp: 612.0, Id: 816, Name: "BMS_PwrBattRmngCpSOC", Value: 40},
{Vin: testVIN, Timestamp: 613.0, Id: 792, Name: "ESP_VehSpd", Value: 70},
{Vin: testVIN, Timestamp: 614.0, Id: 816, Name: "BMS_PwrBattRmngCpSOC", Value: 30},
{Vin: testVIN, Timestamp: 615.0, Id: 792, Name: "ESP_VehSpd", Value: 80},
{Vin: testVIN, Timestamp: 616.0, Id: 816, Name: "BMS_PwrBattRmngCpSOC", Value: 20},
{Vin: testVIN, Timestamp: 617.0, Id: 792, Name: "ESP_VehSpd", Value: 90},
{Vin: testVIN, Timestamp: 618.0, Id: 816, Name: "BMS_PwrBattRmngCpSOC", Value: 60},
{Vin: testVIN, Timestamp: 619.0, Id: 792, Name: "ESP_VehSpd", Value: 100},
{Vin: testVIN, Timestamp: 650.0, Id: 792, Name: "ESP_VehSpd", Value: 100},
{Vin: testVIN, Timestamp: 800.0, Id: 792, Name: "ESP_VehSpd", Value: 100},
{Vin: testVIN, Timestamp: 1500.0, Id: 816, Name: "BMS_PwrBattRmngCpSOC", Value: 20},
{Vin: testVIN, Timestamp: 1501.0, Id: 792, Name: "ESP_VehSpd", Value: 90},
{Vin: testVIN, Timestamp: 1502.0, Id: 816, Name: "BMS_PwrBattRmngCpSOC", Value: 45},
{Vin: testVIN, Timestamp: 1503.0, Id: 792, Name: "ESP_VehSpd", Value: 13},
{Vin: testVIN, Timestamp: 1510.0, Id: 819, Name: "BCM_PwrMod", Value: 0},
{Vin: testVIN, Timestamp: 1511.0, Id: 819, Name: "BCM_PwrMod", Value: 2},
{Vin: testVIN, Timestamp: 1512.0, Id: 792, Name: "ESP_VehSpd", Value: 11},
}
var testCanSignalOrderBatch = []kafka_grpc.GRPC_CANSignal{
{Vin: testVIN, Timestamp: 700.0, Id: 816, Name: "BMS_PwrBattRmngCpSOC", Value: 100},
{Vin: testVIN, Timestamp: 601.0, Id: 792, Name: "ESP_VehSpd", Value: 0},
{Vin: testVIN, Timestamp: 603.0, Id: 792, Name: "ESP_VehSpd", Value: 10},
{Vin: testVIN, Timestamp: 704.0, Id: 816, Name: "BMS_PwrBattRmngCpSOC", Value: 80},
{Vin: testVIN, Timestamp: 605.0, Id: 792, Name: "ESP_VehSpd", Value: 20},
{Vin: testVIN, Timestamp: 607.0, Id: 792, Name: "ESP_VehSpd", Value: 30},
{Vin: testVIN, Timestamp: 708.0, Id: 816, Name: "BMS_PwrBattRmngCpSOC", Value: 70},
{Vin: testVIN, Timestamp: 609.0, Id: 792, Name: "ESP_VehSpd", Value: 40},
{Vin: testVIN, Timestamp: 607.0, Id: 792, Name: "ESP_VehSpd", Value: 50},
{Vin: testVIN, Timestamp: 710.0, Id: 816, Name: "BMS_PwrBattRmngCpSOC", Value: 50},
{Vin: testVIN, Timestamp: 611.0, Id: 792, Name: "ESP_VehSpd", Value: 60},
{Vin: testVIN, Timestamp: 613.0, Id: 792, Name: "ESP_VehSpd", Value: 70},
{Vin: testVIN, Timestamp: 714.0, Id: 816, Name: "BMS_PwrBattRmngCpSOC", Value: 30},
{Vin: testVIN, Timestamp: 615.0, Id: 792, Name: "ESP_VehSpd", Value: 80},
{Vin: testVIN, Timestamp: 617.0, Id: 792, Name: "ESP_VehSpd", Value: 90},
{Vin: testVIN, Timestamp: 718.0, Id: 816, Name: "BMS_PwrBattRmngCpSOC", Value: 60},
{Vin: testVIN, Timestamp: 619.0, Id: 792, Name: "ESP_VehSpd", Value: 100},
}
func TestVehicleCache(t *testing.T) {
cache := services.GetVehicleCache()
cache.Clear()
for i, signal := range testCanSignalBatch {
cache.UpdateSignal(&signal, 0x0)
if i == 4 {
// testing signal aggregation
state, containsState := cache.Cache[testVIN]
assert.True(t, containsState)
assert.NotNil(t, state)
assert.WithinDuration(t, state.Timestamp, utils.FloatToTime(604.0), 1e8)
assert.WithinDuration(t, state.TripStart, utils.FloatToTime(600.0), 1e8)
assert.Equal(t, state.StateValues["BMS_PwrBattRmngCpSOC"], 80.0)
assert.Equal(t, state.StateValues["ESP_VehSpd"], 10.0)
}
if i == 11 {
// testing out of order messages
state := cache.Cache[testVIN]
assert.WithinDuration(t, state.Timestamp, utils.FloatToTime(609.0), 1e8)
assert.WithinDuration(t, state.TripStart, utils.FloatToTime(600.0), 1e8)
assert.Equal(t, state.StateValues["BMS_PwrBattRmngCpSOC"], 60.0)
assert.Equal(t, state.StateValues["ESP_VehSpd"], 40.0)
}
if i == 17 {
// testing signal aggregation after message order is restored
state := cache.Cache[testVIN]
assert.WithinDuration(t, state.Timestamp, utils.FloatToTime(615.0), 1e8)
assert.WithinDuration(t, state.TripStart, utils.FloatToTime(600.0), 1e8)
assert.Equal(t, state.StateValues["BMS_PwrBattRmngCpSOC"], 30.0)
assert.Equal(t, state.StateValues["ESP_VehSpd"], 80.0)
}
if i == 22 {
state := cache.Cache[testVIN]
assert.WithinDuration(t, state.Timestamp, utils.FloatToTime(650.0), 1e8)
assert.WithinDuration(t, state.TripStart, utils.FloatToTime(600.0), 1e8)
assert.Equal(t, state.StateValues["BMS_PwrBattRmngCpSOC"], 60.0)
assert.Equal(t, state.StateValues["ESP_VehSpd"], 100.0)
}
if i == 23 {
state := cache.Cache[testVIN]
assert.WithinDuration(t, state.Timestamp, utils.FloatToTime(800.0), 1e8)
assert.WithinDuration(t, state.TripStart, utils.FloatToTime(600.0), 1e8)
assert.Equal(t, state.StateValues["BMS_PwrBattRmngCpSOC"], 60.0)
assert.Equal(t, state.StateValues["ESP_VehSpd"], 100.0)
}
if i == 25 {
state := cache.Cache[testVIN]
assert.WithinDuration(t, state.Timestamp, utils.FloatToTime(1501.0), 1e8)
assert.WithinDuration(t, state.TripStart, utils.FloatToTime(1500.0), 1e8)
assert.Equal(t, state.StateValues["BMS_PwrBattRmngCpSOC"], 20.0)
assert.Equal(t, state.StateValues["ESP_VehSpd"], 90.0)
}
}
assert.Equal(t, len(cache.Cache), 1)
// testing large timestamp gap; trigger new trip
state := cache.Cache[testVIN]
tripStartTime := utils.FloatToTime(1511.0)
assert.WithinDuration(t, state.TripStart, tripStartTime, 1e8)
assert.Equal(t, state.TripID, fmt.Sprintf("%s_%d", testVIN, 1511))
assert.WithinDuration(t, state.Timestamp, utils.FloatToTime(1512.0), 1e8)
assert.Equal(t, state.StateValues["BMS_PwrBattRmngCpSOC"], 45.0)
assert.Equal(t, state.StateValues["ESP_VehSpd"], 11.0)
}
func TestVehicleCacheOrderly(t *testing.T) {
cache := services.GetVehicleCache()
cache.Clear()
for i, signal := range testCanSignalOrderBatch {
cache.UpdateSignal(&signal, 0x0)
if i == 8 {
state := cache.Cache[testVIN]
assert.WithinDuration(t, state.Timestamp, utils.FloatToTime(708.0), 1e8)
assert.WithinDuration(t, state.TripStart, utils.FloatToTime(700.0), 1e8)
assert.Equal(t, state.StateValues["BMS_PwrBattRmngCpSOC"], 70.0)
assert.Equal(t, state.StateValues["ESP_VehSpd"], 40.0)
}
}
state := cache.Cache[testVIN]
assert.WithinDuration(t, state.Timestamp, utils.FloatToTime(718.0), 1e8)
assert.WithinDuration(t, state.TripStart, utils.FloatToTime(700.0), 1e8)
assert.Equal(t, state.StateValues["BMS_PwrBattRmngCpSOC"], 60.0)
assert.Equal(t, state.StateValues["ESP_VehSpd"], 100.0)
}
func TestVehicleCacheList(t *testing.T) {
cache := services.GetVehicleCache()
cache.Clear()
signal := kafka_grpc.GRPC_CANSignal{
Vin: "TESTVIN1",
Timestamp: 0.0,
Id: 816,
Name: "ESP_VehSpd",
Value: 792,
}
cache.UpdateSignal(&signal, 0x0)
signal = kafka_grpc.GRPC_CANSignal{
Vin: "TESTVIN2",
Timestamp: 0.0,
Id: 816,
Name: "ESP_VehSpd",
Value: 792,
}
cache.UpdateSignal(&signal, 0x0)
signal = kafka_grpc.GRPC_CANSignal{
Vin: "TESTVIN3",
Timestamp: 0.0,
Id: 816,
Name: "ESP_VehSpd",
Value: 792,
}
cache.UpdateSignal(&signal, 0x0)
expectedVins := []string{"TESTVIN1", "TESTVIN2", "TESTVIN3"}
node := cache.StatesListHead
i := 0
for node != nil {
assert.Equal(t, expectedVins[i], node.VIN)
node = node.Next
i++
}
node2 := cache.PopLeft()
expectedVins = []string{"TESTVIN2", "TESTVIN3"}
node = cache.StatesListHead
i = 0
for node != nil {
assert.Equal(t, expectedVins[i], node.VIN)
node = node.Next
i++
}
cache.ReinsertRight(node2)
expectedVins = []string{"TESTVIN2", "TESTVIN3", "TESTVIN1"}
node = cache.StatesListHead
i = 0
for node != nil {
assert.Equal(t, expectedVins[i], node.VIN)
node = node.Next
i++
}
cache.ReinsertRight(cache.Cache["TESTVIN3"])
expectedVins = []string{"TESTVIN2", "TESTVIN1", "TESTVIN3"}
node = cache.StatesListHead
i = 0
for node != nil {
assert.Equal(t, expectedVins[i], node.VIN)
node = node.Next
i++
}
}

View File

@@ -0,0 +1,151 @@
package tests
import (
"context"
"fmt"
"github.com/fiskerinc/cloud-services/services/jetfire/server"
"github.com/fiskerinc/cloud-services/services/jetfire/services"
"github.com/fiskerinc/cloud-services/services/jetfire/utils"
"os"
"testing"
"time"
"github.com/fiskerinc/cloud-services/pkg/grpc/kafka_grpc"
"github.com/fiskerinc/cloud-services/pkg/kafka"
"github.com/fiskerinc/cloud-services/pkg/logger"
"github.com/intel-go/fastjson"
"github.com/stretchr/testify/assert"
"google.golang.org/protobuf/proto"
)
var batchDataSignal = []kafka_grpc.GRPC_CANSignal{}
var dataToPublish kafka_grpc.GRPC_CANSignalBatchPayload
var startTimestamp = time.Now().UTC()
func TestIntegration(t *testing.T) {
t.Skip()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
//cleaning up previous run
conn, err := services.GetClickhouseConnection()
if err != nil {
logger.Error().Err(err)
}
startTimestamp = time.Now().UTC()
timestampString := fmt.Sprintf(
"%d-%d-%d %d:%d:%d",
startTimestamp.Year(),
startTimestamp.Month(),
startTimestamp.Day(),
startTimestamp.Hour(),
startTimestamp.Minute(),
startTimestamp.Second(),
)
println("cleaning up previous run")
conn.Exec(ctx, fmt.Sprintf("ALTER TABLE %s DELETE WHERE VIN=='%s' AND Timestamp<'%s'", services.FEATURE_TABLE, testVIN, timestampString))
conn.Exec(ctx, fmt.Sprintf("ALTER TABLE %s DELETE WHERE VIN=='%s' AND Timestamp<'%s'", services.VEHICLE_SIGNAL_TABLE, testVIN, timestampString))
// conn.Ping(ctx)
//initialization
cache := services.GetVehicleCache()
cache.Clear()
readTestData()
producer, err := kafka.NewAsyncProducer(ctx)
assert.Nil(t, err)
// runJetfire(ctx)
publishDuration := int64(10)
testDuration := time.Duration(publishDuration*2) * time.Second //batch inserts take some time to run. give it some time...
// begin publishing kafka data. Update message timestamps first.
grpcData, err := proto.Marshal(&dataToPublish)
assert.Nil(t, err)
err = producer.ProduceBinary(kafka.VehicleSignal, testVIN, grpcData, nil)
assert.Nil(t, err)
//wait a while since kafka has to rebalance, wait for clickhouse inserts to trigger
time.Sleep(testDuration)
//check clickhouse, count rows
println("querying clickhouse feature...")
query := fmt.Sprintf("SELECT VIN, Timestamp FROM %s WHERE VIN=='%s' AND Timestamp>='%s'",
services.FEATURE_TABLE,
testVIN,
timestampString,
)
checkRows(query, 1, t)
println("querying clickhouse vehicle_signal...")
query = fmt.Sprintf("SELECT VIN, Timestamp FROM %s WHERE VIN=='%s' AND Timestamp>='%s'",
services.VEHICLE_SIGNAL_TABLE,
testVIN,
timestampString,
)
checkRows(query, 1000, t)
}
func checkRows(query string, expected int, t *testing.T) {
conn, err := services.GetClickhouseConnection()
if err != nil {
logger.Error().Err(err)
return
}
fmt.Println(query)
rows, err := conn.Query(context.Background(), query)
assert.Nil(t, err)
count := int(0)
defer rows.Close()
for rows.Next() {
count++
}
assert.GreaterOrEqual(t, count, expected)
}
func readTestData() {
//1176 messages long
jsonPath := "./test-batch-msg.json"
data, err := os.ReadFile(jsonPath)
if err != nil {
panic(err)
}
fastjson.Unmarshal(data, &batchDataSignal)
dataPtr := make([]*kafka_grpc.GRPC_CANSignal, len(batchDataSignal))
offset := -1.0
for i := range batchDataSignal {
dataPtr[i] = &batchDataSignal[i]
// find min timestamp in batch data
if offset < 0 || offset > batchDataSignal[i].Timestamp {
offset = dataPtr[i].Timestamp
}
dataPtr[i].Vin = testVIN // in case we need to chagne the test vin to follow proper pattern
}
for i := range batchDataSignal {
dataPtr[i].Timestamp += utils.TimeToFloat(startTimestamp) - offset
}
dataToPublish = kafka_grpc.GRPC_CANSignalBatchPayload{Data: &kafka_grpc.GRPC_CANSignalData{
Cansignals: dataPtr,
}}
}
func runJetfire(ctx context.Context) {
// first initialize and run jetfire application loops
services.ResetCacheVars()
go server.StartConsumer(ctx, kafka.VehicleSignal)
}

View File

@@ -0,0 +1,39 @@
package tests
import (
"fmt"
"github.com/fiskerinc/cloud-services/services/jetfire/handlers"
"github.com/fiskerinc/cloud-services/services/jetfire/services"
"github.com/fiskerinc/cloud-services/services/jetfire/utils"
"testing"
"github.com/fiskerinc/cloud-services/pkg/grpc/kafka_grpc"
"github.com/stretchr/testify/assert"
)
func TestHandleSignalMessage(t *testing.T) {
cache := services.GetVehicleCache()
ptrArray := make([]*kafka_grpc.GRPC_CANSignal, len(testCanSignalBatch))
for i := range testCanSignalBatch {
ptrArray[i] = &testCanSignalBatch[i]
}
services.ResetCacheVars()
cache = services.GetVehicleCache()
cache.Clear()
handlers.HandleSignalBatch(ptrArray, cache, nil)
assert.Equal(t, len(cache.Cache), 1)
// testing large timestamp gap; trigger new trip
state := cache.Cache[testVIN]
tripStartTime := utils.FloatToTime(1511.0)
assert.WithinDuration(t, state.TripStart, tripStartTime, 1e8)
assert.Equal(t, state.TripID, fmt.Sprintf("%s_%d", testVIN, 1511))
assert.WithinDuration(t, state.Timestamp, utils.FloatToTime(1512.0), 1e8)
assert.Equal(t, state.StateValues["BMS_PwrBattRmngCpSOC"], 45.0)
assert.Equal(t, state.StateValues["ESP_VehSpd"], 11.0)
}

View File

@@ -0,0 +1,26 @@
//go:build reset
// +build reset
package tests
import (
"net/http"
"testing"
"github.com/fiskerinc/cloud-services/services/jetfire/handlers"
th "github.com/fiskerinc/cloud-services/pkg/testhelper"
)
func TestResetSchema(t *testing.T) {
tests := []th.BasicHttpTest{
{
Name: "Reset",
Request: th.MakeTestRequest(http.MethodGet, "http://example.com/reset", nil),
ExpectedStatus: http.StatusOK,
ExpectedResponse: "",
},
}
th.RunBasicHttpTests(t, tests, handlers.ResetSchemaDefinitions)
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,25 @@
package utils
import (
"math"
"time"
"github.com/fiskerinc/cloud-services/pkg/utils/envtool"
)
var (
//other cache constants
TripTimeout = time.Duration(envtool.GetEnvInt64("JETFIRE_TRIP_TIMEOUT_MS", 600000)) * time.Millisecond
EmptySignal = math.NaN()
//bitwise flags for state updates.
//Additional flags can be added for additional sink tables later on
FeatureUpdateFlag uint = 0x1
LatestUpdateFlag uint = 0x2
MaxVinLength = 20
MaxTimestampLength = 12
FeatureVarsDefaults = "default-feature-vars.json"
)

View File

@@ -0,0 +1,11 @@
package utils
import "errors"
var (
ErrInsertFullBlock = errors.New("appending row into full block")
ErrInsertWrongColumns = errors.New("appending buffer with incorrect number of columns")
ErrInvalidAppendType = errors.New("appending invalid type to block")
ErrNilClickhouseClient = errors.New("nil clickhouse client")
ErrNilKafkaConsumer = errors.New("nil kafka consumer")
)

View File

@@ -0,0 +1,63 @@
package utils
import (
"math"
"os"
"time"
"github.com/fiskerinc/cloud-services/pkg/logger"
"github.com/intel-go/fastjson"
"github.com/sony/gobreaker"
)
// Converts float64 (as decimal seconds in unix epoch) to Time.Time struct
func FloatToTime(float float64) time.Time {
s := int64(math.Floor(float))
ns := int64((float - math.Floor(float)) * 1e9)
return time.Unix(
s,
ns,
)
}
func TimeToFloat(timestamp time.Time) float64 {
return float64(timestamp.UnixNano()) / 1e9
}
func FixFloatTimestampScale(float float64) float64 {
for float > 9999999999 {
float /= 1000
}
return float
}
// marchTimer increments the value at timer until newTime has been reached.
// This is used to try to maintain consistent downsample and insertion periods.
func MarchTimer(timer *time.Time, newTime *time.Time, delay time.Duration) {
for timer.Before(*newTime) {
*timer = timer.Add(delay)
}
}
func ReadVarListFromFile(varsFile string) []string {
data, err := os.ReadFile(varsFile)
if err != nil {
// when running tests, pwd is in the wrong directory to find the default json files.
os.Chdir("..")
data, err = os.ReadFile(varsFile)
if err != nil {
panic(err)
}
}
var result []string
err = fastjson.Unmarshal(data, &result)
if err != nil {
panic(err)
}
return result
}
func BreakerStateChange(name string, from gobreaker.State, to gobreaker.State) {
logger.Warn().Stack().Msgf("%s breaker change from %d to %d", name, from, to)
}

View File

@@ -0,0 +1,55 @@
package utils
import (
"time"
"github.com/fiskerinc/cloud-services/pkg/logger"
"github.com/rs/zerolog"
)
var (
outOfOrderSignalMap = make(map[string]uint)
outOfOrderVINMap = make(map[string]uint)
outOfOrderCount uint = 0
outOfOrderLogDelay = time.Hour
outOfOrderTime = time.Now().UTC()
)
// This function is for aggregating and logging out of order incoming messages
func LogOutOfOrderMsg(signal string, VIN string) {
_, ok := outOfOrderSignalMap[signal]
if !ok {
outOfOrderSignalMap[signal] = 0
}
outOfOrderSignalMap[signal] += 1
_, ok = outOfOrderVINMap[VIN]
if !ok {
outOfOrderVINMap[VIN] = 0
}
outOfOrderVINMap[VIN] += 1
outOfOrderCount += 1
if time.Since(outOfOrderTime) > outOfOrderLogDelay {
signalDict := zerolog.Dict()
for k, v := range outOfOrderSignalMap {
signalDict.Uint(k, v)
}
vinDict := zerolog.Dict()
for k, v := range outOfOrderVINMap {
vinDict.Uint(k, v)
}
logger.Warn().Dict(
"Signals", signalDict,
).Dict(
"VINs", vinDict,
).Msgf("Received Out of Order Data! %d out-of-order messages", outOfOrderCount)
outOfOrderCount = 0
outOfOrderTime = time.Now().UTC()
clear(outOfOrderSignalMap)
clear(outOfOrderVINMap)
}
}