Initial cloud-services repo - gateway service + pkg modules
This commit is contained in:
77
pkg/cache/apitokens.go
vendored
Normal file
77
pkg/cache/apitokens.go
vendored
Normal file
@@ -0,0 +1,77 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"fiskerinc.com/modules/common"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"fiskerinc.com/modules/db/queries"
|
||||
"github.com/ReneKroon/ttlcache/v2"
|
||||
)
|
||||
|
||||
const ApiKeyHeader = "Api-Key"
|
||||
|
||||
var (
|
||||
ErrInvalidToken = errors.New("invalid API token")
|
||||
ErrTokenExpired = errors.New("token is expired")
|
||||
)
|
||||
|
||||
type APITokenCache struct {
|
||||
APITokens queries.APITokensInterface
|
||||
cache *ttlcache.Cache
|
||||
onceCache sync.Once
|
||||
}
|
||||
|
||||
func (a *APITokenCache) Get(key string) (string, error) {
|
||||
value, err := a.Cache().Get(key)
|
||||
if err == nil {
|
||||
apiToken, ok := value.(*common.APIToken)
|
||||
if !ok {
|
||||
return "", ErrInvalidToken
|
||||
}
|
||||
|
||||
if apiToken.ExpiresAt != nil && apiToken.ExpiresAt.Before(time.Now()) {
|
||||
return "", ErrTokenExpired
|
||||
}
|
||||
|
||||
return apiToken.Roles, nil
|
||||
} else if !errors.Is(err, ttlcache.ErrNotFound) {
|
||||
return "", err
|
||||
}
|
||||
|
||||
item, err := a.APITokens.Get(key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if item.ExpiresAt != nil && item.ExpiresAt.Before(time.Now()) {
|
||||
return "", ErrTokenExpired
|
||||
}
|
||||
|
||||
err = a.Cache().Set(key, item)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return item.Roles, nil
|
||||
}
|
||||
|
||||
func (a *APITokenCache) Cache() *ttlcache.Cache {
|
||||
a.onceCache.Do(func() {
|
||||
if a.cache == nil {
|
||||
cache := ttlcache.NewCache()
|
||||
cache.SetTTL(10 * time.Minute)
|
||||
cache.SetCacheSizeLimit(10)
|
||||
a.cache = cache
|
||||
}
|
||||
})
|
||||
|
||||
return a.cache
|
||||
}
|
||||
|
||||
func (a *APITokenCache) Close() {
|
||||
a.cache.Close()
|
||||
a.cache = nil
|
||||
a.APITokens = nil
|
||||
}
|
||||
84
pkg/cache/apitokens_test.go
vendored
Normal file
84
pkg/cache/apitokens_test.go
vendored
Normal file
@@ -0,0 +1,84 @@
|
||||
package cache_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"fiskerinc.com/modules/adminroles"
|
||||
"fiskerinc.com/modules/cache"
|
||||
"fiskerinc.com/modules/common"
|
||||
"fiskerinc.com/modules/db"
|
||||
"fiskerinc.com/modules/db/queries"
|
||||
"fiskerinc.com/modules/redis"
|
||||
"fiskerinc.com/modules/testhelper"
|
||||
)
|
||||
|
||||
func TestIntegration(t *testing.T) {
|
||||
t.Skip()
|
||||
testKey := "YYYYYYY"
|
||||
q := queries.APITokens{}
|
||||
client := q.GetClient()
|
||||
client.GetConn().AddQueryHook(db.SQLLogger{})
|
||||
err := client.InitSchema([]interface{}{
|
||||
(*common.APIToken)(nil),
|
||||
})
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
_, err = q.Insert(common.APIToken{
|
||||
Token: testKey,
|
||||
Roles: string(adminroles.RoleCreate),
|
||||
})
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
type testCase struct {
|
||||
Name string
|
||||
Key string
|
||||
ExpectedRoles string
|
||||
ExpectedError string
|
||||
Setup func(client redis.Client, db queries.APITokensInterface) error
|
||||
Teardown func(client redis.Client, db queries.APITokensInterface) error
|
||||
}
|
||||
|
||||
tests := []testCase{
|
||||
{
|
||||
Name: "Invalid token",
|
||||
Key: "XXXXXXXX",
|
||||
ExpectedError: "token not found",
|
||||
},
|
||||
{
|
||||
Name: "From DB",
|
||||
Key: testKey,
|
||||
ExpectedRoles: string(adminroles.RoleCreate),
|
||||
},
|
||||
{
|
||||
Name: "From Cache",
|
||||
Key: testKey,
|
||||
ExpectedRoles: string(adminroles.RoleCreate),
|
||||
},
|
||||
{
|
||||
Name: "No token",
|
||||
Key: "",
|
||||
ExpectedError: "token required",
|
||||
},
|
||||
}
|
||||
|
||||
apitokens := cache.APITokenCache{
|
||||
APITokens: &q,
|
||||
}
|
||||
defer apitokens.Close()
|
||||
|
||||
for _, test := range tests {
|
||||
value, err := apitokens.Get(test.Key)
|
||||
if err != nil && err.Error() != test.ExpectedError {
|
||||
t.Errorf(testhelper.TestErrorTemplate, test.Name, test.ExpectedError, err.Error())
|
||||
}
|
||||
if value != test.ExpectedRoles {
|
||||
t.Errorf(testhelper.TestErrorTemplate, test.Name, test.ExpectedRoles, value)
|
||||
}
|
||||
}
|
||||
|
||||
q.Delete(testKey)
|
||||
}
|
||||
28
pkg/cache/car_dtcs.go
vendored
Normal file
28
pkg/cache/car_dtcs.go
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"fiskerinc.com/modules/common"
|
||||
)
|
||||
|
||||
type CarDTCsCacheInterface interface {
|
||||
Exists(dtc common.DTC_ECU) bool
|
||||
Set(dtc common.DTC_ECU)
|
||||
}
|
||||
|
||||
type CarDTCsCache struct {
|
||||
ringMap *RingMap
|
||||
}
|
||||
|
||||
func NewCarDTCsCache(capacity int) CarDTCsCacheInterface {
|
||||
return &CarDTCsCache{
|
||||
ringMap: NewRingMap(capacity),
|
||||
}
|
||||
}
|
||||
|
||||
func (carDtcCache *CarDTCsCache) Exists(dtc common.DTC_ECU) bool {
|
||||
return carDtcCache.ringMap.Exists(dtc.CacheKey(), dtc.StatusByte)
|
||||
}
|
||||
|
||||
func (carDtcCache *CarDTCsCache) Set(dtc common.DTC_ECU) {
|
||||
carDtcCache.ringMap.Put(dtc.CacheKey(), dtc.StatusByte)
|
||||
}
|
||||
31
pkg/cache/car_dtcs_test.go
vendored
Normal file
31
pkg/cache/car_dtcs_test.go
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
package cache_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
m "fiskerinc.com/modules/common"
|
||||
"fiskerinc.com/modules/cache"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var (
|
||||
dtc = m.DTC_ECU{
|
||||
VIN: "3FAFP13P71R199432",
|
||||
ECU: "ACU",
|
||||
TroubleCode: 8388881,
|
||||
}
|
||||
)
|
||||
|
||||
func TestSetAndExists(t *testing.T) {
|
||||
carDtcCache := cache.NewCarDTCsCache(1000)
|
||||
exists := carDtcCache.Exists(dtc)
|
||||
|
||||
assert.Equal(t, exists, false)
|
||||
|
||||
carDtcCache.Set(dtc)
|
||||
|
||||
exists = carDtcCache.Exists(dtc)
|
||||
|
||||
assert.Equal(t, exists, true)
|
||||
}
|
||||
4
pkg/cache/constants.go
vendored
Normal file
4
pkg/cache/constants.go
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
package cache
|
||||
|
||||
const redisObjectExpire = 600
|
||||
const redisObjectExpireDay = 86400
|
||||
192
pkg/cache/digital_twin.go
vendored
Normal file
192
pkg/cache/digital_twin.go
vendored
Normal file
@@ -0,0 +1,192 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"fiskerinc.com/modules/dbc/state"
|
||||
"fiskerinc.com/modules/logger"
|
||||
"fiskerinc.com/modules/redis"
|
||||
"fiskerinc.com/modules/utils/querystring"
|
||||
redigo "github.com/gomodule/redigo/redis"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
pattern = "car:*:state"
|
||||
)
|
||||
|
||||
type DigitalTwinTimestampState struct {
|
||||
redisClient redis.Client
|
||||
}
|
||||
|
||||
func NewDigitalTwinTimestampState(redisClient redis.Client) *DigitalTwinTimestampState {
|
||||
return &DigitalTwinTimestampState{
|
||||
redisClient: redisClient,
|
||||
}
|
||||
}
|
||||
|
||||
// getStateKeys retrieves car state keys from Redis based on the specified pattern
|
||||
// and returns a sliced list of keys according to the provided offset and limit.
|
||||
//
|
||||
// Parameters:
|
||||
// - offset: An integer indicating the starting index of the slice.
|
||||
// - limit: An integer specifying the maximum number of elements in the sliced list.
|
||||
//
|
||||
// Returns:
|
||||
// - []string: A sliced list of car state keys based on the given offset and limit.
|
||||
// - error: An error, if any, encountered during the Redis operation or slicing process.
|
||||
func (dtts *DigitalTwinTimestampState) getStateKeys(offset, limit int) ([]string, error) {
|
||||
keys, err := redigo.Strings(dtts.redisClient.Execute("KEYS", pattern))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
totalKeys := len(keys)
|
||||
if totalKeys <= offset {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if (offset + limit) > totalKeys {
|
||||
limit = totalKeys - offset
|
||||
}
|
||||
|
||||
sort.Strings(keys)
|
||||
|
||||
keys = keys[offset : offset+limit]
|
||||
return keys, nil
|
||||
}
|
||||
|
||||
// readCarStateByKey retrieves data from Redis based on the specified key using the HGETALL command.
|
||||
// It iterates over all keys and values returned by the command, sets them in a response map,
|
||||
// and returns the populated map along with any encountered errors.
|
||||
//
|
||||
// Parameters:
|
||||
// - key: A string representing the key to retrieve data from in Redis.
|
||||
//
|
||||
// Returns:
|
||||
// - map[string]interface{}: A map containing keys and values retrieved from Redis.
|
||||
// - error: An error, if any, encountered during the Redis HGETALL operation or mapping process.
|
||||
func (dtts *DigitalTwinTimestampState) readCarStateByKey(key string) (map[string]interface{}, error) {
|
||||
|
||||
keyval := make(map[string]interface{})
|
||||
batch := redis.NewRedisBatchCommands()
|
||||
|
||||
batch.Add("HGETALL", key)
|
||||
|
||||
payload, err := redigo.Values(dtts.redisClient.ExecuteBatch(batch))
|
||||
if err != nil {
|
||||
return keyval, err
|
||||
}
|
||||
|
||||
stateValues, err := redigo.Values(payload[0], nil)
|
||||
if err != nil {
|
||||
return keyval, err
|
||||
}
|
||||
|
||||
for i := 0; i < len(stateValues); i += 2 {
|
||||
key, okKey := stateValues[i].([]byte)
|
||||
value, okValue := stateValues[i+1].([]byte)
|
||||
|
||||
if !okKey || !okValue {
|
||||
continue
|
||||
}
|
||||
|
||||
err = dtts.parseCarState(string(key), value, keyval)
|
||||
// log error, do not return error so we can read other properties for digital twin
|
||||
if err != nil {
|
||||
logger.Warn().Err(err).Send()
|
||||
continue
|
||||
}
|
||||
}
|
||||
return keyval, nil
|
||||
}
|
||||
|
||||
// GetDigitalTwinSignals retrieves digital twin signals from Redis based on the specified offset and limit.
|
||||
// It reads all signals from Redis and returns a list of maps, where each map represents a cars signal with its properties.
|
||||
//
|
||||
// Parameters:
|
||||
// - offset: An integer indicating the starting index of the signals to retrieve.
|
||||
// - limit: An integer specifying the maximum number of signals to retrieve.
|
||||
//
|
||||
// Returns:
|
||||
// - []map[string]interface{}: A list of maps representing digital twin signals.
|
||||
|
||||
func (dtts *DigitalTwinTimestampState) GetDigitalTwinSignals(offset, limit int) (resp []map[string]interface{}) {
|
||||
|
||||
keys, err := dtts.getStateKeys(offset, limit)
|
||||
if err != nil {
|
||||
logger.Warn().Err(err).Send()
|
||||
return
|
||||
}
|
||||
for _, key := range keys {
|
||||
keyval, err := dtts.readCarStateByKey(key)
|
||||
if err != nil {
|
||||
logger.Warn().Err(err).Send()
|
||||
continue
|
||||
}
|
||||
if len(keyval) > 0 {
|
||||
keySlice := strings.Split(key, ":")
|
||||
keyval["VIN"] = keySlice[1]
|
||||
resp = append(resp, keyval)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// timestampKey generates a timestamp key based on the provided key by appending ":updated" to it.
|
||||
// It formats the key in a way suitable for storing timestamps associated with the original key in data storage systems.
|
||||
//
|
||||
// Parameters:
|
||||
// - key: A string representing the original key for which the timestamp key is generated.
|
||||
//
|
||||
// Returns:
|
||||
// - string: A formatted string representing the timestamp key.
|
||||
func (dtts *DigitalTwinTimestampState) timestampKey(key string) string {
|
||||
return fmt.Sprintf("%s:%s", key, "updated")
|
||||
}
|
||||
|
||||
// timestampVal parses a byte slice containing a JSON-encoded timestamp and returns a pointer to a time.Time
|
||||
// representing the parsed timestamp. It uses the UnmarshalJSON method of the time.Time type for decoding.
|
||||
//
|
||||
// Parameters:
|
||||
// - val: A byte slice containing the JSON-encoded timestamp to be parsed.
|
||||
//
|
||||
// Returns:
|
||||
// - time.Time: A time representing the parsed timestamp.
|
||||
// - error: An error, if any, encountered during the parsing process.
|
||||
func (dtts *DigitalTwinTimestampState) timestampVal(val []byte) (time.Time, error) {
|
||||
t := &time.Time{}
|
||||
err := t.UnmarshalJSON(val)
|
||||
return *t, err
|
||||
}
|
||||
|
||||
// parseCarState checks if the provided key is needed and, if so, sets the key and value in the given map.
|
||||
//
|
||||
// Parameters:
|
||||
// - key: A string representing the key to check and potentially set in the map.
|
||||
// - value: A byte slice containing the value associated with the key.
|
||||
// - keyval: A map[string]interface{} where the key and valueset if the key is needed.
|
||||
//
|
||||
// Returns:
|
||||
// - error: An error, if any, encountered during the parsing and mapping process.
|
||||
func (dtts *DigitalTwinTimestampState) parseCarState(key string, value []byte, keyval map[string]interface{}) error {
|
||||
var err error
|
||||
val := string(value)
|
||||
switch key {
|
||||
case state.BMS_PwrBattRmngCpSOC, state.BMS_RmChrgTi_FullChrg, state.BCM_PwrMod, state.PWC_ChrgSts, state.VCU_DCChrgRmngTi, state.BMS_RmChrgTi_TrgtSoC:
|
||||
keyval[key], err = strconv.Atoi(val)
|
||||
case state.ICC_TotMilg_ODO:
|
||||
keyval[key], err = querystring.ConvertStringToInt(val)
|
||||
case state.IBS_BatteryVoltage:
|
||||
keyval[key], err = strconv.ParseFloat(val, 64)
|
||||
|
||||
// updated timestamps
|
||||
case dtts.timestampKey(state.BMS_PwrBattRmngCpSOC), dtts.timestampKey(state.ICC_TotMilg_ODO), dtts.timestampKey(state.VCU_DCChrgRmngTi), dtts.timestampKey(state.BMS_RmChrgTi_TrgtSoC), dtts.timestampKey(state.IBS_BatteryVoltage),
|
||||
dtts.timestampKey(state.BMS_RmChrgTi_FullChrg), dtts.timestampKey(state.BCM_PwrMod), dtts.timestampKey(state.PWC_ChrgSts):
|
||||
keyval[key], err = dtts.timestampVal(value)
|
||||
}
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
141
pkg/cache/drivers.go
vendored
Normal file
141
pkg/cache/drivers.go
vendored
Normal file
@@ -0,0 +1,141 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"fiskerinc.com/modules/common"
|
||||
"fiskerinc.com/modules/db/queries"
|
||||
"fiskerinc.com/modules/logger"
|
||||
"fiskerinc.com/modules/redis"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func NewDriversCache(redisClient redis.ClientPoolInterface, cars queries.CarsInterface) *DriversCache {
|
||||
return &DriversCache{
|
||||
redisClientPool: redisClient,
|
||||
cars: cars,
|
||||
}
|
||||
}
|
||||
|
||||
type DriversCache struct {
|
||||
redisClientPool redis.ClientPoolInterface
|
||||
cars queries.CarsInterface
|
||||
}
|
||||
|
||||
func (dc *DriversCache) RedisClientPool() redis.ClientPoolInterface {
|
||||
return dc.redisClientPool
|
||||
}
|
||||
|
||||
func (dc *DriversCache) Cars() queries.CarsInterface {
|
||||
return dc.cars
|
||||
}
|
||||
|
||||
func (dc *DriversCache) hasCachedNoDrivers(drivers []string) bool {
|
||||
// Redis will return []string{""} for no drivers
|
||||
return len(drivers) == 1 && len(drivers[0]) == 0
|
||||
}
|
||||
|
||||
func (dc *DriversCache) cacheDrivers(key string, drivers []string) error {
|
||||
client := dc.redisClientPool.GetFromPool()
|
||||
defer client.Close()
|
||||
// cache driver IDs
|
||||
if len(drivers) > 0 {
|
||||
return client.NewSet(key, drivers, redisObjectExpire)
|
||||
}
|
||||
|
||||
// Redis will not take an empty array as an arg
|
||||
return client.NewSet(key, nil, redisObjectExpire)
|
||||
}
|
||||
|
||||
// RetrieveDriverIDs retrieves IDs from redis or from DB and proceeds to cache both the drivers and IDs
|
||||
// redis keys:
|
||||
//
|
||||
// car:<VIN>:drivers
|
||||
func (dc *DriversCache) RetrieveDriverIDs(vin string) ([]string, error) {
|
||||
var driverIDs []string
|
||||
driverIDsKey := redis.CarToAllDriversKey(vin)
|
||||
|
||||
// retrieve IDs from redis
|
||||
client := dc.redisClientPool.GetFromPool()
|
||||
err := client.GetSet(driverIDsKey, &driverIDs)
|
||||
if err != nil {
|
||||
logger.Warn().Err(err).Send()
|
||||
}
|
||||
client.Close()
|
||||
|
||||
if dc.hasCachedNoDrivers(driverIDs) {
|
||||
return []string{}, nil
|
||||
}
|
||||
if len(driverIDs) > 0 {
|
||||
return driverIDs, nil
|
||||
}
|
||||
|
||||
// if IDs not present in redis perform DB lookup
|
||||
var drivers []common.CarToDriver
|
||||
drivers, err = dc.cars.GetDrivers(vin)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, driver := range drivers {
|
||||
driverIDs = append(driverIDs, driver.DriverID)
|
||||
}
|
||||
|
||||
err = dc.cacheDrivers(driverIDsKey, driverIDs)
|
||||
if err != nil {
|
||||
return driverIDs, err
|
||||
}
|
||||
|
||||
return driverIDs, nil
|
||||
}
|
||||
|
||||
// RetrieveDriverIDsAsSet retrieves IDs from redis or from DB and proceeds to cache both the drivers and IDs
|
||||
// redis keys:
|
||||
//
|
||||
// car:<VIN>:drivers
|
||||
func (dc *DriversCache) RetrieveDriverIDsAsSet(vin string) (map[string]struct{}, error) {
|
||||
driverIDs, err := dc.RetrieveDriverIDs(vin)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var dIDsSet = make(map[string]struct{})
|
||||
for _, did := range driverIDs {
|
||||
dIDsSet[did] = struct{}{}
|
||||
}
|
||||
|
||||
return dIDsSet, nil
|
||||
}
|
||||
|
||||
func (dc *DriversCache) IsDriverOfVIN(vin string, driverid string) (bool, error) {
|
||||
ids, err := dc.RetrieveDriverIDs(vin)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
for _, id := range ids {
|
||||
if id == driverid {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
return false, dc.NotDriverError(vin, driverid)
|
||||
}
|
||||
|
||||
// Add driver to database and cache
|
||||
func (dc *DriversCache) AddDriver(car *common.Car, driver *common.Driver, role string) (*common.CarToDriver, error) {
|
||||
|
||||
relation, err := dc.cars.AddDriver(car, driver, role)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
driverIDsKey := redis.CarToAllDriversKey(car.VIN)
|
||||
|
||||
client := dc.redisClientPool.GetFromPool()
|
||||
defer client.Close()
|
||||
client.AddToSet(driverIDsKey, driver.ID, redisObjectExpire)
|
||||
return relation, nil
|
||||
}
|
||||
|
||||
func (dc DriversCache) NotDriverError(vin string, driverid string) error {
|
||||
return errors.Errorf("id %s is not a driver for vin %v", driverid, vin)
|
||||
}
|
||||
107
pkg/cache/drivers_test.go
vendored
Normal file
107
pkg/cache/drivers_test.go
vendored
Normal file
@@ -0,0 +1,107 @@
|
||||
package cache_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"fiskerinc.com/modules/cache"
|
||||
"fiskerinc.com/modules/db/queries"
|
||||
"fiskerinc.com/modules/db/queries/mocks"
|
||||
"fiskerinc.com/modules/redis"
|
||||
"fiskerinc.com/modules/redis/tester"
|
||||
"fiskerinc.com/modules/testhelper"
|
||||
)
|
||||
|
||||
var mockRedis redis.Client
|
||||
var mockDB queries.CarsInterface
|
||||
|
||||
func setupRedisMock() {
|
||||
redis.MockRedisConnection()
|
||||
}
|
||||
|
||||
func setupDBMock() {
|
||||
mockDB = &mocks.MockCars{}
|
||||
}
|
||||
|
||||
type mockRedisCache struct {
|
||||
redis.Connection
|
||||
}
|
||||
|
||||
func (c *mockRedisCache) GetSet(id string, data interface{}) error {
|
||||
drivers := []string{"valid-id-1", "valid-id-2", "valid-id-3"}
|
||||
|
||||
dataBytes, err := json.Marshal(drivers)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = json.Unmarshal(dataBytes, data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type mockRedisEmptyCache struct {
|
||||
redis.Connection
|
||||
}
|
||||
|
||||
func (c *mockRedisEmptyCache) GetSet(id string, data interface{}) error {
|
||||
drivers := []string{}
|
||||
|
||||
dataBytes, err := json.Marshal(drivers)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = json.Unmarshal(dataBytes, data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *mockRedisEmptyCache) SetObjects(id []string, data []interface{}, expire int) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestRetrieveAndCacheDriverIDs(t *testing.T) {
|
||||
setupRedisMock()
|
||||
setupDBMock()
|
||||
mockRedis = &mockRedisCache{}
|
||||
redisPool := tester.NewMockClientPool(mockRedis)
|
||||
drivers := cache.NewDriversCache(redisPool, mockDB)
|
||||
|
||||
_, err := drivers.RetrieveDriverIDs("FISKER123")
|
||||
if err != nil {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "TestRetrieveAndCacheDriverIDs", "no error", err)
|
||||
}
|
||||
|
||||
mockRedis = &mockRedisEmptyCache{}
|
||||
_, err = drivers.RetrieveDriverIDs("FISKER456")
|
||||
if err != nil {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "TestRetrieveAndCacheDriverIDs", "no error", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRetrieveAndCacheDriverIDsAsSet(t *testing.T) {
|
||||
setupRedisMock()
|
||||
setupDBMock()
|
||||
mockRedis = &mockRedisCache{}
|
||||
redisPool := tester.NewMockClientPool(mockRedis)
|
||||
|
||||
drivers := cache.NewDriversCache(redisPool, mockDB)
|
||||
|
||||
_, err := drivers.RetrieveDriverIDs("FISKER123")
|
||||
if err != nil {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "TestRetrieveAndCacheDriverIDs", "no error", err)
|
||||
}
|
||||
|
||||
mockRedis = &mockRedisEmptyCache{}
|
||||
_, err = drivers.RetrieveDriverIDsAsSet("FISKER456")
|
||||
if err != nil {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "TestRetrieveAndCacheDriverIDs", "no error", err)
|
||||
}
|
||||
}
|
||||
11
pkg/cache/errors.go
vendored
Normal file
11
pkg/cache/errors.go
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
package cache
|
||||
|
||||
import "github.com/pkg/errors"
|
||||
|
||||
func ErrInvalidCarToDriverAssociation(vin string, driverID string) error {
|
||||
return errors.Errorf("no relationship found between vin %s and driver %s", vin, driverID)
|
||||
}
|
||||
|
||||
func ErrCarHasNoDrivers(vin string) error {
|
||||
return errors.Errorf("car %s has no drivers", vin)
|
||||
}
|
||||
147
pkg/cache/fileids.go
vendored
Normal file
147
pkg/cache/fileids.go
vendored
Normal file
@@ -0,0 +1,147 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"fiskerinc.com/modules/common"
|
||||
"fiskerinc.com/modules/db/queries"
|
||||
"fiskerinc.com/modules/logger"
|
||||
"fiskerinc.com/modules/redis"
|
||||
|
||||
r "github.com/gomodule/redigo/redis"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func RetrieveFileEncryptionParams(client redis.Client, db queries.FileKeysInterface, fileids []string) ([]common.FileKeyResponse, error) {
|
||||
result, err := retrieveFileEncryptionParamsRedis(client, fileids)
|
||||
if err != nil && !errors.Is(err, redis.ErrNilObject) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dbFileIDs := missingFileIDs(result, fileids)
|
||||
if len(dbFileIDs) > 0 {
|
||||
rows, err := retrieveFileEncryptionParamsDB(db, dbFileIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = cacheFileEncryptionParamsRedis(client, rows, redisObjectExpireDay)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result = append(rows, result...)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func retrieveFileEncryptionParamsRedis(client redis.Client, fileids []string) ([]common.FileKeyResponse, error) {
|
||||
keys := make([]string, len(fileids))
|
||||
|
||||
for i, fileid := range fileids {
|
||||
keys[i] = redis.FileIDEncryptionParamsKey(fileid)
|
||||
}
|
||||
|
||||
if len(keys) == 0 {
|
||||
return []common.FileKeyResponse{}, nil
|
||||
}
|
||||
|
||||
values, err := client.GetMulti(keys)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result, err := getFileKeyResponses(values)
|
||||
return result, err
|
||||
}
|
||||
|
||||
func getFileKeyResponses(values []interface{}) ([]common.FileKeyResponse, error) {
|
||||
result := []common.FileKeyResponse{}
|
||||
|
||||
for _, value := range values {
|
||||
if value == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
file := common.FileKeyResponse{}
|
||||
data, err := r.Bytes(value, nil)
|
||||
if err != nil {
|
||||
file.Error = err.Error()
|
||||
return result, errors.WithStack(err)
|
||||
}
|
||||
|
||||
err = json.Unmarshal(data, &file)
|
||||
if err != nil {
|
||||
return result, errors.WithStack(err)
|
||||
}
|
||||
|
||||
result = append(result, file)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func missingFileIDs(items []common.FileKeyResponse, fileids []string) []string {
|
||||
result := []string{}
|
||||
hash := map[string]bool{}
|
||||
|
||||
for _, item := range items {
|
||||
if item.FileID != "" {
|
||||
hash[item.FileID] = true
|
||||
}
|
||||
}
|
||||
|
||||
for _, fileid := range fileids {
|
||||
if !hash[fileid] {
|
||||
result = append(result, fileid)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func retrieveFileEncryptionParamsDB(db queries.FileKeysInterface, fileids []string) ([]common.FileKeyResponse, error) {
|
||||
result := make([]common.FileKeyResponse, len(fileids))
|
||||
hash := map[string]bool{}
|
||||
data, err := db.GetMulti(fileids)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
for i, file := range data {
|
||||
result[i].Apply(&file)
|
||||
hash[file.FileID] = true
|
||||
}
|
||||
|
||||
if len(fileids) != len(data) {
|
||||
starting := len(data)
|
||||
current := 0
|
||||
for _, fileid := range fileids {
|
||||
if !hash[fileid] {
|
||||
logger.Warn().Msgf("file id %s not found", fileid)
|
||||
file := &result[starting+current]
|
||||
file.FileID = fileid
|
||||
file.Error = "not found"
|
||||
current++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func cacheFileEncryptionParamsRedis(client redis.Client, files []common.FileKeyResponse, expire int) error {
|
||||
batch := redis.NewRedisBatchCommands()
|
||||
|
||||
for _, file := range files {
|
||||
serialized, err := json.Marshal(file)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
id := redis.FileIDEncryptionParamsKey(file.FileID)
|
||||
batch.Add("SET", id, serialized, "EX", expire)
|
||||
}
|
||||
|
||||
_, err := client.ExecuteBatch(batch)
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
74
pkg/cache/fileids_test.go
vendored
Normal file
74
pkg/cache/fileids_test.go
vendored
Normal file
@@ -0,0 +1,74 @@
|
||||
package cache_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"fiskerinc.com/modules/cache"
|
||||
"fiskerinc.com/modules/common"
|
||||
"fiskerinc.com/modules/db"
|
||||
"fiskerinc.com/modules/db/queries"
|
||||
"fiskerinc.com/modules/db/queries/mocks"
|
||||
"fiskerinc.com/modules/redis"
|
||||
)
|
||||
|
||||
func TestRetrieveFileEncryptionParams(t *testing.T) {
|
||||
key := "b7f74938c9402dc2"
|
||||
c := NewMockRedisConn()
|
||||
q := mocks.MockFileKeys{
|
||||
GetResponse: &common.FileKey{},
|
||||
}
|
||||
|
||||
_, err := cache.RetrieveFileEncryptionParams(c, &q, []string{key})
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkRetrieveFileEncryptionParams(b *testing.B) {
|
||||
c := redis.NewClient()
|
||||
q := queries.FileKeys{}
|
||||
|
||||
for n := 0; n < b.N; n++ {
|
||||
_, err := cache.RetrieveFileEncryptionParams(c, &q, []string{"b7f74938c9402dc2"})
|
||||
if err != nil {
|
||||
b.Error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func NewMockRedisConn() *MockRedisConn {
|
||||
mock := &MockRedisConn{}
|
||||
mock.SetConn(redis.GetMockPool().Get())
|
||||
return mock
|
||||
}
|
||||
|
||||
type MockRedisConn struct {
|
||||
redis.Connection
|
||||
}
|
||||
|
||||
func (m *MockRedisConn) GetCache(id string, dest interface{}, expire int) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockRedisConn) SetCache(id string, data interface{}, expire int) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockRedisConn) GetValuesMulti(ids []string, data interface{}) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockRedisConn) SetMulti(ids []string, data []interface{}) error {
|
||||
return nil
|
||||
}
|
||||
func TestRetrieveFileEncryptionParamsIntegration(t *testing.T) {
|
||||
t.Skip()
|
||||
c := redis.NewClient()
|
||||
q := queries.FileKeys{}
|
||||
q.GetClient().GetConn().AddQueryHook(db.SQLLogger{})
|
||||
|
||||
_, err := cache.RetrieveFileEncryptionParams(c, &q, []string{"bd2c7a6cc94042cb", "83165a80c940e8b3"})
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
22
pkg/cache/filters.go
vendored
Normal file
22
pkg/cache/filters.go
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"fiskerinc.com/modules/common"
|
||||
"fiskerinc.com/modules/redis"
|
||||
)
|
||||
|
||||
func FillCarFilterOnline(redisCLI redis.Client, filter *common.CarSearch) error {
|
||||
if filter.Online == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var onlineVehicles []string
|
||||
err := redisCLI.GetSet(redis.CarSessionsKey(), &onlineVehicles)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
filter.Online.VINsOnline = onlineVehicles
|
||||
|
||||
return nil
|
||||
}
|
||||
67
pkg/cache/filters_test.go
vendored
Normal file
67
pkg/cache/filters_test.go
vendored
Normal file
@@ -0,0 +1,67 @@
|
||||
package cache_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"fiskerinc.com/modules/cache"
|
||||
m "fiskerinc.com/modules/common"
|
||||
"fiskerinc.com/modules/redis/tester"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var someErr = errors.New("some error")
|
||||
|
||||
func TestFillCarFilterOnline(t *testing.T) {
|
||||
val_false := false
|
||||
tests := map[string]struct {
|
||||
redisCLI *tester.MockRedis
|
||||
filter m.CarSearch
|
||||
expErr error
|
||||
expFilter m.CarSearch
|
||||
}{
|
||||
"correct_common": {
|
||||
redisCLI: &tester.MockRedis{
|
||||
GetSetResults: `["FISKERVIN1","FISKERVIN2"]`,
|
||||
},
|
||||
filter: m.CarSearch{
|
||||
Online: &m.CarOnlineFilter{
|
||||
Online: &val_false,
|
||||
},
|
||||
},
|
||||
expFilter: m.CarSearch{
|
||||
Online: &m.CarOnlineFilter{
|
||||
Online: &val_false,
|
||||
VINsOnline: []string{"FISKERVIN1", "FISKERVIN2"},
|
||||
},
|
||||
},
|
||||
},
|
||||
"filter_nil": {
|
||||
filter: m.CarSearch{},
|
||||
expFilter: m.CarSearch{},
|
||||
},
|
||||
"redis_err": {
|
||||
redisCLI: &tester.MockRedis{
|
||||
Error: someErr,
|
||||
},
|
||||
filter: m.CarSearch{
|
||||
Online: &m.CarOnlineFilter{
|
||||
Online: &val_false,
|
||||
},
|
||||
},
|
||||
expErr: someErr,
|
||||
},
|
||||
}
|
||||
for name, tt := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
err := cache.FillCarFilterOnline(tt.redisCLI, &tt.filter)
|
||||
if err != nil && tt.expErr != nil {
|
||||
assert.Equal(t, tt.expErr.Error(), err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
assert.Equal(t, tt.expErr, err)
|
||||
assert.Equal(t, tt.filter, tt.expFilter)
|
||||
})
|
||||
}
|
||||
}
|
||||
161
pkg/cache/ringmap.go
vendored
Normal file
161
pkg/cache/ringmap.go
vendored
Normal file
@@ -0,0 +1,161 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/elliotchance/orderedmap/v2"
|
||||
)
|
||||
|
||||
type RingMapInterface interface {
|
||||
Exists(key string, value interface{}) bool
|
||||
}
|
||||
|
||||
// Copied from https://github.com/prgsmall/ringmap to use orderedmap v2
|
||||
|
||||
type RingMap struct {
|
||||
orderedMap *orderedmap.OrderedMap[string, interface{}]
|
||||
capacity int
|
||||
writeLock sync.RWMutex
|
||||
}
|
||||
|
||||
func NewRingMap(capacity int) *RingMap {
|
||||
return &RingMap{
|
||||
orderedMap: orderedmap.NewOrderedMap[string, interface{}](),
|
||||
capacity: capacity,
|
||||
}
|
||||
}
|
||||
|
||||
// Convenience function to check if key and value already exists
|
||||
// If key and value does not exists, it is added
|
||||
// If key exists with different value, it is replaced with new value
|
||||
func (m *RingMap) Exists(key string, value interface{}) bool {
|
||||
m.writeLock.RLock()
|
||||
el := m.orderedMap.GetElement(key)
|
||||
m.writeLock.RUnlock()
|
||||
exists := el != nil
|
||||
|
||||
if exists && el.Value != value {
|
||||
m.Delete(key)
|
||||
exists = false
|
||||
} else {
|
||||
m.clearLast()
|
||||
}
|
||||
m.writeLock.Lock()
|
||||
defer m.writeLock.Unlock()
|
||||
m.orderedMap.Set(key, value)
|
||||
|
||||
return exists
|
||||
}
|
||||
|
||||
// Get returns the value for a key. If the key does not exist, the second return
|
||||
// parameter will be false and the value will be nil.
|
||||
func (m *RingMap) Get(key string) (interface{}, bool) {
|
||||
m.writeLock.RLock()
|
||||
defer m.writeLock.RUnlock()
|
||||
return m.orderedMap.Get(key)
|
||||
}
|
||||
|
||||
// Set will set (or replace) a value for a key. If the key was new, then true
|
||||
// will be returned. The returned value will be false if the value was replaced
|
||||
// (even if the value was the same). If a new key is being added and the map is
|
||||
// full, then the front element will be deleted to make room for the new element.
|
||||
func (m *RingMap) Set(key string, value interface{}) bool {
|
||||
_, didExist := m.Get(key)
|
||||
|
||||
if !didExist {
|
||||
m.clearLast()
|
||||
}
|
||||
m.writeLock.Lock()
|
||||
defer m.writeLock.Unlock()
|
||||
m.orderedMap.Set(key, value)
|
||||
|
||||
return !didExist
|
||||
}
|
||||
|
||||
// Put will set a value for a key. If the key already exists, it will be deleted
|
||||
// from and a recreated at the end of the list. If the key was new, then true
|
||||
// will be returned. The returned value will be false if the value was replaced
|
||||
// (even if the value was the same). If a new key is being added and the map is
|
||||
// full, then the front element will be deleted to make room for the new element.
|
||||
func (m *RingMap) Put(key string, value interface{}) bool {
|
||||
_, didExist := m.Get(key)
|
||||
|
||||
if didExist {
|
||||
m.Delete(key)
|
||||
} else {
|
||||
m.clearLast()
|
||||
}
|
||||
m.writeLock.Lock()
|
||||
defer m.writeLock.Unlock()
|
||||
m.orderedMap.Set(key, value)
|
||||
|
||||
return !didExist
|
||||
}
|
||||
|
||||
// GetOrDefault returns the value for a key. If the key does not exist, returns
|
||||
// the default value instead.
|
||||
func (m *RingMap) GetOrDefault(key string, defaultValue interface{}) interface{} {
|
||||
m.writeLock.RLock()
|
||||
defer m.writeLock.RUnlock()
|
||||
return m.orderedMap.GetOrDefault(key, defaultValue)
|
||||
}
|
||||
|
||||
// Len returns the number of elements in the map.
|
||||
func (m *RingMap) Len() int {
|
||||
m.writeLock.RLock()
|
||||
defer m.writeLock.RUnlock()
|
||||
return m.orderedMap.Len()
|
||||
}
|
||||
|
||||
// Capacity returns the capacity of the map
|
||||
func (m *RingMap) Capacity() int {
|
||||
m.writeLock.RLock()
|
||||
defer m.writeLock.RUnlock()
|
||||
return m.capacity
|
||||
}
|
||||
|
||||
// IsFull returns true if the number of elements in the map is Capacity()
|
||||
func (m *RingMap) IsFull() bool {
|
||||
m.writeLock.RLock()
|
||||
defer m.writeLock.RUnlock()
|
||||
return m.orderedMap.Len() == m.capacity
|
||||
}
|
||||
|
||||
// Keys returns all of the keys in the order they were inserted. If a key was
|
||||
// replaced it will retain the same position. To ensure most recently set keys
|
||||
// are always at the end you must always Delete before Set.
|
||||
func (m *RingMap) Keys() (keys []string) {
|
||||
m.writeLock.RLock()
|
||||
defer m.writeLock.RUnlock()
|
||||
return m.orderedMap.Keys()
|
||||
}
|
||||
|
||||
// Delete will remove a key from the map. It will return true if the key was
|
||||
// removed (the key did exist).
|
||||
func (m *RingMap) Delete(key string) (didDelete bool) {
|
||||
m.writeLock.Lock()
|
||||
defer m.writeLock.Unlock()
|
||||
return m.orderedMap.Delete(key)
|
||||
}
|
||||
|
||||
// Front will return the element that is the first (oldest Set element). If
|
||||
// there are no elements this will return nil.
|
||||
func (m *RingMap) Front() *orderedmap.Element[string, interface{}] {
|
||||
m.writeLock.RLock()
|
||||
defer m.writeLock.RUnlock()
|
||||
return m.orderedMap.Front()
|
||||
}
|
||||
|
||||
// Back will return the element that is the last (most recent Set element). If
|
||||
// there are no elements this will return nil.
|
||||
func (m *RingMap) Back() *orderedmap.Element[string, interface{}] {
|
||||
m.writeLock.RLock()
|
||||
defer m.writeLock.RUnlock()
|
||||
return m.orderedMap.Back()
|
||||
}
|
||||
|
||||
func (m *RingMap) clearLast() {
|
||||
if m.IsFull() {
|
||||
m.Delete(m.Front().Key)
|
||||
}
|
||||
}
|
||||
913
pkg/cache/ringmap_test.go
vendored
Normal file
913
pkg/cache/ringmap_test.go
vendored
Normal file
@@ -0,0 +1,913 @@
|
||||
package cache_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"fiskerinc.com/modules/cache"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var ringMapCapacity = 777
|
||||
|
||||
func TestObjectCreation(t *testing.T) {
|
||||
|
||||
t.Run("TestNewRingMap", func(t *testing.T) {
|
||||
m := cache.NewRingMap(ringMapCapacity)
|
||||
assert.IsType(t, &cache.RingMap{}, m)
|
||||
assert.Equal(t, ringMapCapacity, m.Capacity())
|
||||
assert.EqualValues(t, false, m.IsFull())
|
||||
})
|
||||
}
|
||||
|
||||
func TestGet(t *testing.T) {
|
||||
t.Run("ReturnsNotOKIfStringKeyDoesntExist", func(t *testing.T) {
|
||||
m := cache.NewRingMap(ringMapCapacity)
|
||||
_, ok := m.Get("foo")
|
||||
assert.False(t, ok)
|
||||
})
|
||||
|
||||
t.Run("ReturnsNotOKIfNonStringKeyDoesntExist", func(t *testing.T) {
|
||||
m := cache.NewRingMap(ringMapCapacity)
|
||||
_, ok := m.Get("123")
|
||||
assert.False(t, ok)
|
||||
})
|
||||
|
||||
t.Run("ReturnsOKIfKeyExists", func(t *testing.T) {
|
||||
m := cache.NewRingMap(ringMapCapacity)
|
||||
m.Set("foo", "bar")
|
||||
_, ok := m.Get("foo")
|
||||
assert.True(t, ok)
|
||||
})
|
||||
|
||||
t.Run("ReturnsValueForKey", func(t *testing.T) {
|
||||
m := cache.NewRingMap(ringMapCapacity)
|
||||
m.Set("foo", "bar")
|
||||
value, _ := m.Get("foo")
|
||||
assert.Equal(t, "bar", value)
|
||||
})
|
||||
|
||||
t.Run("ReturnsDynamicValueForKey", func(t *testing.T) {
|
||||
m := cache.NewRingMap(ringMapCapacity)
|
||||
m.Set("foo", "baz")
|
||||
value, _ := m.Get("foo")
|
||||
assert.Equal(t, "baz", value)
|
||||
})
|
||||
|
||||
t.Run("KeyDoesntExistOnNonEmptyMap", func(t *testing.T) {
|
||||
m := cache.NewRingMap(ringMapCapacity)
|
||||
m.Set("foo", "baz")
|
||||
_, ok := m.Get("bar")
|
||||
assert.False(t, ok)
|
||||
})
|
||||
|
||||
t.Run("ValueForKeyDoesntExistOnNonEmptyMap", func(t *testing.T) {
|
||||
m := cache.NewRingMap(ringMapCapacity)
|
||||
m.Set("foo", "baz")
|
||||
value, _ := m.Get("bar")
|
||||
assert.Nil(t, value)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPut(t *testing.T) {
|
||||
t.Run("ReturnsTrueIfStringKeyIsNew", func(t *testing.T) {
|
||||
m := cache.NewRingMap(ringMapCapacity)
|
||||
ok := m.Put("foo", "bar")
|
||||
assert.True(t, ok)
|
||||
})
|
||||
|
||||
t.Run("ReturnsTrueIfNonStringKeyIsNew", func(t *testing.T) {
|
||||
m := cache.NewRingMap(ringMapCapacity)
|
||||
ok := m.Put("123", "bar")
|
||||
assert.True(t, ok)
|
||||
})
|
||||
|
||||
t.Run("ValueCanBeNonString", func(t *testing.T) {
|
||||
m := cache.NewRingMap(ringMapCapacity)
|
||||
ok := m.Put("123", true)
|
||||
assert.True(t, ok)
|
||||
})
|
||||
|
||||
t.Run("ReturnsFalseIfKeyIsNotNew", func(t *testing.T) {
|
||||
m := cache.NewRingMap(ringMapCapacity)
|
||||
m.Put("foo", "bar")
|
||||
ok := m.Put("foo", "bar")
|
||||
assert.False(t, ok)
|
||||
})
|
||||
|
||||
t.Run("PutMultipleKeys", func(t *testing.T) {
|
||||
m := cache.NewRingMap(ringMapCapacity)
|
||||
m.Put("foo", "bar")
|
||||
m.Put("baz", "qux")
|
||||
m.Put("mik", "qux")
|
||||
ok := m.Put("quux", "corge")
|
||||
assert.True(t, ok)
|
||||
})
|
||||
|
||||
t.Run("PutMultipleDifferentKeysWithReplace", func(t *testing.T) {
|
||||
m := cache.NewRingMap(ringMapCapacity)
|
||||
m.Put("foo", "bar")
|
||||
m.Put("baz", "baz")
|
||||
m.Put("mik", "mik")
|
||||
ok := m.Put("foo", "corge")
|
||||
assert.False(t, ok)
|
||||
assert.Equal(t, "baz", m.Front().Key)
|
||||
assert.Equal(t, "foo", m.Back().Key)
|
||||
})
|
||||
|
||||
t.Run("PutMultipleDifferentKeysWithReplace", func(t *testing.T) {
|
||||
m := cache.NewRingMap(3)
|
||||
m.Put("ace", "bev")
|
||||
m.Put("foo", "bar")
|
||||
m.Put("baz", "baz")
|
||||
m.Put("mik", "mik")
|
||||
ok := m.Put("foo", "corge")
|
||||
assert.False(t, ok)
|
||||
assert.Equal(t, "baz", m.Front().Key)
|
||||
assert.Equal(t, "foo", m.Back().Key)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSet(t *testing.T) {
|
||||
t.Run("ReturnsTrueIfStringKeyIsNew", func(t *testing.T) {
|
||||
m := cache.NewRingMap(ringMapCapacity)
|
||||
ok := m.Set("foo", "bar")
|
||||
assert.True(t, ok)
|
||||
})
|
||||
|
||||
t.Run("ReturnsTrueIfNonStringKeyIsNew", func(t *testing.T) {
|
||||
m := cache.NewRingMap(ringMapCapacity)
|
||||
ok := m.Set("123", "bar")
|
||||
assert.True(t, ok)
|
||||
})
|
||||
|
||||
t.Run("ValueCanBeNonString", func(t *testing.T) {
|
||||
m := cache.NewRingMap(ringMapCapacity)
|
||||
ok := m.Set("123", true)
|
||||
assert.True(t, ok)
|
||||
})
|
||||
|
||||
t.Run("ReturnsFalseIfKeyIsNotNew", func(t *testing.T) {
|
||||
m := cache.NewRingMap(ringMapCapacity)
|
||||
m.Set("foo", "bar")
|
||||
ok := m.Set("foo", "bar")
|
||||
assert.False(t, ok)
|
||||
})
|
||||
|
||||
t.Run("SetThreeDifferentKeys", func(t *testing.T) {
|
||||
m := cache.NewRingMap(ringMapCapacity)
|
||||
m.Set("foo", "bar")
|
||||
m.Set("baz", "qux")
|
||||
ok := m.Set("quux", "corge")
|
||||
assert.True(t, ok)
|
||||
})
|
||||
}
|
||||
|
||||
func TestLen(t *testing.T) {
|
||||
t.Run("EmptyMapIsZeroLen", func(t *testing.T) {
|
||||
m := cache.NewRingMap(ringMapCapacity)
|
||||
assert.Equal(t, 0, m.Len())
|
||||
})
|
||||
|
||||
t.Run("SingleElementIsLenOne", func(t *testing.T) {
|
||||
m := cache.NewRingMap(ringMapCapacity)
|
||||
m.Set("123", true)
|
||||
assert.Equal(t, 1, m.Len())
|
||||
})
|
||||
|
||||
t.Run("ThreeElements", func(t *testing.T) {
|
||||
m := cache.NewRingMap(ringMapCapacity)
|
||||
m.Set("1", true)
|
||||
m.Set("2", true)
|
||||
m.Set("3", true)
|
||||
assert.Equal(t, 3, m.Len())
|
||||
})
|
||||
|
||||
t.Run("ThreeElementsWithMax", func(t *testing.T) {
|
||||
m := cache.NewRingMap(3)
|
||||
assert.Equal(t, false, m.IsFull())
|
||||
m.Set("1", true)
|
||||
assert.Equal(t, false, m.IsFull())
|
||||
m.Set("2", true)
|
||||
assert.Equal(t, false, m.IsFull())
|
||||
m.Set("3", true)
|
||||
assert.Equal(t, 3, m.Len())
|
||||
assert.Equal(t, true, m.IsFull())
|
||||
assert.Equal(t, m.Front().Key, "1")
|
||||
|
||||
m.Set("4", true)
|
||||
assert.Equal(t, 3, m.Len())
|
||||
assert.Equal(t, true, m.IsFull())
|
||||
assert.Equal(t, m.Front().Key, "2")
|
||||
assert.Equal(t, m.Back().Key, "4")
|
||||
})
|
||||
}
|
||||
|
||||
func TestKeys(t *testing.T) {
|
||||
t.Run("EmptyMap", func(t *testing.T) {
|
||||
m := cache.NewRingMap(ringMapCapacity)
|
||||
assert.Empty(t, m.Keys())
|
||||
})
|
||||
|
||||
t.Run("OneElement", func(t *testing.T) {
|
||||
m := cache.NewRingMap(ringMapCapacity)
|
||||
m.Set("1", true)
|
||||
assert.Equal(t, []string{"1"}, m.Keys())
|
||||
})
|
||||
|
||||
t.Run("RetainsOrder", func(t *testing.T) {
|
||||
m := cache.NewRingMap(ringMapCapacity)
|
||||
for i := 1; i < 10; i++ {
|
||||
m.Set(strconv.Itoa(i), true)
|
||||
}
|
||||
assert.Equal(t,
|
||||
[]string{"1", "2", "3", "4", "5", "6", "7", "8", "9"},
|
||||
m.Keys())
|
||||
})
|
||||
|
||||
t.Run("ReplacingKeyDoesntChangeOrder", func(t *testing.T) {
|
||||
m := cache.NewRingMap(ringMapCapacity)
|
||||
m.Set("foo", true)
|
||||
m.Set("bar", true)
|
||||
m.Set("foo", false)
|
||||
assert.Equal(t,
|
||||
[]string{"foo", "bar"},
|
||||
m.Keys())
|
||||
})
|
||||
|
||||
t.Run("KeysAfterDelete", func(t *testing.T) {
|
||||
m := cache.NewRingMap(ringMapCapacity)
|
||||
m.Set("foo", true)
|
||||
m.Set("bar", true)
|
||||
m.Delete("foo")
|
||||
assert.Equal(t, []string{"bar"}, m.Keys())
|
||||
})
|
||||
}
|
||||
|
||||
func TestDelete(t *testing.T) {
|
||||
t.Run("KeyDoesntExistReturnsFalse", func(t *testing.T) {
|
||||
m := cache.NewRingMap(ringMapCapacity)
|
||||
assert.False(t, m.Delete("foo"))
|
||||
})
|
||||
|
||||
t.Run("KeyDoesExist", func(t *testing.T) {
|
||||
m := cache.NewRingMap(ringMapCapacity)
|
||||
m.Set("foo", nil)
|
||||
assert.True(t, m.Delete("foo"))
|
||||
})
|
||||
|
||||
t.Run("KeyNoLongerExists", func(t *testing.T) {
|
||||
m := cache.NewRingMap(ringMapCapacity)
|
||||
m.Set("foo", nil)
|
||||
m.Delete("foo")
|
||||
_, exists := m.Get("foo")
|
||||
assert.False(t, exists)
|
||||
})
|
||||
|
||||
t.Run("KeyDeleteIsIsolated", func(t *testing.T) {
|
||||
m := cache.NewRingMap(ringMapCapacity)
|
||||
m.Set("foo", nil)
|
||||
m.Set("bar", nil)
|
||||
m.Delete("foo")
|
||||
_, exists := m.Get("bar")
|
||||
assert.True(t, exists)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRingMap_Front(t *testing.T) {
|
||||
t.Run("NilOnEmptyMap", func(t *testing.T) {
|
||||
m := cache.NewRingMap(ringMapCapacity)
|
||||
assert.Nil(t, m.Front())
|
||||
})
|
||||
|
||||
t.Run("NilOnEmptyMap", func(t *testing.T) {
|
||||
m := cache.NewRingMap(ringMapCapacity)
|
||||
m.Set("1", true)
|
||||
assert.NotNil(t, m.Front())
|
||||
})
|
||||
}
|
||||
|
||||
func TestRingMap_Back(t *testing.T) {
|
||||
t.Run("NilOnEmptyMap", func(t *testing.T) {
|
||||
m := cache.NewRingMap(ringMapCapacity)
|
||||
assert.Nil(t, m.Back())
|
||||
})
|
||||
|
||||
t.Run("NilOnEmptyMap", func(t *testing.T) {
|
||||
m := cache.NewRingMap(ringMapCapacity)
|
||||
m.Set("1", true)
|
||||
assert.NotNil(t, m.Back())
|
||||
})
|
||||
}
|
||||
func TestRingMap_Concurrency(t *testing.T) {
|
||||
t.Run("NilOnEmptyMap", func(t *testing.T) {
|
||||
m := cache.NewRingMap(ringMapCapacity)
|
||||
assert.Nil(t, m.Back())
|
||||
for i := 0; i < 1000000; i++ {
|
||||
go func() {
|
||||
m.Set("foo", nil)
|
||||
m.Exists("foo", nil)
|
||||
m.Set("bar", nil)
|
||||
m.Delete("foo")
|
||||
m.Get("bar")
|
||||
m.Delete("bar")
|
||||
}()
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func benchmarkMap_Set(multiplier int) func(b *testing.B) {
|
||||
return func(b *testing.B) {
|
||||
m := make(map[int]bool)
|
||||
for i := 0; i < b.N*multiplier; i++ {
|
||||
m[i] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMap_Set(b *testing.B) {
|
||||
benchmarkMap_Set(1)(b)
|
||||
}
|
||||
|
||||
func benchmarkRingMap_Set(multiplier int) func(b *testing.B) {
|
||||
return func(b *testing.B) {
|
||||
m := cache.NewRingMap(ringMapCapacity)
|
||||
for i := 0; i < b.N*multiplier; i++ {
|
||||
m.Set(strconv.Itoa(i), true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkRingMap_Set(b *testing.B) {
|
||||
benchmarkRingMap_Set(1)(b)
|
||||
}
|
||||
|
||||
func benchmarkMap_Get(multiplier int) func(b *testing.B) {
|
||||
m := make(map[int]bool)
|
||||
for i := 0; i < 1000*multiplier; i++ {
|
||||
m[i] = true
|
||||
}
|
||||
|
||||
return func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = m[i%1000*multiplier]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMap_Get(b *testing.B) {
|
||||
benchmarkMap_Get(1)(b)
|
||||
}
|
||||
|
||||
func benchmarkRingMap_Get(multiplier int) func(b *testing.B) {
|
||||
m := cache.NewRingMap(ringMapCapacity)
|
||||
for i := 0; i < 1000*multiplier; i++ {
|
||||
m.Set(strconv.Itoa(i), true)
|
||||
}
|
||||
|
||||
return func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
m.Get(strconv.Itoa(i % 1000 * multiplier))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkRingMap_Get(b *testing.B) {
|
||||
benchmarkRingMap_Get(1)(b)
|
||||
}
|
||||
|
||||
// prevent compiler from optimising Len away.
|
||||
var tempInt int
|
||||
|
||||
func benchmarkRingMap_Len(multiplier int) func(b *testing.B) {
|
||||
m := cache.NewRingMap(ringMapCapacity)
|
||||
for i := 0; i < 1000*multiplier; i++ {
|
||||
m.Set(strconv.Itoa(i), true)
|
||||
}
|
||||
|
||||
return func(b *testing.B) {
|
||||
var temp int
|
||||
for i := 0; i < b.N; i++ {
|
||||
temp = m.Len()
|
||||
}
|
||||
|
||||
// prevent compiler from optimising Len away.
|
||||
tempInt = temp
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkRingMap_Len(b *testing.B) {
|
||||
benchmarkRingMap_Len(1)(b)
|
||||
}
|
||||
|
||||
func benchmarkMap_Delete(multiplier int) func(b *testing.B) {
|
||||
return func(b *testing.B) {
|
||||
m := make(map[int]bool)
|
||||
for i := 0; i < b.N*multiplier; i++ {
|
||||
m[i] = true
|
||||
}
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
delete(m, i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMap_Delete(b *testing.B) {
|
||||
benchmarkMap_Delete(1)(b)
|
||||
}
|
||||
|
||||
func benchmarkRingMap_Delete(multiplier int) func(b *testing.B) {
|
||||
return func(b *testing.B) {
|
||||
m := cache.NewRingMap(ringMapCapacity)
|
||||
for i := 0; i < b.N*multiplier; i++ {
|
||||
m.Set(strconv.Itoa(i), true)
|
||||
}
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
m.Delete(strconv.Itoa(i))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkRingMap_Delete(b *testing.B) {
|
||||
benchmarkRingMap_Delete(1)(b)
|
||||
}
|
||||
|
||||
func benchmarkMap_Iterate(multiplier int) func(b *testing.B) {
|
||||
m := make(map[int]bool)
|
||||
for i := 0; i < 1000*multiplier; i++ {
|
||||
m[i] = true
|
||||
}
|
||||
return func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
for _, v := range m {
|
||||
nothing(v)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
func BenchmarkMap_Iterate(b *testing.B) {
|
||||
benchmarkMap_Iterate(1)(b)
|
||||
}
|
||||
|
||||
func benchmarkRingMap_Iterate(multiplier int) func(b *testing.B) {
|
||||
m := cache.NewRingMap(ringMapCapacity)
|
||||
for i := 0; i < 1000*multiplier; i++ {
|
||||
m.Set(strconv.Itoa(i), true)
|
||||
}
|
||||
|
||||
return func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
for _, key := range m.Keys() {
|
||||
_, v := m.Get(key)
|
||||
nothing(v)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkRingMap_Iterate(b *testing.B) {
|
||||
benchmarkRingMap_Iterate(1)(b)
|
||||
}
|
||||
|
||||
func benchmarkRingMap_Keys(multiplier int) func(b *testing.B) {
|
||||
m := cache.NewRingMap(ringMapCapacity)
|
||||
for i := 0; i < 1000*multiplier; i++ {
|
||||
m.Set(strconv.Itoa(i), true)
|
||||
}
|
||||
|
||||
return func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
m.Keys()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func benchmarkMapString_Set(multiplier int) func(b *testing.B) {
|
||||
return func(b *testing.B) {
|
||||
m := make(map[string]bool)
|
||||
a := "12345678"
|
||||
for i := 0; i < b.N*multiplier; i++ {
|
||||
m[a+strconv.Itoa(i)] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMapString_Set(b *testing.B) {
|
||||
benchmarkMapString_Set(1)(b)
|
||||
}
|
||||
|
||||
func benchmarkRingMapString_Set(multiplier int) func(b *testing.B) {
|
||||
return func(b *testing.B) {
|
||||
m := cache.NewRingMap(ringMapCapacity)
|
||||
a := "12345678"
|
||||
for i := 0; i < b.N*multiplier; i++ {
|
||||
m.Set(a+strconv.Itoa(i), true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkRingMapString_Set(b *testing.B) {
|
||||
benchmarkRingMapString_Set(1)(b)
|
||||
}
|
||||
|
||||
func benchmarkMapString_Get(multiplier int) func(b *testing.B) {
|
||||
m := make(map[string]bool)
|
||||
a := "12345678"
|
||||
for i := 0; i < 1000*multiplier; i++ {
|
||||
m[a+strconv.Itoa(i)] = true
|
||||
}
|
||||
|
||||
return func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = m[a+strconv.Itoa(i%1000*multiplier)]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMapString_Get(b *testing.B) {
|
||||
benchmarkMapString_Get(1)(b)
|
||||
}
|
||||
|
||||
func benchmarkRingMapString_Get(multiplier int) func(b *testing.B) {
|
||||
m := cache.NewRingMap(ringMapCapacity)
|
||||
a := "12345678"
|
||||
for i := 0; i < 1000*multiplier; i++ {
|
||||
m.Set(a+strconv.Itoa(i), true)
|
||||
}
|
||||
|
||||
return func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
m.Get(a + strconv.Itoa(i%1000*multiplier))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkRingMapString_Get(b *testing.B) {
|
||||
benchmarkRingMapString_Get(1)(b)
|
||||
}
|
||||
|
||||
func benchmarkMapString_Delete(multiplier int) func(b *testing.B) {
|
||||
return func(b *testing.B) {
|
||||
m := make(map[string]bool)
|
||||
a := "12345678"
|
||||
for i := 0; i < b.N*multiplier; i++ {
|
||||
m[a+strconv.Itoa(i)] = true
|
||||
}
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
delete(m, a+strconv.Itoa(i))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMapString_Delete(b *testing.B) {
|
||||
benchmarkMapString_Delete(1)(b)
|
||||
}
|
||||
|
||||
func benchmarkRingMapString_Delete(multiplier int) func(b *testing.B) {
|
||||
return func(b *testing.B) {
|
||||
m := cache.NewRingMap(ringMapCapacity)
|
||||
a := "12345678"
|
||||
for i := 0; i < b.N*multiplier; i++ {
|
||||
m.Set(a+strconv.Itoa(i), true)
|
||||
}
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
m.Delete(a + strconv.Itoa(i))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkRingMapString_Delete(b *testing.B) {
|
||||
benchmarkRingMapString_Delete(1)(b)
|
||||
}
|
||||
|
||||
func benchmarkMapString_Iterate(multiplier int) func(b *testing.B) {
|
||||
m := make(map[string]bool)
|
||||
a := "12345678"
|
||||
for i := 0; i < 1000*multiplier; i++ {
|
||||
m[a+strconv.Itoa(i)] = true
|
||||
}
|
||||
return func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
for _, v := range m {
|
||||
nothing(v)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
func BenchmarkMapString_Iterate(b *testing.B) {
|
||||
benchmarkMapString_Iterate(1)(b)
|
||||
}
|
||||
|
||||
func benchmarkRingMapString_Iterate(multiplier int) func(b *testing.B) {
|
||||
m := cache.NewRingMap(ringMapCapacity)
|
||||
a := "12345678"
|
||||
for i := 0; i < 1000*multiplier; i++ {
|
||||
m.Set(a+strconv.Itoa(i), true)
|
||||
}
|
||||
|
||||
return func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
for _, key := range m.Keys() {
|
||||
_, v := m.Get(key)
|
||||
nothing(v)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkRingMapString_Iterate(b *testing.B) {
|
||||
benchmarkRingMapString_Iterate(1)(b)
|
||||
}
|
||||
|
||||
func BenchmarkRingMap_Keys(b *testing.B) {
|
||||
benchmarkRingMap_Keys(1)(b)
|
||||
}
|
||||
|
||||
func ExampleNewRingMap() {
|
||||
m := cache.NewRingMap(ringMapCapacity)
|
||||
|
||||
m.Set("foo", "bar")
|
||||
m.Set("qux", 1.23)
|
||||
m.Set("123", true)
|
||||
|
||||
m.Delete("qux")
|
||||
|
||||
for _, key := range m.Keys() {
|
||||
value, _ := m.Get(key)
|
||||
fmt.Println(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
func ExampleRingMap_Front() {
|
||||
m := cache.NewRingMap(ringMapCapacity)
|
||||
m.Set("1", true)
|
||||
m.Set("2", true)
|
||||
|
||||
for el := m.Front(); el != nil; el = el.Next() {
|
||||
fmt.Println(el)
|
||||
}
|
||||
}
|
||||
|
||||
func nothing(v interface{}) {
|
||||
v = false
|
||||
}
|
||||
|
||||
func benchmarkBigMap_Set() func(b *testing.B) {
|
||||
return func(b *testing.B) {
|
||||
for j := 0; j < b.N; j++ {
|
||||
m := make(map[int]bool)
|
||||
for i := 0; i < 10000000; i++ {
|
||||
m[i] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkBigMap_Set(b *testing.B) {
|
||||
benchmarkBigMap_Set()(b)
|
||||
}
|
||||
|
||||
func benchmarkBigRingMap_Set() func(b *testing.B) {
|
||||
return func(b *testing.B) {
|
||||
for j := 0; j < b.N; j++ {
|
||||
m := cache.NewRingMap(ringMapCapacity)
|
||||
for i := 0; i < 10000000; i++ {
|
||||
m.Set(strconv.Itoa(i), true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkBigRingMap_Set(b *testing.B) {
|
||||
benchmarkBigRingMap_Set()(b)
|
||||
}
|
||||
|
||||
func benchmarkBigMap_Get() func(b *testing.B) {
|
||||
m := make(map[int]bool)
|
||||
for i := 0; i < 10000000; i++ {
|
||||
m[i] = true
|
||||
}
|
||||
|
||||
return func(b *testing.B) {
|
||||
for j := 0; j < b.N; j++ {
|
||||
for i := 0; i < 10000000; i++ {
|
||||
_ = m[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkBigMap_Get(b *testing.B) {
|
||||
benchmarkBigMap_Get()(b)
|
||||
}
|
||||
|
||||
func benchmarkBigRingMap_Get() func(b *testing.B) {
|
||||
m := cache.NewRingMap(ringMapCapacity)
|
||||
for i := 0; i < 10000000; i++ {
|
||||
m.Set(strconv.Itoa(i), true)
|
||||
}
|
||||
|
||||
return func(b *testing.B) {
|
||||
for j := 0; j < b.N; j++ {
|
||||
for i := 0; i < 10000000; i++ {
|
||||
m.Get(strconv.Itoa(i))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkBigRingMap_Get(b *testing.B) {
|
||||
benchmarkBigRingMap_Get()(b)
|
||||
}
|
||||
|
||||
func benchmarkBigMap_Iterate() func(b *testing.B) {
|
||||
m := make(map[int]bool)
|
||||
for i := 0; i < 10000000; i++ {
|
||||
m[i] = true
|
||||
}
|
||||
return func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
for _, v := range m {
|
||||
nothing(v)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
func BenchmarkBigMap_Iterate(b *testing.B) {
|
||||
benchmarkBigMap_Iterate()(b)
|
||||
}
|
||||
|
||||
func benchmarkBigRingMap_Iterate() func(b *testing.B) {
|
||||
m := cache.NewRingMap(ringMapCapacity)
|
||||
for i := 0; i < 10000000; i++ {
|
||||
m.Set(strconv.Itoa(i), true)
|
||||
}
|
||||
|
||||
return func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
for _, key := range m.Keys() {
|
||||
_, v := m.Get(key)
|
||||
nothing(v)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkBigRingMap_Iterate(b *testing.B) {
|
||||
benchmarkBigRingMap_Iterate()(b)
|
||||
}
|
||||
|
||||
func benchmarkBigMapString_Set() func(b *testing.B) {
|
||||
return func(b *testing.B) {
|
||||
for j := 0; j < b.N; j++ {
|
||||
m := make(map[string]bool)
|
||||
a := "1234567"
|
||||
for i := 0; i < 10000000; i++ {
|
||||
m[a+strconv.Itoa(i)] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkBigMapString_Set(b *testing.B) {
|
||||
benchmarkBigMapString_Set()(b)
|
||||
}
|
||||
|
||||
func benchmarkBigRingMapString_Set() func(b *testing.B) {
|
||||
return func(b *testing.B) {
|
||||
for j := 0; j < b.N; j++ {
|
||||
m := cache.NewRingMap(ringMapCapacity)
|
||||
a := "1234567"
|
||||
for i := 0; i < 10000000; i++ {
|
||||
m.Set(a+strconv.Itoa(i), true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkBigRingMapString_Set(b *testing.B) {
|
||||
benchmarkBigRingMapString_Set()(b)
|
||||
}
|
||||
|
||||
func benchmarkBigMapString_Get() func(b *testing.B) {
|
||||
m := make(map[string]bool)
|
||||
a := "1234567"
|
||||
for i := 0; i < 10000000; i++ {
|
||||
m[a+strconv.Itoa(i)] = true
|
||||
}
|
||||
|
||||
return func(b *testing.B) {
|
||||
for j := 0; j < b.N; j++ {
|
||||
for i := 0; i < 10000000; i++ {
|
||||
_ = m[a+strconv.Itoa(i)]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkBigMapString_Get(b *testing.B) {
|
||||
benchmarkBigMapString_Get()(b)
|
||||
}
|
||||
|
||||
func benchmarkBigRingMapString_Get() func(b *testing.B) {
|
||||
m := cache.NewRingMap(ringMapCapacity)
|
||||
a := "1234567"
|
||||
for i := 0; i < 10000000; i++ {
|
||||
m.Set(a+strconv.Itoa(i), true)
|
||||
}
|
||||
|
||||
return func(b *testing.B) {
|
||||
for j := 0; j < b.N; j++ {
|
||||
for i := 0; i < 10000000; i++ {
|
||||
m.Get(a + strconv.Itoa(i))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkBigRingMapString_Get(b *testing.B) {
|
||||
benchmarkBigRingMapString_Get()(b)
|
||||
}
|
||||
|
||||
func benchmarkBigMapString_Iterate() func(b *testing.B) {
|
||||
m := make(map[string]bool)
|
||||
a := "12345678"
|
||||
for i := 0; i < 10000000; i++ {
|
||||
m[a+strconv.Itoa(i)] = true
|
||||
}
|
||||
return func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
for _, v := range m {
|
||||
nothing(v)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
func BenchmarkBigMapString_Iterate(b *testing.B) {
|
||||
benchmarkBigMapString_Iterate()(b)
|
||||
}
|
||||
|
||||
func benchmarkBigRingMapString_Iterate() func(b *testing.B) {
|
||||
m := cache.NewRingMap(ringMapCapacity)
|
||||
a := "12345678"
|
||||
for i := 0; i < 10000000; i++ {
|
||||
m.Set(a+strconv.Itoa(i), true)
|
||||
}
|
||||
|
||||
return func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
for _, key := range m.Keys() {
|
||||
_, v := m.Get(key)
|
||||
nothing(v)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkBigRingMapString_Iterate(b *testing.B) {
|
||||
benchmarkBigRingMapString_Iterate()(b)
|
||||
}
|
||||
|
||||
func BenchmarkAll(b *testing.B) {
|
||||
b.Run("BenchmarkRingMap_Keys", BenchmarkRingMap_Keys)
|
||||
|
||||
b.Run("BenchmarkRingMap_Set", BenchmarkRingMap_Set)
|
||||
b.Run("BenchmarkMap_Set", BenchmarkMap_Set)
|
||||
b.Run("BenchmarkRingMap_Get", BenchmarkRingMap_Get)
|
||||
b.Run("BenchmarkMap_Get", BenchmarkMap_Get)
|
||||
b.Run("BenchmarkRingMap_Delete", BenchmarkRingMap_Delete)
|
||||
b.Run("BenchmarkMap_Delete", BenchmarkMap_Delete)
|
||||
b.Run("BenchmarkRingMap_Iterate", BenchmarkRingMap_Iterate)
|
||||
b.Run("BenchmarkMap_Iterate", BenchmarkMap_Iterate)
|
||||
|
||||
b.Run("BenchmarkBigMap_Set", BenchmarkBigMap_Set)
|
||||
b.Run("BenchmarkBigRingMap_Set", BenchmarkBigRingMap_Set)
|
||||
b.Run("BenchmarkBigMap_Get", BenchmarkBigMap_Get)
|
||||
b.Run("BenchmarkBigRingMap_Get", BenchmarkBigRingMap_Get)
|
||||
b.Run("BenchmarkBigRingMap_Iterate", BenchmarkBigRingMap_Iterate)
|
||||
b.Run("BenchmarkBigMap_Iterate", BenchmarkBigMap_Iterate)
|
||||
|
||||
b.Run("BenchmarkRingMapString_Set", BenchmarkRingMapString_Set)
|
||||
b.Run("BenchmarkMapString_Set", BenchmarkMapString_Set)
|
||||
b.Run("BenchmarkRingMapString_Get", BenchmarkRingMapString_Get)
|
||||
b.Run("BenchmarkMapString_Get", BenchmarkMapString_Get)
|
||||
b.Run("BenchmarkRingMapString_Delete", BenchmarkRingMapString_Delete)
|
||||
b.Run("BenchmarkMapString_Delete", BenchmarkMapString_Delete)
|
||||
b.Run("BenchmarkRingMapString_Iterate", BenchmarkRingMapString_Iterate)
|
||||
b.Run("BenchmarkMapString_Iterate", BenchmarkMapString_Iterate)
|
||||
|
||||
b.Run("BenchmarkBigMapString_Set", BenchmarkBigMapString_Set)
|
||||
b.Run("BenchmarkBigRingMapString_Set", BenchmarkBigRingMapString_Set)
|
||||
b.Run("BenchmarkBigMapString_Get", BenchmarkBigMapString_Get)
|
||||
b.Run("BenchmarkBigRingMapString_Get", BenchmarkBigRingMapString_Get)
|
||||
b.Run("BenchmarkBigRingMapString_Iterate", BenchmarkBigRingMapString_Iterate)
|
||||
b.Run("BenchmarkBigMapString_Iterate", BenchmarkBigMapString_Iterate)
|
||||
}
|
||||
103
pkg/cache/subscription_types.go
vendored
Normal file
103
pkg/cache/subscription_types.go
vendored
Normal file
@@ -0,0 +1,103 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"fiskerinc.com/modules/common"
|
||||
"fiskerinc.com/modules/db/queries"
|
||||
"fiskerinc.com/modules/duration"
|
||||
"fiskerinc.com/modules/logger"
|
||||
"fiskerinc.com/modules/redis"
|
||||
"github.com/google/uuid"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func RetrieveSubscriptionTypesList(client redis.Client, db queries.SubscriptionTypesInterface) ([]common.SubscriptionType, error) {
|
||||
data, err := retrieveSubscriptionTypesRedis(client, uuid.Nil)
|
||||
if err != nil && !errors.Is(err, redis.ErrInvalidResults) {
|
||||
return nil, err
|
||||
}
|
||||
if data != nil {
|
||||
return convertSubTypesList(data)
|
||||
}
|
||||
|
||||
items, err := db.Select(&common.SubscriptionType{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(items) == 0 {
|
||||
logger.Warn().Msg("no subscription types")
|
||||
return []common.SubscriptionType{}, nil
|
||||
}
|
||||
|
||||
for i := range items {
|
||||
item := &items[i]
|
||||
item.ClearDates()
|
||||
}
|
||||
|
||||
err = storeSubscriptionTypesRedis(client, uuid.Nil, items)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func RetrieveSubscriptionType(client redis.Client, db queries.SubscriptionTypesInterface, subtypeID uuid.UUID) (*common.SubscriptionType, error) {
|
||||
data, err := retrieveSubscriptionTypesRedis(client, subtypeID)
|
||||
if err != nil && !errors.Is(err, redis.ErrInvalidResults) {
|
||||
return nil, err
|
||||
}
|
||||
if data != nil {
|
||||
return convertSubType(data)
|
||||
}
|
||||
|
||||
items, err := db.Select(&common.SubscriptionType{ID: subtypeID})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(items) == 0 {
|
||||
return nil, errors.Errorf("subscription type %v not found", subtypeID)
|
||||
}
|
||||
|
||||
err = storeSubscriptionTypesRedis(client, subtypeID, items[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &items[0], nil
|
||||
}
|
||||
|
||||
func storeSubscriptionTypesRedis(client redis.Client, subtypeID uuid.UUID, data interface{}) error {
|
||||
return client.SetCache(redis.SubscriptionTypeListKey(subtypeID), data, duration.Hour)
|
||||
}
|
||||
|
||||
func retrieveSubscriptionTypesRedis(client redis.Client, subtypeID uuid.UUID) ([]byte, error) {
|
||||
key := redis.SubscriptionTypeListKey(subtypeID)
|
||||
values, err := client.Get(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if values == nil {
|
||||
return nil, redis.ErrInvalidResults
|
||||
}
|
||||
|
||||
data, ok := values.([]byte)
|
||||
if !ok {
|
||||
return nil, errors.New("unable to convert to []byte")
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func convertSubTypesList(data []byte) ([]common.SubscriptionType, error) {
|
||||
result := []common.SubscriptionType{}
|
||||
err := json.Unmarshal(data, &result)
|
||||
return result, err
|
||||
}
|
||||
|
||||
func convertSubType(data []byte) (*common.SubscriptionType, error) {
|
||||
result := common.SubscriptionType{}
|
||||
err := json.Unmarshal(data, &result)
|
||||
return &result, err
|
||||
}
|
||||
88
pkg/cache/subscription_types_test.go
vendored
Normal file
88
pkg/cache/subscription_types_test.go
vendored
Normal file
@@ -0,0 +1,88 @@
|
||||
package cache_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"fiskerinc.com/modules/cache"
|
||||
"fiskerinc.com/modules/common"
|
||||
"fiskerinc.com/modules/db/queries"
|
||||
"fiskerinc.com/modules/redis"
|
||||
"fiskerinc.com/modules/testhelper"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func TestRetrieveSubscriptionTypes(t *testing.T) {
|
||||
t.Skip()
|
||||
query := queries.SubscriptionTypes{}
|
||||
client := redis.NewClient()
|
||||
subtype, err := setupTestSubscritionType(&query)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
defer cleanupTestSubscriptionType(client, &query, subtype)
|
||||
|
||||
list, err := cache.RetrieveSubscriptionTypesList(client, &query)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if len(list) == 0 {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "From DB", "1 or more", len(list))
|
||||
}
|
||||
|
||||
list, err = cache.RetrieveSubscriptionTypesList(client, &query)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if len(list) == 0 {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "From Redis", "1 or more", len(list))
|
||||
}
|
||||
|
||||
item, err := cache.RetrieveSubscriptionType(client, &query, subtype.ID)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if item.Name != subtype.Name {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "RetrieveSubscriptionType From DB", subtype.Name, item.Name)
|
||||
}
|
||||
|
||||
item, err = cache.RetrieveSubscriptionType(client, &query, subtype.ID)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if item.Name != subtype.Name {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "RetrieveSubscriptionType From Redis", subtype.Name, item.Name)
|
||||
}
|
||||
|
||||
item, err = cache.RetrieveSubscriptionType(client, &query, uuid.New())
|
||||
if err == nil {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "Invalid subscription type id", "found out", err)
|
||||
}
|
||||
if item != nil {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "Invalid subscription type item", nil, item)
|
||||
}
|
||||
}
|
||||
|
||||
func setupTestSubscritionType(query *queries.SubscriptionTypes) (*common.SubscriptionType, error) {
|
||||
subtype := common.SubscriptionType{
|
||||
Name: fmt.Sprintf("Test type %s", uuid.New().String()),
|
||||
Destination: "ICC",
|
||||
Description: "test",
|
||||
Currency: "USD",
|
||||
Price: 10000, // $100 USD
|
||||
DurationValue: 1,
|
||||
DurationUnit: "Hours",
|
||||
}
|
||||
_, err := query.Insert(&subtype)
|
||||
|
||||
return &subtype, err
|
||||
}
|
||||
|
||||
func cleanupTestSubscriptionType(client redis.Client, query *queries.SubscriptionTypes, subtype *common.SubscriptionType) {
|
||||
client.Delete(redis.SubscriptionTypeListKey(subtype.ID))
|
||||
client.Delete(redis.SubscriptionTypeListKey(uuid.Nil))
|
||||
query.Delete(subtype)
|
||||
}
|
||||
197
pkg/cache/vehicle_config.go
vendored
Normal file
197
pkg/cache/vehicle_config.go
vendored
Normal file
@@ -0,0 +1,197 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"fiskerinc.com/modules/common"
|
||||
"fiskerinc.com/modules/logger"
|
||||
"fiskerinc.com/modules/mongo"
|
||||
"fiskerinc.com/modules/redis"
|
||||
"fiskerinc.com/modules/utils/envtool"
|
||||
|
||||
"fiskerinc.com/modules/utils/elptr"
|
||||
redigo "github.com/gomodule/redigo/redis"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
ENABLE_DBG_MASK_EV_NAME = "ENABLE_DEBUGMASK"
|
||||
ENABLE_DBG_MASK_VAL_FALSE = "0"
|
||||
ENABLE_DBG_MASK_VAL_TRUE = "1"
|
||||
ENABLE_DBG_MASK_VAL_DEFAULT = ENABLE_DBG_MASK_VAL_FALSE
|
||||
)
|
||||
|
||||
// This flag is to decide whether retrieved value of DebugMask is to be passed to TrexCfg or not.
|
||||
// When the flag is true, the retrieved value is passed; else no value is passed.
|
||||
// The value of flag is fetched from the specific environmental variable. If that environmental
|
||||
// variable is not present / not defined, we assume the flag itself to be FALSE. That is the
|
||||
// default value (FALSE) of the environmental variable. When user/developer has set this evironmental
|
||||
// variable correctly, the flag can become TRUE in which case the value is passed to TrexCfg.
|
||||
var ENABLE_DEBUG_MASK = DbgMaskEnabled()
|
||||
|
||||
// method introduced so as unit testing is easier otherwise not necessary since environment variables
|
||||
// can't be changed so easily subsequent to a process start (meaning revaluation at runtime of no much use).
|
||||
func DbgMaskEnabled() bool {
|
||||
return envtool.GetEnv(ENABLE_DBG_MASK_EV_NAME, ENABLE_DBG_MASK_VAL_DEFAULT) == ENABLE_DBG_MASK_VAL_TRUE
|
||||
}
|
||||
|
||||
func RetrieveVehicleConfig(r redis.Client, m mongo.Client, id string) (*common.TRexConfigResponse, error) {
|
||||
config := &common.TRexConfigResponse{}
|
||||
|
||||
reply, err := checkCacheForVehicleConfig(r, id)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
if reply != nil {
|
||||
err = json.Unmarshal(reply, config)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
if config.CANBus.DTCEnabled == nil {
|
||||
config.CANBus.DTCEnabled = elptr.ElPtr(false)
|
||||
}
|
||||
return config, nil
|
||||
}
|
||||
|
||||
config.LogLevel = common.Critical
|
||||
config.Log = &common.LogConfig{
|
||||
Matches: []common.LogConfigChannel{
|
||||
{
|
||||
Channel: common.ChannelCMD,
|
||||
Level: common.Trace,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
config.CANBus.Enabled = true
|
||||
config.CANBus.DataLogger = true
|
||||
|
||||
filters := make(FiltersMap)
|
||||
|
||||
f, err := checkFleetsDBForVehicleConfig(m, id)
|
||||
if err != nil {
|
||||
logger.Warn().Err(err).Send()
|
||||
}
|
||||
if f != nil {
|
||||
config.CANBus = f.CANBus
|
||||
config.LogLevel = f.LogLevel
|
||||
filters.AppendFilters(f.CANBus.Filters)
|
||||
}
|
||||
|
||||
v, err := checkVehiclesDBForVehicleConfig(m, id)
|
||||
if err != nil {
|
||||
logger.Warn().Err(err).Send()
|
||||
}
|
||||
if v != nil {
|
||||
config.CANBus = v.CANBus
|
||||
config.LogLevel = v.LogLevel
|
||||
config.DLTEnabled = v.DLTEnabled
|
||||
config.DLTLevel = v.DLTLevel
|
||||
// we should evaluate at run-time, not just at start-up time
|
||||
if ENABLE_DEBUG_MASK {
|
||||
config.DebugMask = v.DebugMask
|
||||
}
|
||||
config.IDPSEnabled = v.IDPSEnabled
|
||||
|
||||
filters.AppendFilters(v.CANBus.Filters)
|
||||
}
|
||||
|
||||
config.CANBus.Filters = filters.ToSlice()
|
||||
if config.CANBus.DTCEnabled == nil {
|
||||
config.CANBus.DTCEnabled = elptr.ElPtr(false)
|
||||
}
|
||||
err = setCacheForVehicleConfig(r, id, config)
|
||||
return config, err
|
||||
}
|
||||
|
||||
func checkCacheForVehicleConfig(r redis.Client, id string) ([]byte, error) {
|
||||
key := redis.CarConfigKey(id)
|
||||
|
||||
reply, err := redigo.Bytes(r.Execute("GET", key))
|
||||
if err != nil {
|
||||
if errors.Is(err, redigo.ErrNil) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return reply, nil
|
||||
}
|
||||
|
||||
func checkVehiclesDBForVehicleConfig(m mongo.Client, id string) (*mongo.Vehicle, error) {
|
||||
return m.GetVehicles().FindVehicle(&mongo.Vehicle{VIN: id})
|
||||
}
|
||||
|
||||
func checkFleetsDBForVehicleConfig(m mongo.Client, id string) (*mongo.Fleet, error) {
|
||||
return m.GetFleets().GetCANBusForVehicle(id)
|
||||
}
|
||||
|
||||
func setCacheForVehicleConfig(r redis.Client, id string, config *common.TRexConfigResponse) error {
|
||||
key := redis.CarConfigKey(id)
|
||||
|
||||
data, err := json.Marshal(config)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
batch := redis.NewRedisBatchCommands()
|
||||
batch.Add("SET", key, data)
|
||||
batch.Add("EXPIRE", key, redisObjectExpire)
|
||||
|
||||
_, err = r.ExecuteBatch(batch)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func RemoveCacheConfigForVehicles(r redis.Client, vins []string) error {
|
||||
batch := redis.NewRedisBatchCommands()
|
||||
for _, vin := range vins {
|
||||
batch.Add("DEL", redis.CarConfigKey(vin))
|
||||
}
|
||||
|
||||
_, err := r.ExecuteBatch(batch)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type IntervalEdgeMask struct {
|
||||
Interval *int
|
||||
EdgeMask *common.BinaryHex
|
||||
}
|
||||
|
||||
type FiltersMap map[string]IntervalEdgeMask
|
||||
|
||||
func (f FiltersMap) AppendFilters(filters []common.CANFilter) {
|
||||
for _, filter := range filters {
|
||||
if filter.EdgeMask != nil && filter.EdgeMask.String() != "" {
|
||||
f[filter.CANID] = IntervalEdgeMask{
|
||||
EdgeMask: filter.EdgeMask,
|
||||
}
|
||||
} else if filter.Interval != nil {
|
||||
f[filter.CANID] = IntervalEdgeMask{
|
||||
Interval: filter.Interval,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (f FiltersMap) ToSlice() []common.CANFilter {
|
||||
filters := make([]common.CANFilter, 0, len(f))
|
||||
|
||||
for k, v := range f {
|
||||
filters = append(filters, common.CANFilter{
|
||||
CANID: k,
|
||||
Interval: v.Interval,
|
||||
EdgeMask: v.EdgeMask,
|
||||
})
|
||||
}
|
||||
|
||||
return filters
|
||||
}
|
||||
203
pkg/cache/vehicle_config_test.go
vendored
Normal file
203
pkg/cache/vehicle_config_test.go
vendored
Normal file
@@ -0,0 +1,203 @@
|
||||
package cache_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"fiskerinc.com/modules/cache"
|
||||
"fiskerinc.com/modules/common"
|
||||
"fiskerinc.com/modules/mongo"
|
||||
"fiskerinc.com/modules/redis"
|
||||
"fiskerinc.com/modules/testhelper"
|
||||
"fiskerinc.com/modules/utils/elptr"
|
||||
redigo "github.com/gomodule/redigo/redis"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestRetrieveVehicleConfig(t *testing.T) {
|
||||
setupRedisMock()
|
||||
id := "TESTVIN1234567"
|
||||
|
||||
mockRedis = &mockRedisVehicleConfig{}
|
||||
config, err := cache.RetrieveVehicleConfig(mockRedis, mongo.NewMockClient(), id)
|
||||
if err != nil {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "TestRetrieveVehicleConfig", nil, err)
|
||||
}
|
||||
data, err := json.Marshal(&config)
|
||||
if err != nil {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "TestRetrieveVehicleConfig", nil, err)
|
||||
}
|
||||
assert.Equal(t, `{"canbus":{"enabled":false,"data_logger_enabled":false,"dtc_enabled":false},"log_level":"trace"}`, string(data))
|
||||
|
||||
mockRedis = &mockRedisNoVehicleConfig{}
|
||||
config, err = cache.RetrieveVehicleConfig(mockRedis, mongo.NewMockClient(), id)
|
||||
if err != nil {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "TestRetrieveVehicleConfig", nil, err)
|
||||
}
|
||||
data, err = json.Marshal(&config)
|
||||
if err != nil {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "TestRetrieveVehicleConfig", nil, err)
|
||||
}
|
||||
assert.Equal(t, `{"canbus":{"enabled":false,"data_logger_enabled":false,"dtc_enabled":false},"log_level":"trace","log":{"matches":[{"channel":"cmd","level":"trace"}]}}`, string(data))
|
||||
}
|
||||
|
||||
func TestRetrieveVehicleConfigDbgMask(t *testing.T) {
|
||||
setupRedisMock()
|
||||
id := "TESTVIN1234567"
|
||||
mockVehicle := mongo.Vehicle{VIN: id}
|
||||
mockRedis = &mockRedisNoVehicleConfig{}
|
||||
|
||||
// validate that by default, retrieved debug value IS NOT passed to trxCfg
|
||||
trxCfg, err := cache.RetrieveVehicleConfig(mockRedis, mongo.NewMockClient(), id)
|
||||
existingValue := trxCfg.DebugMask
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, trxCfg)
|
||||
// assert that trxCfg value is unchanged
|
||||
assert.Equal(t, trxCfg.DebugMask, existingValue)
|
||||
|
||||
// let us try to enable
|
||||
// the mock for redis is with no data so that code will fall through to the DB part
|
||||
// we ensure that what we get from DB has speific debug mask which should be
|
||||
// passed to Trex when the flag is true
|
||||
t.Setenv(cache.ENABLE_DBG_MASK_EV_NAME, cache.ENABLE_DBG_MASK_VAL_TRUE)
|
||||
cache.ENABLE_DEBUG_MASK = cache.DbgMaskEnabled()
|
||||
mmc := mongo.NewMockMongoClient()
|
||||
mockVehicle.DebugMask = "test"
|
||||
mmc.GetVehicles().AddVehicle(&mockVehicle)
|
||||
trxCfg, _ = cache.RetrieveVehicleConfig(mockRedis, mmc, id)
|
||||
// now validate that Trex config got the value as set in the mocked vehicle
|
||||
// (presumed as retrieved)
|
||||
assert.Equal(t, trxCfg.DebugMask, mockVehicle.DebugMask)
|
||||
|
||||
// now set back the env variable so new values don't flow to trex
|
||||
t.Setenv(cache.ENABLE_DBG_MASK_EV_NAME, cache.ENABLE_DBG_MASK_VAL_FALSE)
|
||||
cache.ENABLE_DEBUG_MASK = cache.DbgMaskEnabled()
|
||||
oldMask := mockVehicle.DebugMask
|
||||
mockVehicle.DebugMask = "new-value"
|
||||
// skipping adding to the cache/DB as we still had the valid reference
|
||||
trxCfg, _ = cache.RetrieveVehicleConfig(mockRedis, mmc, id)
|
||||
// assert that trex does not have new value
|
||||
assert.NotEqual(t, trxCfg.DebugMask, oldMask)
|
||||
}
|
||||
|
||||
func TestFiltersMap(t *testing.T) {
|
||||
filters := make(cache.FiltersMap)
|
||||
|
||||
if len(filters) != 0 {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "TestFiltersMap", 0, len(filters))
|
||||
return
|
||||
}
|
||||
|
||||
emptyHex := common.NewBinaryHex([]byte{})
|
||||
bhex := common.BinaryHex("123")
|
||||
filters.AppendFilters(
|
||||
[]common.CANFilter{
|
||||
{CANID: "123", Interval: elptr.ElPtr(123)},
|
||||
{CANID: "456", Interval: elptr.ElPtr(456)},
|
||||
{CANID: "789", EdgeMask: &emptyHex},
|
||||
{CANID: "901", EdgeMask: &bhex},
|
||||
{CANID: "222", Interval: elptr.ElPtr(123), EdgeMask: &bhex},
|
||||
{CANID: "333", Interval: elptr.ElPtr(0)},
|
||||
},
|
||||
)
|
||||
if len(filters) != 5 {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "TestFiltersMap", 5, len(filters))
|
||||
return
|
||||
}
|
||||
|
||||
interval, ok := filters["123"]
|
||||
if !ok || *interval.Interval != 123 && interval.EdgeMask != nil {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "TestFiltersMap", 123, "error")
|
||||
return
|
||||
}
|
||||
|
||||
interval, ok = filters["456"]
|
||||
|
||||
if !ok || *interval.Interval != 456 && interval.EdgeMask != nil {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "TestFiltersMap", 456, "error")
|
||||
return
|
||||
}
|
||||
|
||||
interval, ok = filters["789"]
|
||||
if ok || interval.EdgeMask != nil || interval.Interval != nil {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "TestFiltersMap", emptyHex, "error")
|
||||
return
|
||||
}
|
||||
|
||||
interval, ok = filters["901"]
|
||||
if !ok || interval.EdgeMask.String() != bhex.String() && interval.Interval != nil {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "TestFiltersMap", bhex, "error")
|
||||
return
|
||||
}
|
||||
|
||||
interval, ok = filters["222"]
|
||||
if !ok || interval.EdgeMask.String() != bhex.String() && interval.Interval != nil {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "TestFiltersMap", bhex, "error")
|
||||
return
|
||||
}
|
||||
|
||||
interval, ok = filters["333"]
|
||||
if !ok || interval.EdgeMask != nil && *interval.Interval != 0 {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "TestFiltersMap", nil, "error")
|
||||
return
|
||||
}
|
||||
|
||||
slice := filters.ToSlice()
|
||||
if len(slice) != 5 {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "TestFiltersMap", 5, len(slice))
|
||||
return
|
||||
}
|
||||
|
||||
sort.Slice(slice, func(i, j int) bool {
|
||||
return slice[i].CANID < slice[j].CANID
|
||||
})
|
||||
|
||||
if slice[0].CANID != "123" {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "TestFiltersMap", "123", slice[0].CANID)
|
||||
return
|
||||
}
|
||||
|
||||
if slice[1].CANID != "222" {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "TestFiltersMap", "222", slice[1].CANID)
|
||||
return
|
||||
}
|
||||
|
||||
if slice[2].CANID != "333" {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "TestFiltersMap", "333", slice[2].CANID)
|
||||
return
|
||||
}
|
||||
|
||||
if slice[3].CANID != "456" {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "TestFiltersMap", "456", slice[0].CANID)
|
||||
return
|
||||
}
|
||||
|
||||
if slice[4].CANID != "901" {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "TestFiltersMap", "901", slice[0].CANID)
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
type mockRedisVehicleConfig struct {
|
||||
redis.Connection
|
||||
}
|
||||
|
||||
func (c *mockRedisVehicleConfig) Execute(command ...interface{}) (interface{}, error) {
|
||||
config := common.TRexConfigResponse{}
|
||||
data, _ := json.Marshal(config)
|
||||
return data, nil
|
||||
}
|
||||
|
||||
type mockRedisNoVehicleConfig struct {
|
||||
redis.Connection
|
||||
}
|
||||
|
||||
func (c *mockRedisNoVehicleConfig) Execute(command ...interface{}) (interface{}, error) {
|
||||
return nil, redigo.ErrNil
|
||||
}
|
||||
|
||||
func (c *mockRedisNoVehicleConfig) ExecuteBatch(batch *redis.RedisBatchCommands) (interface{}, error) {
|
||||
return nil, nil
|
||||
}
|
||||
583
pkg/cache/vehicle_state.go
vendored
Normal file
583
pkg/cache/vehicle_state.go
vendored
Normal file
@@ -0,0 +1,583 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"fiskerinc.com/modules/common"
|
||||
dt "fiskerinc.com/modules/dbc/state"
|
||||
"fiskerinc.com/modules/logger"
|
||||
"fiskerinc.com/modules/redis"
|
||||
"fiskerinc.com/modules/utils/querystring"
|
||||
|
||||
redigo "github.com/gomodule/redigo/redis"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const UPDATED_TIME_FORMAT = "2006-01-02T15:04:05Z"
|
||||
|
||||
type stateParser func(state *common.CarState, key string, value []byte) error
|
||||
|
||||
func NewVehicleState(client redis.ClientPoolInterface) *VehicleState {
|
||||
return &VehicleState{client: client}
|
||||
}
|
||||
|
||||
type VehicleState struct {
|
||||
client redis.ClientPoolInterface
|
||||
}
|
||||
|
||||
func (v *VehicleState) Get(vin string) (common.CarState, error) {
|
||||
var state common.CarState
|
||||
|
||||
values, err := v.queryVehicleState(vin)
|
||||
if err != nil {
|
||||
return state, err
|
||||
}
|
||||
|
||||
state, err = v.ParsePayloadForVehicleState(values)
|
||||
if err != nil {
|
||||
return state, err
|
||||
}
|
||||
|
||||
return state, nil
|
||||
}
|
||||
|
||||
func (v *VehicleState) queryVehicleState(vin string) ([]interface{}, error) {
|
||||
var payload []interface{}
|
||||
client := v.client.GetFromPool()
|
||||
defer client.Close()
|
||||
|
||||
batch := redis.NewRedisBatchCommands()
|
||||
batch.Add("SISMEMBER", redis.CarSessionsKey(), vin)
|
||||
batch.Add("SISMEMBER", redis.HMISessionsKey(), vin)
|
||||
batch.Add("HGETALL", redis.CarStateHashKey(vin))
|
||||
|
||||
payload, err := redigo.Values(client.ExecuteBatch(batch))
|
||||
if err != nil {
|
||||
return payload, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return payload, nil
|
||||
}
|
||||
|
||||
func (v *VehicleState) ParsePayloadForVehicleState(payload []interface{}) (common.CarState, error) {
|
||||
var state common.CarState
|
||||
|
||||
if len(payload) != 3 {
|
||||
return state, redis.ErrInvalidResults
|
||||
}
|
||||
|
||||
online, err := redigo.Bool(payload[0], nil)
|
||||
if err != nil {
|
||||
return state, err
|
||||
} else {
|
||||
state.Online = online
|
||||
}
|
||||
|
||||
online, err = redigo.Bool(payload[1], nil)
|
||||
if err != nil {
|
||||
return state, errors.WithStack(err)
|
||||
} else {
|
||||
state.OnlineHMI = online
|
||||
}
|
||||
|
||||
err = v.parseCarStatePayload(&state, payload[2])
|
||||
|
||||
return state, err
|
||||
}
|
||||
|
||||
func (v *VehicleState) parseCarStatePayload(state *common.CarState, payload interface{}) error {
|
||||
stateValues, err := redigo.Values(payload, nil)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
if len(stateValues)%2 != 0 {
|
||||
return errors.New("object does not contain equal number of key value pairs")
|
||||
}
|
||||
|
||||
err = v.parseStateValues(state, stateValues, parseCarState)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (v *VehicleState) parseStateValues(state *common.CarState, stateValues []interface{}, parser stateParser) error {
|
||||
for i := 0; i < len(stateValues); i += 2 {
|
||||
key, okKey := stateValues[i].([]byte)
|
||||
value, okValue := stateValues[i+1].([]byte)
|
||||
|
||||
if !okKey || !okValue {
|
||||
return errors.New("cannot parse object into car state")
|
||||
}
|
||||
|
||||
err := parser(state, string(key), value)
|
||||
// log error, do not return error so we can read other properties for digital twin
|
||||
if err != nil {
|
||||
logger.Err(err).Send()
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseCarState(state *common.CarState, key string, value []byte) error {
|
||||
var err error
|
||||
val := string(value)
|
||||
|
||||
switch key {
|
||||
case dt.VCU_VehChrgDchgMod:
|
||||
state.GetVCU0x260().ChargeType = string(value)
|
||||
case dt.BMS_Bat_SoC_usable:
|
||||
state.GetStateOfCharge().Usable, err = strconv.Atoi(val)
|
||||
case dt.BMS_Bat_SOH:
|
||||
state.GetStateOfCharge().Health, err = strconv.Atoi(val)
|
||||
case dt.BCM_AP_FL_LeFrntWinPosnInfo:
|
||||
state.GetWindows().LeftFront, err = strconv.Atoi(val)
|
||||
case dt.BCM_AP_FL_RiFrntWinPosnInfo:
|
||||
state.GetWindows().RightFront, err = strconv.Atoi(val)
|
||||
case dt.BCM_AP_FL_LeReWinPosnInfo:
|
||||
state.GetWindows().LeftRear, err = strconv.Atoi(val)
|
||||
case dt.BCM_AP_FL_RiReWinPosnInfo:
|
||||
state.GetWindows().RightRear, err = strconv.Atoi(val)
|
||||
case dt.BMS_PwrBattRmngCpSOC:
|
||||
state.GetBattery().Percent, err = strconv.Atoi(val)
|
||||
case dt.BCM_ReDefrstHeatgCmd:
|
||||
state.GetRearDefrost().On, err = strconv.ParseBool(val)
|
||||
case dt.BCM_PasFrntDoorSts:
|
||||
state.GetDoors().RightFront, err = strconv.ParseBool(val)
|
||||
case dt.BCM_DrFrntDoorSts:
|
||||
state.GetDoors().LeftFront, err = strconv.ParseBool(val)
|
||||
case dt.BCM_FrntDrDoorLockSts:
|
||||
state.GetLocks().Driver, err = notValue(strconv.ParseBool(val))
|
||||
case dt.BCM_CenLockSwtSts:
|
||||
state.GetLocks().All = (val == "2")
|
||||
case dt.BCM_RiReDoorSts:
|
||||
state.GetDoors().RightRear, err = strconv.ParseBool(val)
|
||||
case dt.BCM_LeReDoorSts:
|
||||
state.GetDoors().LeftRear, err = strconv.ParseBool(val)
|
||||
case dt.BCM_FrntHoodLidSts:
|
||||
state.GetDoors().Hood, err = strconv.ParseBool(val)
|
||||
case dt.PLGM_TrSts:
|
||||
state.GetDoors().Trunk, err = strconv.ParseBool(val)
|
||||
case dt.BCM_SunroofPosnInfo:
|
||||
state.GetSunroof().Sunroof, err = strconv.Atoi(val)
|
||||
case dt.BCM_AP_TL_LeReWinPosnInfo:
|
||||
state.GetMiscWindows().LeftRearQuarter, err = strconv.Atoi(val)
|
||||
case dt.BCM_AP_TL_RiReWinPosnInfo:
|
||||
state.GetMiscWindows().RightRearQuarter, err = strconv.Atoi(val)
|
||||
case dt.BCM_AP_RW_WinPosnInfo:
|
||||
state.GetMiscWindows().RearWindshield, err = strconv.Atoi(val)
|
||||
case dt.BMS_BattAvrgT:
|
||||
state.GetCellTemperature().AvgBatteryTemp, err = strconv.Atoi(val)
|
||||
case dt.ECC_OutdT:
|
||||
state.GetAmbientTemperature().Temperature, err = strconv.Atoi(val)
|
||||
case dt.BCM_HeatedSteerWhlSt:
|
||||
state.GetSteeringWheelHeat().On, err = strconv.ParseBool(val)
|
||||
case dt.ESP_VehSpd:
|
||||
state.GetVehicleSpeed().Speed, err = strconv.ParseFloat(val, 64)
|
||||
case dt.VCU_DrvgMilg:
|
||||
state.GetMaxRange().MaxMiles, err = strconv.Atoi(val)
|
||||
case dt.PSM_PassSeatHeatgSts:
|
||||
state.GetPassengerSeatHeat().Level, err = strconv.Atoi(val)
|
||||
case dt.DSMC_DrvrSeatHeatgSts:
|
||||
state.GetDriverSeatHeat().Level, err = strconv.Atoi(val)
|
||||
case dt.ICC_TotMilg_ODO:
|
||||
state.GetBattery().TotalMileageOdometer, err = querystring.ConvertStringToInt(val)
|
||||
case dt.VCU_DCChrgRmngTi, dt.BMS_RmChrgTi_TrgtSoC:
|
||||
state.GetChargingMetrics().RemainingChargingTime, err = strconv.Atoi(val)
|
||||
case dt.IBS_BatteryVoltage:
|
||||
state.GetBattery().BatteryVoltage, err = strconv.ParseFloat(val, 64)
|
||||
state.GetBattery12V().IBS_BatteryVoltage = ref(state.GetBattery().BatteryVoltage)
|
||||
case dt.VCU_GearSig:
|
||||
var gear int
|
||||
gear, err = strconv.Atoi(val)
|
||||
state.GetGear().InPark = (gear <= 2)
|
||||
case dt.BMS_RmChrgTi_FullChrg:
|
||||
state.GetChargingMetrics().RemainingChargingTimeFull, err = strconv.Atoi(val)
|
||||
case dt.ECC_InsdT:
|
||||
state.GetCabinClimate().InternalTemperature, err = strconv.Atoi(val)
|
||||
case dt.ECC_RemTSetSts:
|
||||
state.GetCabinClimate().CabinTemperature, err = strconv.Atoi(val)
|
||||
case dt.TBOX_GPSHei:
|
||||
state.GetLocation().Altitude, err = strconv.ParseFloat(val, 64)
|
||||
case dt.TBOX_GPSLongi:
|
||||
state.GetLocation().Longitude, err = strconv.ParseFloat(val, 64)
|
||||
case dt.TBOX_GPSLati:
|
||||
state.GetLocation().Latitude, err = strconv.ParseFloat(val, 64)
|
||||
case dt.DBC_VERSION:
|
||||
state.DBCVersion = val
|
||||
case dt.TREX_VERSION:
|
||||
state.TRexVersion = val
|
||||
case dt.TREX_IP:
|
||||
state.IP = val
|
||||
case dt.UPDATED_AT:
|
||||
var t time.Time
|
||||
t, err = time.Parse(UPDATED_TIME_FORMAT, strings.Trim(val, "\""))
|
||||
if !t.IsZero() {
|
||||
state.UpdatedAt = ref(t)
|
||||
}
|
||||
case dt.VCU_VehSt:
|
||||
state.GetSafeState().VehicleSafeState = val == dt.VCU_VehSt_Safestate
|
||||
case dt.VCU_VcuState:
|
||||
state.GetSafeState().VCUSafeState = val == dt.VCU_VcuState_Safestate
|
||||
case dt.MCU_F_ActSafeSt:
|
||||
state.GetSafeState().MCUFrontSafeState = val == dt.MCU_F_ActSafeSt_AS0 || val == dt.MCU_F_ActSafeSt_ASC || val == dt.MCU_F_ActSafeSt_ASC_Emergency
|
||||
case dt.MCU_R_ActSafeSt:
|
||||
state.GetSafeState().MCURearSafeState = val == dt.MCU_R_ActSafeSt_AS0 || val == dt.MCU_R_ActSafeSt_ASC || val == dt.MCU_R_ActSafeSt_ASC_Emergency
|
||||
case dt.MCU_R_Decoup_State:
|
||||
state.GetSafeState().MCURearDecoupState = val == dt.MCU_R_Decoup_State_Connected
|
||||
case dt.MCU_F_CrtMod:
|
||||
state.GetSafeState().MCUFrontInverterError = val == dt.MCU_F_CrtMod_Internal_inverter_error || val == dt.MCU_F_CrtMod_Invalid
|
||||
case dt.MCU_R_CrtMod:
|
||||
state.GetSafeState().MCURearInverterError = val == dt.MCU_R_CrtMod_Internal_inverter_error || val == dt.MCU_R_CrtMod_Invalid
|
||||
case dt.ACU_Drvr_Occpt_St:
|
||||
var vi int
|
||||
vi, err = strconv.Atoi(val)
|
||||
state.DriverOccupySeatState = ref(vi)
|
||||
case dt.BCM_PwrMod:
|
||||
var vi int
|
||||
vi, err = strconv.Atoi(val)
|
||||
state.PowerMode = ref(vi)
|
||||
case dt.PWC_ChrgSts:
|
||||
var vi int
|
||||
vi, err = strconv.Atoi(val)
|
||||
state.ChargingStatus = ref(vi)
|
||||
case dt.VCU_RdyLamp:
|
||||
state.GetVehicleReadyState().IsVehicleReady, err = strconv.ParseBool(val)
|
||||
// New untested signals
|
||||
// case dt.IBS_SOCUpperTolerance:
|
||||
// var vi float64
|
||||
// vi, err = strconv.ParseFloat(val, 64)
|
||||
// state.GetExpandedSignals().IBS_SOCUpperTolerance = ref(vi)
|
||||
// case dt.IBS_SOCLowerTolerance:
|
||||
// var vi float64
|
||||
// vi, err = strconv.ParseFloat(val, 64)
|
||||
// state.GetExpandedSignals().IBS_SOCLowerTolerance = ref(vi)
|
||||
case dt.IBS_StateOfCharge:
|
||||
var vi float64
|
||||
vi, err = strconv.ParseFloat(val, 64)
|
||||
state.GetBattery12V().IBS_StateOfCharge = ref(vi)
|
||||
case dt.IBS_StateOfHealth:
|
||||
var vi int
|
||||
vi, err = strconv.Atoi(val)
|
||||
state.GetBattery12V().IBS_StateOfHealth = ref(vi)
|
||||
case dt.IBS_NominalCapacity:
|
||||
var vi int
|
||||
vi, err = strconv.Atoi(val)
|
||||
state.GetExpandedSignals().IBS_NominalCapacity = ref(vi)
|
||||
case dt.IBS_AvailableCapacity:
|
||||
var vi int
|
||||
vi, err = strconv.Atoi(val)
|
||||
state.GetExpandedSignals().IBS_AvailableCapacity = ref(vi)
|
||||
case dt.BCM_TotMilg_ODO:
|
||||
var vi float64
|
||||
vi, err = strconv.ParseFloat(val, 64)
|
||||
state.GetExpandedSignals().BCM_TotMilg_ODO = ref(vi)
|
||||
case dt.BMS_SwVersS:
|
||||
var vi int
|
||||
vi, err = strconv.Atoi(val)
|
||||
state.GetExpandedSignals().BMS_SwVersS = ref(vi)
|
||||
case dt.BMS_SwVersM:
|
||||
var vi int
|
||||
vi, err = strconv.Atoi(val)
|
||||
state.GetExpandedSignals().BMS_SwVersM = ref(vi)
|
||||
case dt.BMS_SwVers:
|
||||
var vi int
|
||||
vi, err = strconv.Atoi(val)
|
||||
state.GetExpandedSignals().BMS_SwVers = ref(vi)
|
||||
case dt.BMS_AccueDchaTotAh:
|
||||
var vi int
|
||||
vi, err = strconv.Atoi(val)
|
||||
state.GetExpandedSignals().BMS_AccueDchaTotAh = ref(vi)
|
||||
case dt.BMS_AccueChrgTotAh:
|
||||
var vi int
|
||||
vi, err = strconv.Atoi(val)
|
||||
state.GetExpandedSignals().BMS_AccueChrgTotAh = ref(vi)
|
||||
case dt.TBOX_Heading:
|
||||
state.GetLocation().Heading, err = strconv.ParseFloat(val, 64)
|
||||
case dt.PKC_KeyStsMod:
|
||||
state.GetGear().Immobilizer = val
|
||||
}
|
||||
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
func ParseCarState(state *common.CarState, key string, value interface{}) (err error) {
|
||||
found := false
|
||||
ok := false
|
||||
switch key {
|
||||
case dt.VCU_VehChrgDchgMod:
|
||||
found = true
|
||||
state.GetVCU0x260().ChargeType = value.(string)
|
||||
case dt.BMS_Bat_SoC_usable:
|
||||
found = true
|
||||
state.GetStateOfCharge().Usable, ok = value.(int)
|
||||
case dt.BMS_Bat_SOH:
|
||||
found = true
|
||||
state.GetStateOfCharge().Health, ok = value.(int)
|
||||
case dt.BCM_AP_FL_LeFrntWinPosnInfo:
|
||||
found = true
|
||||
state.GetWindows().LeftFront, ok = value.(int)
|
||||
case dt.BCM_AP_FL_RiFrntWinPosnInfo:
|
||||
found = true
|
||||
state.GetWindows().RightFront, ok = value.(int)
|
||||
case dt.BCM_AP_FL_LeReWinPosnInfo:
|
||||
found = true
|
||||
state.GetWindows().LeftRear, ok = value.(int)
|
||||
case dt.BCM_AP_FL_RiReWinPosnInfo:
|
||||
found = true
|
||||
state.GetWindows().RightRear, ok = value.(int)
|
||||
case dt.BMS_PwrBattRmngCpSOC:
|
||||
found = true
|
||||
state.GetBattery().Percent, ok = value.(int)
|
||||
case dt.BCM_ReDefrstHeatgCmd:
|
||||
found = true
|
||||
state.GetRearDefrost().On, ok = value.(bool)
|
||||
case dt.BCM_PasFrntDoorSts:
|
||||
found = true
|
||||
state.GetDoors().RightFront, ok = value.(bool)
|
||||
case dt.BCM_DrFrntDoorSts:
|
||||
found = true
|
||||
state.GetDoors().LeftFront, ok = value.(bool)
|
||||
case dt.BCM_FrntDrDoorLockSts:
|
||||
found = true
|
||||
var vv bool
|
||||
vv, ok = value.(bool)
|
||||
state.GetLocks().Driver = !vv
|
||||
case dt.BCM_CenLockSwtSts:
|
||||
found = true
|
||||
state.GetLocks().All = strconv.Itoa(value.(int)) == "2"
|
||||
case dt.BCM_RiReDoorSts:
|
||||
found = true
|
||||
state.GetDoors().RightRear, ok = value.(bool)
|
||||
case dt.BCM_LeReDoorSts:
|
||||
found = true
|
||||
state.GetDoors().LeftRear, ok = value.(bool)
|
||||
case dt.BCM_FrntHoodLidSts:
|
||||
found = true
|
||||
state.GetDoors().Hood, ok = value.(bool)
|
||||
case dt.PLGM_TrSts:
|
||||
found = true
|
||||
state.GetDoors().Trunk, ok = value.(bool)
|
||||
case dt.BCM_SunroofPosnInfo:
|
||||
found = true
|
||||
state.GetSunroof().Sunroof, ok = value.(int)
|
||||
case dt.BCM_AP_TL_LeReWinPosnInfo:
|
||||
found = true
|
||||
state.GetMiscWindows().LeftRearQuarter, ok = value.(int)
|
||||
case dt.BCM_AP_TL_RiReWinPosnInfo:
|
||||
found = true
|
||||
state.GetMiscWindows().RightRearQuarter, ok = value.(int)
|
||||
case dt.BCM_AP_RW_WinPosnInfo:
|
||||
found = true
|
||||
state.GetMiscWindows().RearWindshield, ok = value.(int)
|
||||
case dt.BMS_BattAvrgT:
|
||||
found = true
|
||||
state.GetCellTemperature().AvgBatteryTemp, ok = value.(int)
|
||||
case dt.ECC_OutdT:
|
||||
found = true
|
||||
state.GetAmbientTemperature().Temperature, ok = value.(int)
|
||||
case dt.BCM_HeatedSteerWhlSt:
|
||||
found = true
|
||||
state.GetSteeringWheelHeat().On, ok = value.(bool)
|
||||
case dt.ESP_VehSpd:
|
||||
found = true
|
||||
state.GetVehicleSpeed().Speed, ok = value.(float64)
|
||||
case dt.VCU_DrvgMilg:
|
||||
found = true
|
||||
state.GetMaxRange().MaxMiles, ok = value.(int)
|
||||
case dt.PSM_PassSeatHeatgSts:
|
||||
found = true
|
||||
state.GetPassengerSeatHeat().Level, ok = value.(int)
|
||||
case dt.DSMC_DrvrSeatHeatgSts:
|
||||
found = true
|
||||
state.GetDriverSeatHeat().Level, ok = value.(int)
|
||||
case dt.ICC_TotMilg_ODO:
|
||||
found = true
|
||||
// Seems wierd its sometimes an int, sometimes a float
|
||||
state.GetBattery().TotalMileageOdometer, ok = value.(int)
|
||||
case dt.VCU_DCChrgRmngTi, dt.BMS_RmChrgTi_TrgtSoC:
|
||||
found = true
|
||||
state.GetChargingMetrics().RemainingChargingTime, ok = value.(int)
|
||||
case dt.IBS_BatteryVoltage:
|
||||
found = true
|
||||
state.GetBattery().BatteryVoltage, ok = value.(float64)
|
||||
state.GetBattery12V().IBS_BatteryVoltage = ref(state.GetBattery().BatteryVoltage)
|
||||
case dt.VCU_GearSig:
|
||||
found = true
|
||||
var gear int
|
||||
gear, ok = value.(int)
|
||||
state.GetGear().InPark = (gear <= 2)
|
||||
case dt.BMS_RmChrgTi_FullChrg:
|
||||
found = true
|
||||
state.GetChargingMetrics().RemainingChargingTimeFull, ok = value.(int)
|
||||
case dt.ECC_InsdT:
|
||||
found = true
|
||||
state.GetCabinClimate().InternalTemperature, ok = value.(int)
|
||||
case dt.ECC_RemTSetSts:
|
||||
found = true
|
||||
state.GetCabinClimate().CabinTemperature, ok = value.(int)
|
||||
case dt.TBOX_GPSHei:
|
||||
found = true
|
||||
state.GetLocation().Altitude, ok = value.(float64)
|
||||
case dt.TBOX_GPSLongi:
|
||||
found = true
|
||||
state.GetLocation().Longitude, ok = value.(float64)
|
||||
case dt.TBOX_GPSLati:
|
||||
found = true
|
||||
state.GetLocation().Latitude, ok = value.(float64)
|
||||
case dt.DBC_VERSION:
|
||||
found = true
|
||||
state.DBCVersion = value.(string)
|
||||
case dt.TREX_VERSION:
|
||||
found = true
|
||||
state.TRexVersion = value.(string)
|
||||
case dt.TREX_IP:
|
||||
found = true
|
||||
state.IP = value.(string)
|
||||
case dt.UPDATED_AT:
|
||||
var t time.Time
|
||||
t, err = time.Parse(UPDATED_TIME_FORMAT, strings.Trim(value.(string), "\""))
|
||||
if !t.IsZero() {
|
||||
state.UpdatedAt = ref(t)
|
||||
}
|
||||
case dt.VCU_VehSt:
|
||||
found = true
|
||||
state.GetSafeState().VehicleSafeState = strconv.Itoa(value.(int)) == dt.VCU_VehSt_Safestate
|
||||
case dt.VCU_VcuState:
|
||||
found = true
|
||||
state.GetSafeState().VCUSafeState = strconv.Itoa(value.(int)) == dt.VCU_VcuState_Safestate
|
||||
case dt.MCU_F_ActSafeSt:
|
||||
found = true
|
||||
state.GetSafeState().MCUFrontSafeState = strconv.Itoa(value.(int)) == dt.MCU_F_ActSafeSt_AS0 || strconv.Itoa(value.(int)) == dt.MCU_F_ActSafeSt_ASC || strconv.Itoa(value.(int)) == dt.MCU_F_ActSafeSt_ASC_Emergency
|
||||
case dt.MCU_R_ActSafeSt:
|
||||
found = true
|
||||
state.GetSafeState().MCURearSafeState = strconv.Itoa(value.(int)) == dt.MCU_R_ActSafeSt_AS0 || strconv.Itoa(value.(int)) == dt.MCU_R_ActSafeSt_ASC || strconv.Itoa(value.(int)) == dt.MCU_R_ActSafeSt_ASC_Emergency
|
||||
case dt.MCU_R_Decoup_State:
|
||||
found = true
|
||||
state.GetSafeState().MCURearDecoupState = strconv.Itoa(value.(int)) == dt.MCU_R_Decoup_State_Connected
|
||||
case dt.MCU_F_CrtMod:
|
||||
found = true
|
||||
state.GetSafeState().MCUFrontInverterError = strconv.Itoa(value.(int)) == dt.MCU_F_CrtMod_Internal_inverter_error || strconv.Itoa(value.(int)) == dt.MCU_F_CrtMod_Invalid
|
||||
case dt.MCU_R_CrtMod:
|
||||
found = true
|
||||
state.GetSafeState().MCURearInverterError = strconv.Itoa(value.(int)) == dt.MCU_R_CrtMod_Internal_inverter_error || strconv.Itoa(value.(int)) == dt.MCU_R_CrtMod_Invalid
|
||||
case dt.ACU_Drvr_Occpt_St:
|
||||
found = true
|
||||
var vi int
|
||||
vi, ok = value.(int)
|
||||
state.DriverOccupySeatState = ref(vi)
|
||||
case dt.BCM_PwrMod:
|
||||
found = true
|
||||
var vi int
|
||||
vi, ok = value.(int)
|
||||
state.PowerMode = ref(vi)
|
||||
case dt.PWC_ChrgSts:
|
||||
found = true
|
||||
var vi int
|
||||
vi, ok = value.(int)
|
||||
state.ChargingStatus = ref(vi)
|
||||
case dt.VCU_RdyLamp:
|
||||
found = true
|
||||
state.GetVehicleReadyState().IsVehicleReady, ok = value.(bool)
|
||||
case "online":
|
||||
found = true
|
||||
state.Online, ok = value.(bool)
|
||||
case "online_hmi":
|
||||
found = true
|
||||
state.OnlineHMI, ok = value.(bool)
|
||||
// New untested signals
|
||||
// case dt.IBS_SOCUpperTolerance:
|
||||
// found = true
|
||||
// var vi float64
|
||||
// vi, ok = value.(float64)
|
||||
// state.GetExpandedSignals().IBS_SOCUpperTolerance = ref(vi)
|
||||
// case dt.IBS_SOCLowerTolerance:
|
||||
// found = true
|
||||
// var vi float64
|
||||
// vi, ok = value.(float64)
|
||||
// state.GetExpandedSignals().IBS_SOCLowerTolerance = ref(vi)
|
||||
case dt.IBS_StateOfCharge:
|
||||
found = true
|
||||
var vi float64
|
||||
vi, ok = value.(float64)
|
||||
state.GetBattery12V().IBS_StateOfCharge = ref(vi)
|
||||
case dt.IBS_StateOfHealth:
|
||||
found = true
|
||||
var vi int
|
||||
vi, ok = value.(int)
|
||||
state.GetBattery12V().IBS_StateOfHealth = ref(vi)
|
||||
case dt.IBS_NominalCapacity:
|
||||
found = true
|
||||
var vi int
|
||||
vi, ok = value.(int)
|
||||
state.GetExpandedSignals().IBS_NominalCapacity = ref(vi)
|
||||
case dt.IBS_AvailableCapacity:
|
||||
found = true
|
||||
var vi int
|
||||
vi, ok = value.(int)
|
||||
state.GetExpandedSignals().IBS_AvailableCapacity = ref(vi)
|
||||
case dt.BCM_TotMilg_ODO:
|
||||
found = true
|
||||
var vi float64
|
||||
vi, ok = value.(float64)
|
||||
state.GetExpandedSignals().BCM_TotMilg_ODO = ref(vi)
|
||||
case dt.BMS_SwVersS:
|
||||
found = true
|
||||
var vi int
|
||||
vi, ok = value.(int)
|
||||
state.GetExpandedSignals().BMS_SwVersS = ref(vi)
|
||||
case dt.BMS_SwVersM:
|
||||
found = true
|
||||
var vi int
|
||||
vi, ok = value.(int)
|
||||
state.GetExpandedSignals().BMS_SwVersM = ref(vi)
|
||||
case dt.BMS_SwVers:
|
||||
found = true
|
||||
var vi int
|
||||
vi, ok = value.(int)
|
||||
state.GetExpandedSignals().BMS_SwVers = ref(vi)
|
||||
case dt.BMS_AccueDchaTotAh:
|
||||
found = true
|
||||
var vi int
|
||||
vi, ok = value.(int)
|
||||
state.GetExpandedSignals().BMS_AccueDchaTotAh = ref(vi)
|
||||
case dt.BMS_AccueChrgTotAh:
|
||||
found = true
|
||||
var vi int
|
||||
vi, ok = value.(int)
|
||||
state.GetExpandedSignals().BMS_AccueChrgTotAh = ref(vi)
|
||||
case dt.TBOX_Heading:
|
||||
found = true
|
||||
state.GetLocation().Heading, ok = value.(float64)
|
||||
case dt.PKC_KeyStsMod:
|
||||
found = true
|
||||
state.GetGear().Immobilizer = value.(string)
|
||||
}
|
||||
if found {
|
||||
if !ok {
|
||||
err = fmt.Errorf("failed on key %s value %v", key, value)
|
||||
}
|
||||
} else {
|
||||
logger.Info().Str("key", key).Interface("value", value).Msgf("did not have parsing mode for key")
|
||||
}
|
||||
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
func ref[T any](v T) *T {
|
||||
return &v
|
||||
}
|
||||
|
||||
func IsCarOnline(clientPool redis.ClientPoolInterface, vin string) (bool, error) {
|
||||
client := clientPool.GetFromPool()
|
||||
defer client.Close()
|
||||
return redigo.Bool(
|
||||
client.Execute("SISMEMBER", redis.CarSessionsKey(), vin),
|
||||
)
|
||||
}
|
||||
|
||||
func notValue(value bool, err error) (bool, error) {
|
||||
return !value, err
|
||||
}
|
||||
99
pkg/cache/vehicle_state_multi.go
vendored
Normal file
99
pkg/cache/vehicle_state_multi.go
vendored
Normal file
@@ -0,0 +1,99 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"fiskerinc.com/modules/common"
|
||||
"fiskerinc.com/modules/logger"
|
||||
"fiskerinc.com/modules/redis"
|
||||
|
||||
redigo "github.com/gomodule/redigo/redis"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
|
||||
func GetVINListDigitalTwin(vins []string, clientPool redis.ClientPoolInterface) (digitalTwins map[string]common.CarState, errorList []error) {
|
||||
digitalTwins = make(map[string]common.CarState)
|
||||
client := clientPool.GetFromPool()
|
||||
defer client.Close()
|
||||
|
||||
batch := redis.NewRedisBatchCommands()
|
||||
for _, vin := range vins {
|
||||
batch.Add("SISMEMBER", redis.CarSessionsKey(), vin)
|
||||
batch.Add("SISMEMBER", redis.HMISessionsKey(), vin)
|
||||
batch.Add("HGETALL", redis.CarStateHashKey(vin))
|
||||
|
||||
}
|
||||
|
||||
payload, err := redigo.Values(client.ExecuteBatch(batch))
|
||||
if err != nil {
|
||||
errorList = append(errorList, err)
|
||||
return
|
||||
}
|
||||
|
||||
for index, vin := range vins {
|
||||
startPoint := index * 3
|
||||
tempTwin, err := ParsePayloadForVehicleState(payload[startPoint:startPoint+3])
|
||||
if err != nil {
|
||||
err = errors.WithMessage(err, vin)
|
||||
errorList = append(errorList, err)
|
||||
continue
|
||||
}
|
||||
digitalTwins[vin] = tempTwin
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func ParsePayloadForVehicleState(payload []interface{}) (common.CarState, error) {
|
||||
var state common.CarState
|
||||
|
||||
online, err := redigo.Bool(payload[0], nil)
|
||||
if err != nil {
|
||||
return state, err
|
||||
} else {
|
||||
state.Online = online
|
||||
}
|
||||
|
||||
online, err = redigo.Bool(payload[1], nil)
|
||||
if err != nil {
|
||||
return state, errors.WithStack(err)
|
||||
} else {
|
||||
state.OnlineHMI = online
|
||||
}
|
||||
|
||||
err = parseCarStatePayload(&state, payload[2])
|
||||
|
||||
return state, err
|
||||
}
|
||||
|
||||
func parseCarStatePayload(state *common.CarState, payload interface{}) error {
|
||||
stateValues, err := redigo.Values(payload, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(stateValues)%2 != 0 {
|
||||
return errors.New("object does not contain equal number of key value pairs")
|
||||
}
|
||||
|
||||
err = parseStateValues(state, stateValues, parseCarState)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func parseStateValues(state *common.CarState, stateValues []interface{}, parser stateParser) error {
|
||||
for i := 0; i < len(stateValues); i += 2 {
|
||||
key, okKey := stateValues[i].([]byte)
|
||||
value, okValue := stateValues[i+1].([]byte)
|
||||
|
||||
if !okKey || !okValue {
|
||||
return errors.New("cannot parse object into car state")
|
||||
}
|
||||
|
||||
err := parser(state, string(key), value)
|
||||
// log error, do not return error so we can read other properties for digital twin
|
||||
if err != nil {
|
||||
logger.Err(err).Send()
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
193
pkg/cache/vehicle_state_test.go
vendored
Normal file
193
pkg/cache/vehicle_state_test.go
vendored
Normal file
@@ -0,0 +1,193 @@
|
||||
package cache_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"fiskerinc.com/modules/cache"
|
||||
"fiskerinc.com/modules/common"
|
||||
"fiskerinc.com/modules/redis"
|
||||
"fiskerinc.com/modules/redis/tester"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestConnGetVehicleState(t *testing.T) {
|
||||
var updateTime = time.Date(2020, time.October, 3, 12, 10, 0, 0, time.UTC)
|
||||
vin := "TESTVIN123"
|
||||
redisMock := tester.NewRedisMock()
|
||||
redisPool := tester.NewMockClientPool(redisMock)
|
||||
|
||||
testCases := map[string]struct {
|
||||
sismemberResults map[string]map[string]interface{}
|
||||
hgetallResults map[string][]interface{}
|
||||
expResp common.CarState
|
||||
expErr error
|
||||
}{
|
||||
"correct": {
|
||||
sismemberResults: map[string]map[string]interface{}{
|
||||
redis.CarSessionsKey(): {
|
||||
vin: int64(1),
|
||||
},
|
||||
redis.HMISessionsKey(): {
|
||||
vin: int64(1),
|
||||
},
|
||||
},
|
||||
hgetallResults: map[string][]interface{}{
|
||||
fmt.Sprintf("car:%s:state", vin): {
|
||||
[]byte("DSMC_DrvrSeatHeatgSts"), []byte("2"),
|
||||
[]byte("ESP_VehSpd"), []byte("123.4"),
|
||||
[]byte("BMS_RmChrgTi_TrgtSoC"), []byte("5000"),
|
||||
[]byte("BMS_RmChrgTi_FullChrg"), []byte("6000"),
|
||||
[]byte("VCU_VehChrgDchgMod"), []byte("DC_charging"),
|
||||
[]byte("BCM_AP_FL_LeReWinPosnInfo"), []byte("30"),
|
||||
[]byte("BCM_ReDefrstHeatgCmd"), []byte("1"),
|
||||
[]byte("BCM_FrntHoodLidSts"), []byte("1"),
|
||||
[]byte("BMS_Bat_SOH"), []byte("20"),
|
||||
[]byte("ICC_TotMilg_ODO"), []byte("2345"),
|
||||
[]byte("IBS_BatteryVoltage"), []byte("12.3"),
|
||||
[]byte("TBOX_GPSHei"), []byte("16"),
|
||||
[]byte("ECC_OutdT"), []byte("30"),
|
||||
[]byte("PSM_PassSeatHeatgSts"), []byte("4"),
|
||||
[]byte("TBOX_GPSLati"), []byte("35.831"),
|
||||
[]byte("BCM_PasFrntDoorSts"), []byte("0"),
|
||||
[]byte("BCM_CenLockSwtSts"), []byte("3"),
|
||||
[]byte("BCM_RiReDoorSts"), []byte("1"),
|
||||
[]byte("BCM_LeReDoorSts"), []byte("1"),
|
||||
[]byte("VCU_DrvgMilg"), []byte("1234"),
|
||||
[]byte("TBOX_GPSLongi"), []byte("-120.398"),
|
||||
[]byte("BCM_AP_FL_RiReWinPosnInfo"), []byte("40"),
|
||||
[]byte("BCM_FrntDrDoorLockSts"), []byte("1"),
|
||||
[]byte("BCM_DrFrntDoorSts"), []byte("0"),
|
||||
[]byte("BCM_AP_TL_LeReWinPosnInfo"), []byte("60"),
|
||||
[]byte("ECC_RemTSetSts"), []byte("120"),
|
||||
[]byte("BCM_AP_FL_RiFrntWinPosnInfo"), []byte("20"),
|
||||
[]byte("BMS_PwrBattRmngCpSOC"), []byte("50"),
|
||||
[]byte("BCM_AP_TL_RiReWinPosnInfo"), []byte("70"),
|
||||
[]byte("BCM_HeatedSteerWhlSt"), []byte("1"),
|
||||
[]byte("BCM_AP_RW_WinPosnInfo"), []byte("80"),
|
||||
[]byte("ECC_InsdT"), []byte("30"),
|
||||
[]byte("updated"), []byte(`"2020-10-03T12:10:00Z"`),
|
||||
[]byte("BMS_Bat_SoC_usable"), []byte("10"),
|
||||
[]byte("BCM_AP_FL_LeFrntWinPosnInfo"), []byte("10"),
|
||||
[]byte("BCM_SunroofPosnInfo"), []byte("50"),
|
||||
[]byte("BMS_BattAvrgT"), []byte("90"),
|
||||
[]byte("dbc_version"), []byte("hash"),
|
||||
[]byte("VCU_VehSt"), []byte("12"),
|
||||
[]byte("VCU_VcuState"), []byte("18"),
|
||||
[]byte("MCU_F_ActSafeSt"), []byte("4"),
|
||||
[]byte("MCU_R_ActSafeSt"), []byte("2"),
|
||||
[]byte("MCU_R_Decoup_State"), []byte("3"),
|
||||
[]byte("MCU_F_CrtMod"), []byte("7"),
|
||||
[]byte("MCU_R_CrtMod"), []byte("8"),
|
||||
[]byte("VCU_RdyLamp"), []byte("1"),
|
||||
},
|
||||
},
|
||||
expResp: common.CarState{
|
||||
Online: true,
|
||||
OnlineHMI: true,
|
||||
VehicleSpeed: &common.VehicleSpeed{
|
||||
Speed: 123.4,
|
||||
},
|
||||
Battery: &common.Battery{
|
||||
Percent: 50,
|
||||
TotalMileageOdometer: 2345,
|
||||
BatteryVoltage: 12.3,
|
||||
},
|
||||
MaxRange: &common.MaxRange{
|
||||
MaxMiles: 1234,
|
||||
},
|
||||
Doors: &common.Doors{
|
||||
Hood: true,
|
||||
LeftFront: false,
|
||||
LeftRear: true,
|
||||
RightFront: false,
|
||||
RightRear: true,
|
||||
},
|
||||
Location: &common.Location{
|
||||
Altitude: 16,
|
||||
Longitude: -120.398,
|
||||
Latitude: 35.831,
|
||||
},
|
||||
Locks: &common.Locks{
|
||||
Driver: false,
|
||||
All: false,
|
||||
},
|
||||
Windows: &common.Windows{
|
||||
LeftFront: 10,
|
||||
LeftRear: 30,
|
||||
RightFront: 20,
|
||||
RightRear: 40,
|
||||
},
|
||||
MiscWindows: &common.MiscWindows{
|
||||
LeftRearQuarter: 60,
|
||||
RightRearQuarter: 70,
|
||||
RearWindshield: 80,
|
||||
},
|
||||
Sunroof: &common.Sunroof{
|
||||
Sunroof: 50,
|
||||
},
|
||||
CabinClimate: &common.CabinClimate{
|
||||
CabinTemperature: 120,
|
||||
InternalTemperature: 30,
|
||||
},
|
||||
RearDefrost: &common.RearDefrost{
|
||||
On: true,
|
||||
},
|
||||
DriverSeatHeat: &common.DriverSeatHeat{
|
||||
Level: 2,
|
||||
},
|
||||
PassengerSeatHeat: &common.PassengerSeatHeat{
|
||||
Level: 4,
|
||||
},
|
||||
CellTemperature: &common.CellTemperature{
|
||||
AvgBatteryTemp: 90,
|
||||
},
|
||||
ChargingMetrics: &common.VCUChargingMetrics{
|
||||
RemainingChargingTime: 5000,
|
||||
RemainingChargingTimeFull: 6000,
|
||||
},
|
||||
SteeringWheelHeat: &common.SteeringWheelHeat{
|
||||
On: true,
|
||||
},
|
||||
AmbientTemperature: &common.AmbientTemperature{
|
||||
Temperature: 30,
|
||||
},
|
||||
VCU0x260: &common.VCU0x260Descriptor{
|
||||
ChargeType: "DC_charging",
|
||||
},
|
||||
StateOfCharge: &common.StateOfCharge{
|
||||
Usable: 10,
|
||||
Health: 20,
|
||||
},
|
||||
DBCVersion: "hash",
|
||||
UpdatedAt: &updateTime,
|
||||
SafeState: &common.SafeState{
|
||||
VehicleSafeState: false,
|
||||
VCUSafeState: true,
|
||||
MCUFrontSafeState: false,
|
||||
MCURearSafeState: true,
|
||||
MCURearDecoupState: false,
|
||||
MCUFrontInverterError: true,
|
||||
MCURearInverterError: false,
|
||||
},
|
||||
VehicleReadyState: &common.VehicleReadyState{
|
||||
IsVehicleReady: true,
|
||||
},
|
||||
},
|
||||
expErr: nil,
|
||||
},
|
||||
}
|
||||
|
||||
parser := cache.NewVehicleState(redisPool)
|
||||
|
||||
for tName, tt := range testCases {
|
||||
t.Run(tName, func(t *testing.T) {
|
||||
redisMock.SISMEMBEResults = tt.sismemberResults
|
||||
redisMock.HGETALLResults = tt.hgetallResults
|
||||
state, err := parser.Get(vin)
|
||||
assert.Equal(t, tt.expErr, err)
|
||||
assert.Equal(t, tt.expResp, state)
|
||||
})
|
||||
}
|
||||
}
|
||||
124
pkg/cache/vehicles.go
vendored
Normal file
124
pkg/cache/vehicles.go
vendored
Normal file
@@ -0,0 +1,124 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fiskerinc.com/modules/common"
|
||||
orm "fiskerinc.com/modules/db/queries"
|
||||
"fmt"
|
||||
"github.com/ReneKroon/ttlcache/v2"
|
||||
"github.com/pkg/errors"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrCacheNotInitialized = errors.New("cache is not initialized")
|
||||
)
|
||||
|
||||
type VehicleCacher interface {
|
||||
Set(key VehiclesTTLParams, value *VehiclesTTLResult) error
|
||||
Get(key VehiclesTTLParams) (*VehiclesTTLResult, error)
|
||||
}
|
||||
|
||||
type VehiclesCache struct {
|
||||
duration time.Duration
|
||||
limit int
|
||||
cache *ttlcache.Cache
|
||||
onceCache *sync.Once
|
||||
}
|
||||
|
||||
func (c *VehiclesCache) Duration() time.Duration {
|
||||
return c.duration
|
||||
}
|
||||
|
||||
func (c *VehiclesCache) Limit() int {
|
||||
return c.limit
|
||||
}
|
||||
|
||||
func (c *VehiclesCache) Once() *sync.Once {
|
||||
return c.onceCache
|
||||
}
|
||||
|
||||
func (c *VehiclesCache) SetCache(cache *ttlcache.Cache) {
|
||||
c.cache = cache
|
||||
}
|
||||
|
||||
func (c *VehiclesCache) Cache() *ttlcache.Cache {
|
||||
return c.cache
|
||||
}
|
||||
|
||||
type VehiclesTTLParams struct {
|
||||
Options orm.PageQueryOptions `json:"options"`
|
||||
CarOnlineFilter *common.CarOnlineFilter `json:"car_online_filter"`
|
||||
Search string `json:"search"`
|
||||
}
|
||||
|
||||
type VehiclesTTLResult struct {
|
||||
Data []common.Car `json:"data"`
|
||||
Total int `json:"total"`
|
||||
}
|
||||
|
||||
func NewVehiclesCache(duration time.Duration, limit int) (*VehiclesCache, error) {
|
||||
c := &VehiclesCache{
|
||||
duration: duration,
|
||||
limit: limit,
|
||||
onceCache: &sync.Once{},
|
||||
}
|
||||
|
||||
if cache := logCache(c); cache == nil {
|
||||
return nil, ErrCacheNotInitialized
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
func (c *VehiclesCache) Set(key VehiclesTTLParams, value *VehiclesTTLResult) error {
|
||||
if cache := logCache(c); cache == nil {
|
||||
return ErrCacheNotInitialized
|
||||
}
|
||||
|
||||
keyBts, err := json.Marshal(key)
|
||||
if err != nil {
|
||||
return errors.WithMessagef(err, "failed to marshal key")
|
||||
}
|
||||
|
||||
return c.cache.Set(string(keyBts), value)
|
||||
}
|
||||
|
||||
func (c *VehiclesCache) Get(key VehiclesTTLParams) (*VehiclesTTLResult, error) {
|
||||
if cache := logCache(c); cache == nil {
|
||||
return nil, ErrCacheNotInitialized
|
||||
}
|
||||
|
||||
keyBts, err := json.Marshal(key)
|
||||
if err != nil {
|
||||
return nil, errors.WithMessagef(err, "failed to marshal key")
|
||||
}
|
||||
|
||||
value, err := c.cache.Get(string(keyBts))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get value from cache: %w", err)
|
||||
}
|
||||
|
||||
return value.(*VehiclesTTLResult), nil
|
||||
}
|
||||
|
||||
type Cacher interface {
|
||||
Duration() time.Duration
|
||||
Limit() int
|
||||
Once() *sync.Once
|
||||
Cache() *ttlcache.Cache
|
||||
SetCache(*ttlcache.Cache)
|
||||
}
|
||||
|
||||
func logCache(cacher Cacher) *ttlcache.Cache {
|
||||
cacher.Once().Do(func() {
|
||||
if cacher.Cache() == nil {
|
||||
cache := ttlcache.NewCache()
|
||||
cache.SetTTL(cacher.Duration())
|
||||
cache.SetCacheSizeLimit(cacher.Limit())
|
||||
cacher.SetCache(cache)
|
||||
}
|
||||
})
|
||||
|
||||
return cacher.Cache()
|
||||
}
|
||||
58
pkg/cache/verify.go
vendored
Normal file
58
pkg/cache/verify.go
vendored
Normal file
@@ -0,0 +1,58 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"fiskerinc.com/modules/common"
|
||||
"fiskerinc.com/modules/db/queries"
|
||||
"fiskerinc.com/modules/logger"
|
||||
"fiskerinc.com/modules/redis"
|
||||
|
||||
redigo "github.com/gomodule/redigo/redis"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// VerifyCarToDriver checks cache and DB for car to driver relationship.
|
||||
// If relationship exists and not in cache, will cache value.
|
||||
//
|
||||
// car:<VIN>:driver:<DRIVER_ID>
|
||||
func VerifyCarToDriver(clientPool redis.ClientPoolInterface, db queries.CarsInterface, vin string, driverID string) (bool, error) {
|
||||
key := redis.CarToDriverKey(vin, driverID)
|
||||
|
||||
ok, err := redisCheckGet(clientPool, key)
|
||||
if err != nil {
|
||||
return ok, err
|
||||
}
|
||||
|
||||
if ok {
|
||||
return ok, err
|
||||
}
|
||||
|
||||
carToDrivers, err := db.SelectCarToDriver(&common.CarToDriver{VIN: vin, DriverID: driverID})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
verified := len(carToDrivers) == 1
|
||||
redisPlaceDriverCache(clientPool, key, verified)
|
||||
|
||||
return verified, err
|
||||
}
|
||||
|
||||
func redisCheckGet(clientPool redis.ClientPoolInterface, key string) (bool, error) {
|
||||
client := clientPool.GetFromPool()
|
||||
defer client.Close()
|
||||
ok, err := redigo.Bool(client.Execute("GET", key))
|
||||
if err != nil && !errors.Is(err, redigo.ErrNil) {
|
||||
logger.Warn().Err(err).Send()
|
||||
return ok, err
|
||||
}
|
||||
return ok, nil
|
||||
}
|
||||
|
||||
func redisPlaceDriverCache(clientPool redis.ClientPoolInterface, key string, verified bool) (err error) {
|
||||
client := clientPool.GetFromPool()
|
||||
defer client.Close()
|
||||
batch := redis.NewRedisBatchCommands()
|
||||
batch.Add("SET", key, verified)
|
||||
batch.Add("EXPIRE", key, redisObjectExpire)
|
||||
_, err = client.ExecuteBatch(batch)
|
||||
return
|
||||
}
|
||||
55
pkg/cache/verify_test.go
vendored
Normal file
55
pkg/cache/verify_test.go
vendored
Normal file
@@ -0,0 +1,55 @@
|
||||
package cache_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"fiskerinc.com/modules/cache"
|
||||
"fiskerinc.com/modules/common"
|
||||
"fiskerinc.com/modules/db/queries/mocks"
|
||||
"fiskerinc.com/modules/redis"
|
||||
"fiskerinc.com/modules/redis/tester"
|
||||
"fiskerinc.com/modules/testhelper"
|
||||
|
||||
redigo "github.com/gomodule/redigo/redis"
|
||||
)
|
||||
|
||||
type mockRedisCacheDriverToCars struct {
|
||||
redis.Connection
|
||||
}
|
||||
|
||||
func (c *mockRedisCacheDriverToCars) Execute(command ...interface{}) (interface{}, error) {
|
||||
return []byte("1"), nil
|
||||
}
|
||||
|
||||
type mockRedisEmptyCacheDriverToCars struct {
|
||||
redis.Connection
|
||||
}
|
||||
|
||||
func (c *mockRedisEmptyCacheDriverToCars) Execute(command ...interface{}) (interface{}, error) {
|
||||
return nil, redigo.ErrNil
|
||||
}
|
||||
|
||||
func (c *mockRedisEmptyCacheDriverToCars) ExecuteBatch(batch *redis.RedisBatchCommands) (interface{}, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func TestVerifyCarToDriver(t *testing.T) {
|
||||
setupRedisMock()
|
||||
mockDB := &mocks.MockCars{
|
||||
SelectCarsForDrivers: []common.CarToDriver{{}},
|
||||
}
|
||||
|
||||
mockRedis = &mockRedisCacheDriverToCars{}
|
||||
redisPool := tester.NewMockClientPool(mockRedis)
|
||||
_, err := cache.VerifyCarToDriver(redisPool, mockDB, "VALID_VIN", "VALID_ID")
|
||||
if err != nil {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "TestRetrieveAndCacheDriverIDs", nil, err)
|
||||
}
|
||||
|
||||
mockRedis = &mockRedisEmptyCacheDriverToCars{}
|
||||
redisPool = tester.NewMockClientPool(mockRedis)
|
||||
_, err = cache.VerifyCarToDriver(redisPool, mockDB, "VALID_VIN", "VALID_ID")
|
||||
if err != nil {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "TestRetrieveAndCacheDriverIDs", nil, err)
|
||||
}
|
||||
}
|
||||
60
pkg/cache/vins.go
vendored
Normal file
60
pkg/cache/vins.go
vendored
Normal file
@@ -0,0 +1,60 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"fiskerinc.com/modules/common"
|
||||
"fiskerinc.com/modules/db/queries"
|
||||
"fiskerinc.com/modules/logger"
|
||||
"fiskerinc.com/modules/redis"
|
||||
)
|
||||
|
||||
// RetrieveVINs retrieves VINs from redis or from DB based on driver ID and proceeds to cache VINs
|
||||
// redis keys:
|
||||
//
|
||||
// driver:<ID>:cars
|
||||
func RetrieveVINs(client redis.Client, db queries.CarsInterface, id string) ([]string, error) {
|
||||
var vins []string
|
||||
driverVINsKey := redis.DriverToVINsKey(id)
|
||||
|
||||
// retrieve VINs from redis
|
||||
err := client.GetCache(driverVINsKey, &vins, 0)
|
||||
if err != nil && !errors.Is(err, redis.ErrNilObject) {
|
||||
logger.Warn().Err(err).Send()
|
||||
} else if len(vins) > 0 {
|
||||
return vins, nil
|
||||
}
|
||||
|
||||
// if VINs not present in redis perform DB lookup
|
||||
var vehicles []common.CarToDriver
|
||||
vehicles, err = db.GetCarsForDriver(id)
|
||||
if err != nil {
|
||||
return vins, err
|
||||
}
|
||||
|
||||
for _, vehicle := range vehicles {
|
||||
vins = append(vins, vehicle.VIN)
|
||||
}
|
||||
|
||||
// cache drivers vehicles
|
||||
err = client.SetCache(driverVINsKey, vins, redisObjectExpire)
|
||||
if err != nil {
|
||||
return vins, err
|
||||
}
|
||||
|
||||
return vins, nil
|
||||
}
|
||||
|
||||
func RetrieveVINsAsSet(client redis.Client, db queries.CarsInterface, id string) (map[string]struct{}, error) {
|
||||
vins, err := RetrieveVINs(client, db, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var vinsSet = make(map[string]struct{})
|
||||
for _, vin := range vins {
|
||||
vinsSet[vin] = struct{}{}
|
||||
}
|
||||
|
||||
return vinsSet, nil
|
||||
}
|
||||
71
pkg/cache/vins_test.go
vendored
Normal file
71
pkg/cache/vins_test.go
vendored
Normal file
@@ -0,0 +1,71 @@
|
||||
package cache_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"fiskerinc.com/modules/cache"
|
||||
"fiskerinc.com/modules/redis"
|
||||
"fiskerinc.com/modules/testhelper"
|
||||
)
|
||||
|
||||
type mockRedisCacheVINs struct {
|
||||
redis.Connection
|
||||
}
|
||||
|
||||
func (c *mockRedisCacheVINs) GetCache(id string, data interface{}, expire int) error {
|
||||
vins := []string{"TESTVIN123", "TESTVIN456"}
|
||||
|
||||
dataBytes, err := json.Marshal(vins)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = json.Unmarshal(dataBytes, data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type mockRedisEmptyCacheVINs struct {
|
||||
redis.Connection
|
||||
}
|
||||
|
||||
func (c *mockRedisEmptyCacheVINs) GetCache(id string, data interface{}, expire int) error {
|
||||
vins := []string{}
|
||||
|
||||
dataBytes, err := json.Marshal(vins)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = json.Unmarshal(dataBytes, data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *mockRedisEmptyCacheVINs) SetCache(id string, data interface{}, expire int) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestRetrieveAndCacheVINs(t *testing.T) {
|
||||
setupRedisMock()
|
||||
setupDBMock()
|
||||
|
||||
mockRedis = &mockRedisCacheVINs{}
|
||||
_, err := cache.RetrieveVINs(mockRedis, mockDB, "VALID_ID")
|
||||
if err != nil {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "TestRetrieveAndCacheDriverIDs", nil, err)
|
||||
}
|
||||
|
||||
mockRedis = &mockRedisEmptyCacheVINs{}
|
||||
_, err = cache.RetrieveVINs(mockRedis, mockDB, "VALID_ID")
|
||||
if err != nil {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "TestRetrieveAndCacheDriverIDs", nil, err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user