src.dualinventive.com/go/redis-proxy/internal/redis/process_msg.go

225 lines
5.4 KiB
Go

package redis
import (
"encoding/json"
"errors"
"fmt"
"strconv"
"strings"
"github.com/gomodule/redigo/redis"
"src.dualinventive.com/go/dinet/ditime"
"src.dualinventive.com/go/dinet/rpc"
"src.dualinventive.com/go/lib/dilog"
)
type redisResultValueItem struct {
rpc.ResultValueItem
LastUpdate ditime.Time `json:"last_update"`
}
// marshalResult converts the result object to JSON for redis. It adds last_update to the JSON string
func marshalResult(result interface{}) ([]byte, error) {
var values interface{}
switch res := result.(type) {
case map[string]interface{}:
res["last_update"] = ditime.Now()
values = res
case rpc.ResultValueItem:
nRes := redisResultValueItem{res, ditime.Now()}
values = nRes
default:
return nil, errors.New("unknown marshal result type")
}
return json.Marshal(values)
}
// processMsgResult converts the result to JSON and writes it to redis. Id is used for key calculation
// the caller can use e.g sensor item uid: sensor:10:data.
func (r *Redis) processMsgResult(logger dilog.Logger, key, field string, result interface{}) error {
value, err := marshalResult(result)
if err != nil {
return fmt.Errorf("json marshal failed: %v", err)
}
err = r.write(logger, key, field, value)
if err != nil {
return fmt.Errorf("redis write failed: %v", err)
}
return nil
}
// processGeneralClassMethod process all messages that don't have a specific result object
func (r *Redis) processGeneralClassMethod(logger dilog.Logger, m *rpc.Msg, valueItems []map[string]interface{}) error {
newUIDFields := make([]string, len(valueItems))
key := keyName(m)
if key == "" {
logger.Warning("processGeneralClassMethod keyName is empty")
return nil
}
logger = logger.WithField("key", key)
for i, result := range valueItems {
var time ditime.Time
if _, ok := result["time"].(ditime.Time); !ok {
// Add server time when not existing in the message
time = m.Time
result["time"] = time
}
var id uint16
// Add id if exist
if uid, ok := result["uid"]; ok {
if fuid, ok := uid.(float64); ok {
id = uint16(fuid)
}
}
if rid, ok := result["id"]; ok {
if fid, ok := rid.(float64); ok {
id = uint16(fid)
}
}
newUIDFields[i] = strconv.Itoa(int(id))
field := fieldName(m, id)
if !r.IsNew(logger, key, field, time) {
logger.WithFields(dilog.Fields{
"field": field,
"time": time,
}).Debug("not new")
continue
}
err := r.processMsgResult(logger, key, field, result)
if err != nil {
return err
}
r.UpdateCache(key, field, time)
}
if err := r.removeOldInfoFields(m, newUIDFields); err != nil {
logger.WithError(err).Error("error removing info fields")
}
return nil
}
// processSensorData process all sensor data messages
func (r *Redis) processSensorData(logger dilog.Logger, m *rpc.Msg, result []rpc.ResultValueItem) error {
key := keyName(m)
if key == "" {
logger.Warning("processSensorData keyName is empty")
return nil
}
logger = logger.WithField("key", key)
for _, sensorItem := range result {
field := fieldName(m, sensorItem.UID)
// Should not happen, but just in case
if sensorItem.Time == 0 {
sensorItem.Time = m.Time
}
if !r.IsNew(logger, key, field, sensorItem.Time) {
logger.WithFields(dilog.Fields{
"field": field,
"time": sensorItem.Time,
}).Debug("not new")
continue
}
if sensorItem.UID == rpc.SensorGPS.UID() {
r.processGPSData(logger, m, sensorItem)
}
err := r.processMsgResult(logger, key, field, sensorItem)
if err != nil {
return err
}
r.UpdateCache(key, field, sensorItem.Time)
}
return nil
}
// processGPSData grabs the GPS data and send it to redis
func (r *Redis) processGPSData(logger dilog.Logger, m *rpc.Msg, sensorItem rpc.ResultValueItem) {
// Grab GPS
rawValue, ok := sensorItem.Value.(*json.RawMessage)
if ok {
gpsData := rpc.GPSSensorData{}
if err := json.Unmarshal(*rawValue, &gpsData); err != nil {
logger.WithField("data", *rawValue).Error("Invalid GPS data")
} else if err := r.writeSensorGPS(m, gpsData); err != nil {
logger.WithError(err).Error("cannot writeGPS data")
}
}
}
func (r *Redis) removeOldInfoFields(m *rpc.Msg, uids []string) error {
// Skip non reply messages
if m.Type != rpc.MsgTypeReply {
return nil
}
// Skip non info messages
if m.Method() != "info" {
return nil
}
class := m.Class()
hKey := keyName(m)
if hKey == "" {
return fmt.Errorf("keyName is empty")
}
c, err := r.getConn()
if err != nil {
return err
}
defer func() {
closeerr := c.Close()
// we are closing the connection so it is too late for errors.
_ = closeerr
}()
keys, err := redis.Strings(c.Do("HKEYS", hKey))
if err != nil {
return err
}
rKeys := make([]interface{}, 0, len(uids)+1)
rKeys = append(rKeys, hKey)
for _, k := range keys {
if removeInfoField(uids, class, k) {
// Append missing key
rKeys = append(rKeys, k)
}
}
if len(rKeys) > 1 {
// Remove missing keys
_, err = c.Do("HDEL", rKeys...)
}
return err
}
// removeInfoField returns true when the given key is an info field of class 'class' and contains none of the given ids
func removeInfoField(uids []string, class rpc.Class, key string) bool {
p := strings.SplitN(key, ":", 3)
// Skip keys that don't contains id's
if len(p) != 3 {
return false
}
// Skip non info keys
if p[0] != string(class) || p[2] != "info" {
return false
}
for _, i := range uids {
if i == p[1] {
return false
}
}
return true
}