Add action type *dynamicRanking

This commit is contained in:
arberkatellari
2025-07-23 15:10:33 +02:00
committed by Dan Christian Bogos
parent 6302bb0fa1
commit b779388005
4 changed files with 242 additions and 3 deletions

View File

@@ -38,11 +38,11 @@ func parseParamStringToMap(paramStr string, targetMap map[string]any) error {
for tuple := range strings.SplitSeq(paramStr, utils.ANDSep) {
// Use strings.Cut to split 'tuple' into key-value pairs at the first occurrence of ':'.
// This ensures that additional ':' characters within the value do not affect parsing.
key, value, found := strings.Cut(tuple, utils.InInFieldSep)
if !found {
keyVal := strings.SplitN(tuple, utils.InInFieldSep, 2)
if len(keyVal) != 2 {
return fmt.Errorf("invalid key-value pair: %s", tuple)
}
targetMap[key] = value
targetMap[keyVal[0]] = keyVal[1]
}
return nil
}
@@ -919,3 +919,134 @@ func (aL *actDynamicTrend) execute(ctx *context.Context, data utils.MapStorage,
}
return
}
// actDynamicRanking processes the `ActionDiktatsOpts` field from the action to construct a RankingProfile
//
// The ActionDiktatsOpts field format is expected as follows:
//
// 0 Tenant: string
// 1 ID: string
// 2 Schedule: string
// 3 StatIDs: strings separated by "&".
// 4 MetricIDs: strings separated by "&".
// 5 Sorting: string
// 6 SortingParameters: strings separated by "&".
// 7 Stored: bool
// 8 ThresholdIDs: strings separated by "&".
// 9 APIOpts: set of key-value pairs (separated by "&").
//
// Parameters are separated by ";" and must be provided in the specified order.
type actDynamicRanking struct {
config *config.CGRConfig
connMgr *engine.ConnManager
fltrS *engine.FilterS
aCfg *utils.APAction
tnt string
cgrEv *utils.CGREvent
}
func (aL *actDynamicRanking) id() string {
return aL.aCfg.ID
}
func (aL *actDynamicRanking) cfg() *utils.APAction {
return aL.aCfg
}
// execute implements actioner interface
func (aL *actDynamicRanking) execute(ctx *context.Context, data utils.MapStorage, trgID string) (err error) {
if len(aL.config.ActionSCfg().AdminSConns) == 0 {
return fmt.Errorf("no connection with AdminS")
}
data[utils.MetaNow] = time.Now()
data[utils.MetaTenant] = utils.FirstNonEmpty(aL.cgrEv.Tenant, aL.tnt,
config.CgrConfig().GeneralCfg().DefaultTenant)
// Parse action parameters based on the predefined format.
if len(aL.aCfg.Diktats) == 0 {
return fmt.Errorf("No diktats were speified for action <%v>", aL.aCfg.ID)
}
weights := make(map[string]float64) // stores sorting weights by Diktat ID
diktats := make([]*utils.APDiktat, 0) // list of diktats which have *template in opts, will be weight sorted later
for _, diktat := range aL.aCfg.Diktats {
if pass, err := aL.fltrS.Pass(ctx, aL.tnt, diktat.FilterIDs, data); err != nil {
return err
} else if !pass {
continue
}
weight, err := engine.WeightFromDynamics(ctx, diktat.Weights, aL.fltrS, aL.tnt, data)
if err != nil {
return err
}
weights[diktat.ID] = weight
diktats = append(diktats, diktat)
}
// Sort by weight (higher values first).
slices.SortFunc(diktats, func(a, b *utils.APDiktat) int {
return cmp.Compare(weights[b.ID], weights[a.ID])
})
for _, diktat := range diktats {
params := strings.Split(utils.IfaceAsString(diktat.Opts[utils.MetaTemplate]),
utils.InfieldSep)
if len(params) != 10 {
return fmt.Errorf("invalid number of parameters <%d> expected 10", len(params))
}
// parse dynamic parameters
for i := range params {
if params[i], err = utils.ParseParamForDataProvider(params[i], data, false); err != nil {
return err
}
}
// Prepare request arguments based on provided parameters.
args := &utils.RankingProfileWithAPIOpts{
RankingProfile: &utils.RankingProfile{
Tenant: params[0],
ID: params[1],
Schedule: params[2],
Sorting: params[5],
},
APIOpts: make(map[string]any),
}
// populate Ranking's StatIDs
if params[3] != utils.EmptyString {
args.StatIDs = strings.Split(params[3], utils.ANDSep)
}
// populate Ranking's MetricIDs
if params[4] != utils.EmptyString {
args.MetricIDs = strings.Split(params[4], utils.ANDSep)
}
// populate Ranking's SortingParameters
if params[6] != utils.EmptyString {
args.SortingParameters = strings.Split(params[6], utils.ANDSep)
}
// populate Ranking's Stored
if params[7] != utils.EmptyString {
args.Stored, err = strconv.ParseBool(params[7])
if err != nil {
return err
}
}
// populate Ranking's ThresholdIDs
if params[8] != utils.EmptyString {
args.ThresholdIDs = strings.Split(params[8], utils.ANDSep)
}
// populate Ranking's APIOpts
if params[9] != utils.EmptyString {
if err := parseParamStringToMap(params[9], args.APIOpts); err != nil {
return err
}
}
// create the RankingProfile based on the populated parameters
var rply string
if err = aL.connMgr.Call(ctx, aL.config.ActionSCfg().AdminSConns,
utils.AdminSv1SetRankingProfile, args, &rply); err != nil {
return err
}
if blocker, err := engine.BlockerFromDynamics(ctx, diktat.Blockers, aL.fltrS, aL.tnt, data); err != nil {
return err
} else if blocker {
break
}
}
return
}

View File

@@ -43,6 +43,8 @@ func actionTarget(act string) string {
return utils.MetaResources
case utils.MetaDynamicTrend:
return utils.MetaTrends
case utils.MetaDynamicRanking:
return utils.MetaRankings
default:
return utils.MetaNone
}
@@ -149,6 +151,8 @@ func newActioner(ctx *context.Context, cgrEv *utils.CGREvent, cfg *config.CGRCon
return &actDynamicResource{cfg, connMgr, fltrS, aCfg, tnt, cgrEv}, nil
case utils.MetaDynamicTrend:
return &actDynamicTrend{cfg, connMgr, fltrS, aCfg, tnt, cgrEv}, nil
case utils.MetaDynamicRanking:
return &actDynamicRanking{cfg, connMgr, fltrS, aCfg, tnt, cgrEv}, nil
default:
return nil, fmt.Errorf("unsupported action type: <%s>", aCfg.Type)

View File

@@ -259,6 +259,7 @@ func TestDynThdIT(t *testing.T) {
utils.MetaAttributes: {"someID": {}},
utils.MetaResources: {"someID": {}},
utils.MetaTrends: {"someID": {}},
utils.MetaRankings: {"someID": {}},
},
Actions: []*utils.APAction{
{
@@ -546,6 +547,63 @@ func TestDynThdIT(t *testing.T) {
},
},
},
{
ID: "Dynamic_Ranking_ID",
Type: utils.MetaDynamicRanking,
Diktats: []*utils.APDiktat{
{
ID: "CreateDynamicRanking1002",
FilterIDs: []string{"*string:~*req.Account:1002"},
Opts: map[string]any{
"*template": "*tenant;DYNAMICLY_RNK_<~*req.Account>;@every 1s;Stats1&Stats2;*acc&*tcc;*asc;*acc&*pdd;true;THID1&THID2;~*opts",
},
Weights: utils.DynamicWeights{
{
Weight: 50,
},
},
},
{
ID: "CreateDynamicRanking1002NotFoundFilter",
FilterIDs: []string{"*string:~*req.Account:1003"},
Opts: map[string]any{
"*template": "*tenant;DYNAMICLY_RNK_2_<~*req.Account>;@every 1s;Stats1&Stats2;*acc&*tcc;*asc;*acc&*pdd;true;THID1&THID2;~*opts",
},
Weights: utils.DynamicWeights{
{
Weight: 90,
},
},
},
{
ID: "CreateDynamicRanking1002Blocker",
Opts: map[string]any{
"*template": "*tenant;DYNAMICLY_RNK_3_<~*req.Account>;@every 1s;Stats1&Stats2;*acc&*tcc;*asc;*acc&*pdd;true;THID1&THID2;~*opts",
},
Weights: utils.DynamicWeights{
{
Weight: 20,
},
},
Blockers: utils.DynamicBlockers{
{
Blocker: true,
},
},
},
{
ID: "CreateDynamicRanking1002Blocked",
Opts: map[string]any{
"*template": "*tenant;DYNAMICLY_RNK_4_<~*req.Account>;@every 1s;Stats1&Stats2;*acc&*tcc;*asc;*acc&*pdd;true;THID1&THID2;~*opts",
},
Weights: utils.DynamicWeights{
{
Weight: 10,
},
},
},
},
},
},
},
}
@@ -1013,4 +1071,49 @@ func TestDynThdIT(t *testing.T) {
}
})
t.Run("GetDynamicRankingProfile", func(t *testing.T) {
var rcv []*utils.RankingProfile
if err := client.Call(context.Background(), utils.AdminSv1GetRankingProfiles,
&utils.ArgsItemIDs{
Tenant: utils.CGRateSorg,
}, &rcv); err != nil {
t.Errorf("AdminSv1GetRankingProfiles failed unexpectedly: %v", err)
}
if len(rcv) != 2 {
t.Fatalf("AdminSv1GetRankingProfiles len(rcv)=%v, want 2", len(rcv))
}
sort.Slice(rcv, func(i, j int) bool {
return rcv[i].ID > rcv[j].ID
})
exp := []*utils.RankingProfile{
{
Tenant: "cgrates.org",
ID: "DYNAMICLY_RNK_3_1002",
Schedule: "@every 1s",
StatIDs: []string{"Stats1", "Stats2"},
MetricIDs: []string{"*acc", "*tcc"},
Sorting: "*asc",
SortingParameters: []string{"*acc", "*pdd"},
Stored: true,
ThresholdIDs: []string{"THID1", "THID2"},
},
{
Tenant: "cgrates.org",
ID: "DYNAMICLY_RNK_1002",
Schedule: "@every 1s",
StatIDs: []string{"Stats1", "Stats2"},
MetricIDs: []string{"*acc", "*tcc"},
Sorting: "*asc",
SortingParameters: []string{"*acc", "*pdd"},
Stored: true,
ThresholdIDs: []string{"THID1", "THID2"},
},
}
if !reflect.DeepEqual(exp, rcv) {
t.Errorf("Expected <%v>\nReceived <%v>", utils.ToJSON(exp), utils.ToJSON(rcv))
}
})
}

View File

@@ -1151,6 +1151,7 @@ const (
MetaDynamicAttribute = "*dynamic_attribute"
MetaDynamicResource = "*dynamic_resource"
MetaDynamicTrend = "*dynamicTrend"
MetaDynamicRanking = "*dynamicRanking"
// Diktats Opts Fields
MetaBalancePath = "*balancePath"