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 }