diff --git a/apis/replicator.go b/apis/replicator.go index 01af6d8c0..327696f57 100644 --- a/apis/replicator.go +++ b/apis/replicator.go @@ -626,7 +626,7 @@ func (rplSv1 *ReplicatorSv1) SetActionProfile(ctx *context.Context, sp *engine.A } func (rplSv1 *ReplicatorSv1) RemoveRateProfile(ctx *context.Context, args *utils.TenantIDWithAPIOpts, reply *string) (err error) { - if err = rplSv1.dm.DataDB().RemoveRateProfileDrv(ctx, args.Tenant, args.ID); err != nil { + if err = rplSv1.dm.DataDB().RemoveRateProfileDrv(ctx, args.Tenant, args.ID, []string{}); err != nil { return } if err = rplSv1.v1.CallCache(ctx, utils.IfaceAsString(args.APIOpts[utils.MetaCache]), diff --git a/data/conf/samples/rates_mysql/cgrates.json b/data/conf/samples/rates_mysql/cgrates.json index 9fac54edb..7de5b8e1b 100644 --- a/data/conf/samples/rates_mysql/cgrates.json +++ b/data/conf/samples/rates_mysql/cgrates.json @@ -2,6 +2,10 @@ // CGRateS Configuration file // will be used in apis/attributes_it_test.go + "general": { + "node_id": "id", // identifier of this instance in the cluster, if empty it will be autogenerated + "log_level": 7, + }, "data_db": { // database used to store runtime data (eg: accounts, cdr stats) "db_type": "redis", // data_db type: diff --git a/engine/datadbmock.go b/engine/datadbmock.go index 3f3a28612..2e1843e2a 100644 --- a/engine/datadbmock.go +++ b/engine/datadbmock.go @@ -24,7 +24,7 @@ import ( ) type DataDBMock struct { - RemoveRateProfileDrvF func(ctx *context.Context, str1 string, str2 string) error + RemoveRateProfileDrvF func(ctx *context.Context, str1 string, str2 string, rateIDs []string) error SetRateProfileDrvF func(*context.Context, *utils.RateProfile) error GetRateProfileDrvF func(*context.Context, string, string) (*utils.RateProfile, error) GetKeysForPrefixF func(*context.Context, string) ([]string, error) @@ -382,9 +382,9 @@ func (dbM *DataDBMock) SetRateProfileDrv(ctx *context.Context, rt *utils.RatePro return utils.ErrNotImplemented } -func (dbM *DataDBMock) RemoveRateProfileDrv(ctx *context.Context, str1 string, str2 string) error { +func (dbM *DataDBMock) RemoveRateProfileDrv(ctx *context.Context, str1 string, str2 string, rateIDs []string) error { if dbM.RemoveRateProfileDrvF != nil { - return dbM.RemoveRateProfileDrvF(ctx, str1, str2) + return dbM.RemoveRateProfileDrvF(ctx, str1, str2, []string{}) } return utils.ErrNotImplemented } diff --git a/engine/datamanager.go b/engine/datamanager.go index 9b1e60d55..55acae5bc 100644 --- a/engine/datamanager.go +++ b/engine/datamanager.go @@ -2028,7 +2028,7 @@ func (dm *DataManager) RemoveRateProfile(ctx *context.Context, tenant, id string if err != nil && err != utils.ErrNotFound { return } - if err = dm.DataDB().RemoveRateProfileDrv(ctx, tenant, id); err != nil { + if err = dm.DataDB().RemoveRateProfileDrv(ctx, tenant, id, []string{}); err != nil { return } if oldRpp == nil { @@ -2103,8 +2103,8 @@ func (dm *DataManager) RemoveRateProfileRates(ctx *context.Context, tenant, id s delete(oldRpp.Rates, rateID) } } - if err = dm.DataDB().SetRateProfileDrv(ctx, oldRpp); err != nil { - return err + if err = dm.DataDB().RemoveRateProfileDrv(ctx, tenant, id, rateIDs); err != nil { + return } if itm := config.CgrConfig().DataDbCfg().Items[utils.MetaRateProfiles]; itm.Replicate { diff --git a/engine/storage_interface.go b/engine/storage_interface.go index cb11233c9..1c8dbf607 100644 --- a/engine/storage_interface.go +++ b/engine/storage_interface.go @@ -95,7 +95,7 @@ type DataDB interface { RemoveDispatcherHostDrv(*context.Context, string, string) error GetRateProfileDrv(*context.Context, string, string) (*utils.RateProfile, error) SetRateProfileDrv(*context.Context, *utils.RateProfile) error - RemoveRateProfileDrv(*context.Context, string, string) error + RemoveRateProfileDrv(*context.Context, string, string, []string) error GetActionProfileDrv(*context.Context, string, string) (*ActionProfile, error) SetActionProfileDrv(*context.Context, *ActionProfile) error RemoveActionProfileDrv(*context.Context, string, string) error diff --git a/engine/storage_internal_datadb.go b/engine/storage_internal_datadb.go index f1dde3f2c..5a298abae 100644 --- a/engine/storage_internal_datadb.go +++ b/engine/storage_internal_datadb.go @@ -485,7 +485,7 @@ func (iDB *InternalDB) SetRateProfileDrv(_ *context.Context, rpp *utils.RateProf return } -func (iDB *InternalDB) RemoveRateProfileDrv(_ *context.Context, tenant, id string) (err error) { +func (iDB *InternalDB) RemoveRateProfileDrv(_ *context.Context, tenant, id string, rateIDs []string) (err error) { iDB.db.Remove(utils.CacheRateProfiles, utils.ConcatenatedKey(tenant, id), true, utils.NonTransactional) return diff --git a/engine/storage_mongo_datadb.go b/engine/storage_mongo_datadb.go index dbb4236fe..7c7a875c6 100644 --- a/engine/storage_mongo_datadb.go +++ b/engine/storage_mongo_datadb.go @@ -1228,7 +1228,7 @@ func (ms *MongoStorage) SetRateProfileDrv(ctx *context.Context, rpp *utils.RateP }) } -func (ms *MongoStorage) RemoveRateProfileDrv(ctx *context.Context, tenant, id string) (err error) { +func (ms *MongoStorage) RemoveRateProfileDrv(ctx *context.Context, tenant, id string, rateIDs []string) (err error) { return ms.query(ctx, func(sctx mongo.SessionContext) (err error) { dr, err := ms.getCol(ColRpp).DeleteOne(sctx, bson.M{"tenant": tenant, "id": id}) if dr.DeletedCount == 0 { diff --git a/engine/storage_redis.go b/engine/storage_redis.go index 86d26e8be..8287a3608 100644 --- a/engine/storage_redis.go +++ b/engine/storage_redis.go @@ -64,6 +64,7 @@ const ( redisHGET = "HGET" redisRENAME = "RENAME" redisHMSET = "HMSET" + redisHSET = "HSET" redisLoadError = "Redis is loading the dataset in memory" RedisLimit = 524287 // https://github.com/StackExchange/StackExchange.Redis/issues/201#issuecomment-98639005 @@ -727,6 +728,7 @@ func (rs *RedisStorage) RemoveLoadIDsDrv() (err error) { return rs.Cmd(nil, redisDEL, utils.LoadIDs) } +/* func (rs *RedisStorage) GetRateProfileDrv(ctx *context.Context, tenant, id string) (rpp *utils.RateProfile, err error) { var values []byte if err = rs.Cmd(&values, redisGET, utils.RateProfilePrefix+utils.ConcatenatedKey(tenant, id)); err != nil { @@ -739,6 +741,7 @@ func (rs *RedisStorage) GetRateProfileDrv(ctx *context.Context, tenant, id strin return } + func (rs *RedisStorage) SetRateProfileDrv(ctx *context.Context, rpp *utils.RateProfile) (err error) { var result []byte if result, err = rs.ms.Marshal(rpp); err != nil { @@ -746,8 +749,40 @@ func (rs *RedisStorage) SetRateProfileDrv(ctx *context.Context, rpp *utils.RateP } return rs.Cmd(nil, redisSET, utils.RateProfilePrefix+utils.ConcatenatedKey(rpp.Tenant, rpp.ID), string(result)) } +*/ -func (rs *RedisStorage) RemoveRateProfileDrv(ctx *context.Context, tenant, id string) (err error) { +func (rs *RedisStorage) SetRateProfileDrv(ctx *context.Context, rpp *utils.RateProfile) (err error) { + rpMap, err := rpp.AsDataDBMap() + if err != nil { + return + } + for key, val := range rpMap { + if err = rs.FlatCmd(nil, redisHSET, utils.RateProfilePrefix+utils.ConcatenatedKey(rpp.Tenant, rpp.ID), key, val); err != nil { + break + } + } + return +} + +func (rs *RedisStorage) GetRateProfileDrv(ctx *context.Context, tenant, id string) (rpp *utils.RateProfile, err error) { + mapRP := make(map[string]interface{}) + if err = rs.Cmd(&mapRP, redisHGETALL, utils.RateProfilePrefix+utils.ConcatenatedKey(tenant, id)); err != nil { + return + } else if len(mapRP) == 0 { + err = utils.ErrNotFound + return + } + rpp, err = utils.NewRateProfileFromMapDataDBMap(tenant, id, mapRP) + return +} + +func (rs *RedisStorage) RemoveRateProfileDrv(ctx *context.Context, tenant, id string, rateIDs []string) (err error) { + if len(rateIDs) != 0 { + tntID := utils.ConcatenatedKey(tenant, id) + for _, rateID := range rateIDs { + return rs.Cmd(nil, redisHDEL, utils.RateProfilePrefix+tntID, utils.Rates+utils.InInFieldSep+rateID) + } + } return rs.Cmd(nil, redisDEL, utils.RateProfilePrefix+utils.ConcatenatedKey(tenant, id)) } diff --git a/utils/librates.go b/utils/librates.go index 93ec200bd..0ce974e03 100644 --- a/utils/librates.go +++ b/utils/librates.go @@ -19,6 +19,7 @@ along with this program. If not, see package utils import ( + "encoding/json" "fmt" "sort" "strconv" @@ -909,3 +910,91 @@ func (iR *IntervalRate) FieldAsInterface(fldPath []string) (_ interface{}, err e return iR.Increment, nil } } + +// AsDataDBMap is used to is a convert method in order to properly set trough a hasmap in redis server our rate profile +func (rp *RateProfile) AsDataDBMap() (mp map[string]interface{}, err error) { + mp = map[string]interface{}{ + MaxCostStrategy: rp.MaxCostStrategy, + } + if len(rp.FilterIDs) != 0 { + var fltrs string + for idx, fltr := range rp.FilterIDs { + fltrs += fltr + if idx != len(rp.FilterIDs)-1 { + fltrs += InfieldSep + } + } + mp[FilterIDs] = fltrs + } + if rp.Weights != nil { + mp[Weights] = rp.Weights.String(InfieldSep, ANDSep) + } + if rp.MinCost != nil { + minCostBts, err := rp.MinCost.MarshalBinary() + if err != nil { + return nil, err + } + mp[MinCost] = minCostBts + } + if rp.MaxCost != nil { + maxCostBts, err := rp.MaxCost.MarshalBinary() + if err != nil { + return nil, err + } + mp[MaxCost] = maxCostBts + } + for rateID, rt := range rp.Rates { + var result []byte + if result, err = json.Marshal(rt); err != nil { + return nil, err + } + fldKey := ConcatenatedKey(Rates, rateID) + mp[fldKey] = result + } + return mp, nil +} + +// NewRateProfileFromMapDataDBMap will convert a RateProfile map into a RatePRofile struct. This is used when we get the map from redis database +func NewRateProfileFromMapDataDBMap(tnt, id string, mapRP map[string]interface{}) (rp *RateProfile, err error) { + rp = &RateProfile{ + ID: id, + Tenant: tnt, + MaxCostStrategy: IfaceAsString(mapRP[MaxCostStrategy]), + Rates: make(map[string]*Rate), + } + if fltrsIDs, has := mapRP[FilterIDs]; has { + fltrs := strings.Split(IfaceAsString(fltrsIDs), InfieldSep) + rp.FilterIDs = make([]string, len(fltrs)) + for idx, fltr := range fltrs { + rp.FilterIDs[idx] = fltr + } + } + if weights, has := mapRP[Weights]; has { + rp.Weights, err = NewDynamicWeightsFromString(IfaceAsString(weights), InfieldSep, ANDSep) + if err != nil { + return nil, err + } + } + if minCost, has := mapRP[MinCost]; has { + rp.MinCost, err = NewDecimalFromString(IfaceAsString(minCost)) + if err != nil { + return nil, err + } + } + if maxCost, has := mapRP[MaxCost]; has { + rp.MaxCost, err = NewDecimalFromString(IfaceAsString(maxCost)) + if err != nil { + return nil, err + } + } + for keyID, rateStr := range mapRP { + if strings.HasPrefix(keyID, Rates+ConcatenatedKeySep) { + var rate *Rate + if err := json.Unmarshal([]byte(IfaceAsString(rateStr)), &rate); err != nil { + return nil, err + } + rp.Rates[strings.TrimPrefix(keyID, Rates+ConcatenatedKeySep)] = rate + } + } + return rp, err +}