/* Real-time Online/Offline Charging System (OCS) for Telecom & ISP environments Copyright (C) ITsysCOM GmbH This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ package engine import ( "fmt" "math" "sort" "strconv" "strings" "time" "maps" "github.com/cgrates/birpc/context" "github.com/cgrates/cgrates/config" "github.com/cgrates/cgrates/utils" ) // Route defines routes related information used within a RouteProfile type Route struct { ID string // RouteID FilterIDs []string AccountIDs []string RatingPlanIDs []string // used when computing price ResourceIDs []string // queried in some strategies StatIDs []string // queried in some strategies Weight float64 Blocker bool // do not process further route after this one RouteParameters string cacheRoute map[string]any // cache["*ratio"]=ratio lazyCheckRules []*FilterRule } // Clone method for Route func (r *Route) Clone() *Route { clone := &Route{ ID: r.ID, Weight: r.Weight, Blocker: r.Blocker, RouteParameters: r.RouteParameters, } if r.FilterIDs != nil { clone.FilterIDs = make([]string, len(r.FilterIDs)) copy(clone.FilterIDs, r.FilterIDs) } if r.AccountIDs != nil { clone.AccountIDs = make([]string, len(r.AccountIDs)) copy(clone.AccountIDs, r.AccountIDs) } if r.RatingPlanIDs != nil { clone.RatingPlanIDs = make([]string, len(r.RatingPlanIDs)) copy(clone.RatingPlanIDs, r.RatingPlanIDs) } if r.ResourceIDs != nil { clone.ResourceIDs = make([]string, len(r.ResourceIDs)) copy(clone.ResourceIDs, r.ResourceIDs) } if r.StatIDs != nil { clone.StatIDs = make([]string, len(r.StatIDs)) copy(clone.StatIDs, r.StatIDs) } if r.cacheRoute != nil { clone.cacheRoute = make(map[string]any) maps.Copy(clone.cacheRoute, r.cacheRoute) } if r.lazyCheckRules != nil { clone.lazyCheckRules = make([]*FilterRule, len(r.lazyCheckRules)) for i, rule := range r.lazyCheckRules { clone.lazyCheckRules[i] = rule.Clone() } } return clone } // RouteProfile represents the configuration of a Route profile type RouteProfile struct { Tenant string ID string // LCR Profile ID FilterIDs []string ActivationInterval *utils.ActivationInterval // Activation interval Sorting string // Sorting strategy SortingParameters []string Routes []*Route Weight float64 } // Clone method for RouteProfile func (rp *RouteProfile) Clone() *RouteProfile { if rp == nil { return nil } clone := &RouteProfile{ Tenant: rp.Tenant, ID: rp.ID, Sorting: rp.Sorting, Weight: rp.Weight, } if rp.FilterIDs != nil { clone.FilterIDs = make([]string, len(rp.FilterIDs)) copy(clone.FilterIDs, rp.FilterIDs) } if rp.SortingParameters != nil { clone.SortingParameters = make([]string, len(rp.SortingParameters)) copy(clone.SortingParameters, rp.SortingParameters) } if rp.Routes != nil { clone.Routes = make([]*Route, len(rp.Routes)) for i, route := range rp.Routes { clone.Routes[i] = route.Clone() } } if rp.ActivationInterval != nil { clone.ActivationInterval = rp.ActivationInterval.Clone() } return clone } // CacheClone returns a clone of RouteProfile used by ltcache CacheCloner func (rp *RouteProfile) CacheClone() any { return rp.Clone() } // RouteProfileWithAPIOpts is used in replicatorV1 for dispatcher type RouteProfileWithAPIOpts struct { *RouteProfile APIOpts map[string]any } func (rp *RouteProfile) compileCacheParameters() error { if rp.Sorting == utils.MetaLoad { // construct the map for ratio ratioMap := make(map[string]int) // []string{"routeID:Ratio"} for _, splIDWithRatio := range rp.SortingParameters { splitted := strings.Split(splIDWithRatio, utils.ConcatenatedKeySep) ratioVal, err := strconv.Atoi(splitted[1]) if err != nil { return err } ratioMap[splitted[0]] = ratioVal } // add the ratio for each route for _, route := range rp.Routes { route.cacheRoute = make(map[string]any) if ratioRoute, has := ratioMap[route.ID]; !has { // in case that ratio isn't defined for specific routes check for default if ratioDefault, has := ratioMap[utils.MetaDefault]; !has { // in case that *default ratio isn't defined take it from config route.cacheRoute[utils.MetaRatio] = config.CgrConfig().RouteSCfg().DefaultRatio } else { route.cacheRoute[utils.MetaRatio] = ratioDefault } } else { route.cacheRoute[utils.MetaRatio] = ratioRoute } } } return nil } // Compile is a wrapper for convenience setting up the RouteProfile func (rp *RouteProfile) Compile() error { return rp.compileCacheParameters() } // TenantID returns unique identifier of the LCRProfile in a multi-tenant environment func (rp *RouteProfile) TenantID() string { return utils.ConcatenatedKey(rp.Tenant, rp.ID) } // RouteProfiles is a sortable list of RouteProfile type RouteProfiles []*RouteProfile // Sort is part of sort interface, sort based on Weight func (lps RouteProfiles) Sort() { sort.Slice(lps, func(i, j int) bool { return lps[i].Weight > lps[j].Weight }) } // NewRouteService initializes the Route Service func NewRouteService(dm *DataManager, filterS *FilterS, cgrcfg *config.CGRConfig, connMgr *ConnManager) (rS *RouteService) { rS = &RouteService{ dm: dm, filterS: filterS, cgrcfg: cgrcfg, connMgr: connMgr, } rS.sorter = NewRouteSortDispatcher(rS) return } // RouteService is the service computing route queries type RouteService struct { dm *DataManager filterS *FilterS cgrcfg *config.CGRConfig sorter RouteSortDispatcher connMgr *ConnManager } // Shutdown is called to shutdown the service func (rpS *RouteService) Shutdown() { utils.Logger.Info(fmt.Sprintf("<%s> service shutdown initialized", utils.RouteS)) utils.Logger.Info(fmt.Sprintf("<%s> service shutdown complete", utils.RouteS)) } // matchingRouteProfilesForEvent returns ordered list of matching resources which are active by the time of the call func (rpS *RouteService) matchingRouteProfilesForEvent(tnt string, ev *utils.CGREvent) (matchingRPrf []*RouteProfile, err error) { evNm := utils.MapStorage{ utils.MetaReq: ev.Event, utils.MetaOpts: ev.APIOpts, } rPrfIDs, err := MatchingItemIDsForEvent(evNm, rpS.cgrcfg.RouteSCfg().StringIndexedFields, rpS.cgrcfg.RouteSCfg().PrefixIndexedFields, rpS.cgrcfg.RouteSCfg().SuffixIndexedFields, rpS.cgrcfg.RouteSCfg().ExistsIndexedFields, rpS.dm, utils.CacheRouteFilterIndexes, tnt, rpS.cgrcfg.RouteSCfg().IndexedSelects, rpS.cgrcfg.RouteSCfg().NestedFields, ) if err != nil { return nil, err } matchingRPrf = make([]*RouteProfile, 0, len(rPrfIDs)) for lpID := range rPrfIDs { rPrf, err := rpS.dm.GetRouteProfile(tnt, lpID, true, true, utils.NonTransactional) if err != nil { if err == utils.ErrNotFound { continue } return nil, err } if rPrf.ActivationInterval != nil && ev.Time != nil && !rPrf.ActivationInterval.IsActiveAtTime(*ev.Time) { // not active continue } if pass, err := rpS.filterS.Pass(tnt, rPrf.FilterIDs, evNm); err != nil { return nil, err } else if !pass { continue } matchingRPrf = append(matchingRPrf, rPrf) } if len(matchingRPrf) == 0 { return nil, utils.ErrNotFound } sort.Slice(matchingRPrf, func(i, j int) bool { return matchingRPrf[i].Weight > matchingRPrf[j].Weight }) return } // costForEvent will compute cost out of accounts and rating plans for event // returns map[string]any with cost and relevant matching information inside func (rpS *RouteService) costForEvent(ev *utils.CGREvent, acntIDs, rpIDs []string) (costData map[string]any, err error) { costData = make(map[string]any) if err = ev.CheckMandatoryFields([]string{utils.AccountField, utils.Destination, utils.SetupTime}); err != nil { return } var acnt, subj, dst string if acnt, err = ev.FieldAsString(utils.AccountField); err != nil { return } if subj, err = ev.FieldAsString(utils.Subject); err != nil { if err != utils.ErrNotFound { return } subj = acnt } if dst, err = ev.FieldAsString(utils.Destination); err != nil { return } var sTime time.Time if sTime, err = ev.FieldAsTime(utils.SetupTime, rpS.cgrcfg.GeneralCfg().DefaultTimezone); err != nil { return } var usage time.Duration if usage, err = ev.FieldAsDuration(utils.Usage); err != nil { if err != utils.ErrNotFound { return } // in case usage is missing from event we decide to use 1 minute as default usage = time.Duration(1 * time.Minute) err = nil } var accountMaxUsage time.Duration var acntCost map[string]any var initialUsage time.Duration if len(acntIDs) != 0 { if err := rpS.connMgr.Call(context.TODO(), rpS.cgrcfg.RouteSCfg().RALsConns, utils.ResponderGetMaxSessionTimeOnAccounts, &utils.GetMaxSessionTimeOnAccountsArgs{ Tenant: ev.Tenant, Subject: subj, Destination: dst, SetupTime: sTime, Usage: usage, AccountIDs: acntIDs, APIOpts: ev.APIOpts, }, &acntCost); err != nil { return nil, err } if ifaceMaxUsage, has := acntCost[utils.CapMaxUsage]; has { if accountMaxUsage, err = utils.IfaceAsDuration(ifaceMaxUsage); err != nil { return nil, err } if usage > accountMaxUsage { // remain usage needs to be covered by rating plans if len(rpIDs) == 0 { return nil, fmt.Errorf("no rating plans defined for remaining usage") } // update the setup time and the usage sTime = sTime.Add(accountMaxUsage) initialUsage = usage usage = usage - accountMaxUsage } for k, v := range acntCost { // update the costData with the infos from AccountS costData[k] = v } } } if accountMaxUsage == 0 || accountMaxUsage < initialUsage { var rpCost map[string]any if err := rpS.connMgr.Call(context.TODO(), rpS.cgrcfg.RouteSCfg().RALsConns, utils.ResponderGetCostOnRatingPlans, &utils.GetCostOnRatingPlansArgs{ Tenant: ev.Tenant, Account: acnt, Subject: subj, Destination: dst, SetupTime: sTime, Usage: usage, RatingPlanIDs: rpIDs, APIOpts: ev.APIOpts, }, &rpCost); err != nil { return nil, err } for k, v := range rpCost { // do not overwrite the return map costData[k] = v } } return } // statMetrics will query a list of statIDs and return composed metric values // first metric found is always returned func (rpS *RouteService) statMetrics(statIDs []string, tenant string) (stsMetric map[string]float64, err error) { stsMetric = make(map[string]float64) provStsMetrics := make(map[string][]float64) if len(rpS.cgrcfg.RouteSCfg().StatSConns) != 0 { for _, statID := range statIDs { var metrics map[string]float64 if err = rpS.connMgr.Call(context.TODO(), rpS.cgrcfg.RouteSCfg().StatSConns, utils.StatSv1GetQueueFloatMetrics, &utils.TenantIDWithAPIOpts{TenantID: &utils.TenantID{Tenant: tenant, ID: statID}}, &metrics); err != nil && err.Error() != utils.ErrNotFound.Error() { utils.Logger.Warning( fmt.Sprintf("<%s> error: %s getting statMetrics for stat : %s", utils.RouteS, err.Error(), statID)) } for key, val := range metrics { //add value of metric in a slice in case that we get the same metric from different stat provStsMetrics[key] = append(provStsMetrics[key], val) } } for metric, slice := range provStsMetrics { sum := 0.0 for _, val := range slice { sum += val } stsMetric[metric] = sum / float64(len(slice)) } } return } // statMetricsForLoadDistribution will query a list of statIDs and return the sum of metrics // first metric found is always returned func (rpS *RouteService) statMetricsForLoadDistribution(statIDs []string, tenant string) (result float64, err error) { provStsMetrics := make(map[string][]float64) if len(rpS.cgrcfg.RouteSCfg().StatSConns) != 0 { for _, statID := range statIDs { // check if we get an ID in the following form (StatID:MetricID) statWithMetric := strings.Split(statID, utils.InInFieldSep) var metrics map[string]float64 if err = rpS.connMgr.Call(context.TODO(), rpS.cgrcfg.RouteSCfg().StatSConns, utils.StatSv1GetQueueFloatMetrics, &utils.TenantIDWithAPIOpts{ TenantID: &utils.TenantID{ Tenant: tenant, ID: statWithMetric[0]}}, &metrics); err != nil && err.Error() != utils.ErrNotFound.Error() { utils.Logger.Warning( fmt.Sprintf("<%s> error: %s getting statMetrics for stat : %s", utils.RouteS, err.Error(), statWithMetric[0])) } if len(statWithMetric) == 2 { // in case we have MetricID defined with StatID we consider only that metric // check if statQueue have metric defined metricVal, has := metrics[statWithMetric[1]] if !has { return 0, fmt.Errorf("<%s> error: %s metric %s for statID: %s", utils.RouteS, utils.ErrNotFound, statWithMetric[1], statWithMetric[0]) } provStsMetrics[statWithMetric[1]] = append(provStsMetrics[statWithMetric[1]], metricVal) } else { // otherwise we consider all metrics for key, val := range metrics { //add value of metric in a slice in case that we get the same metric from different stat provStsMetrics[key] = append(provStsMetrics[key], val) } } } for _, slice := range provStsMetrics { sum := 0.0 for _, val := range slice { sum += val } result += sum } } return } // resourceUsage returns sum of all resource usages out of list func (rpS *RouteService) resourceUsage(resIDs []string, tenant string) (tUsage float64, err error) { if len(rpS.cgrcfg.RouteSCfg().ResourceSConns) != 0 { for _, resID := range resIDs { var res Resource if err = rpS.connMgr.Call(context.TODO(), rpS.cgrcfg.RouteSCfg().ResourceSConns, utils.ResourceSv1GetResource, &utils.TenantIDWithAPIOpts{TenantID: &utils.TenantID{Tenant: tenant, ID: resID}}, &res); err != nil && err.Error() != utils.ErrNotFound.Error() { utils.Logger.Warning( fmt.Sprintf("<%s> error: %s getting resource for ID : %s", utils.RouteS, err.Error(), resID)) continue } tUsage += res.TotalUsage() } } return } func (rpS *RouteService) populateSortingData(ev *utils.CGREvent, route *Route, extraOpts *optsGetRoutes) (srtRoute *SortedRoute, pass bool, err error) { sortedSpl := &SortedRoute{ RouteID: route.ID, SortingData: map[string]any{ utils.Weight: route.Weight, }, sortingDataF64: map[string]float64{ utils.Weight: route.Weight, }, RouteParameters: route.RouteParameters, } //calculate costData if we have fields if len(route.AccountIDs) != 0 || len(route.RatingPlanIDs) != 0 { costData, err := rpS.costForEvent(ev, route.AccountIDs, route.RatingPlanIDs) if err != nil { if extraOpts.ignoreErrors { utils.Logger.Warning( fmt.Sprintf("<%s> ignoring route with ID: %s, err: %s", utils.RouteS, route.ID, err.Error())) return nil, false, nil } return nil, false, err } else if len(costData) == 0 { utils.Logger.Warning( fmt.Sprintf("<%s> ignoring route with ID: %s, missing cost information", utils.RouteS, route.ID)) return nil, false, nil } else { if extraOpts.maxCost != 0 && costData[utils.Cost].(float64) > extraOpts.maxCost { utils.Logger.Warning( fmt.Sprintf("<%s> ignoring route with ID: %s, err: %s", utils.RouteS, route.ID, utils.ErrMaxCostExceeded.Error())) return nil, false, nil } for k, v := range costData { sortedSpl.SortingData[k] = v sortedSpl.sortingDataF64[k], _ = v.(float64) } } } //calculate metrics //in case we have *load strategy we use statMetricsForLoadDistribution function to calculate the result if len(route.StatIDs) != 0 { if extraOpts.sortingStrategy == utils.MetaLoad { metricSum, err := rpS.statMetricsForLoadDistribution(route.StatIDs, ev.Tenant) //create metric map for route if err != nil { if extraOpts.ignoreErrors { utils.Logger.Warning( fmt.Sprintf("<%s> ignoring route with ID: %s, err: %s", utils.RouteS, route.ID, err.Error())) return nil, false, nil } return nil, false, err } sortedSpl.SortingData[utils.Load] = metricSum sortedSpl.sortingDataF64[utils.Load] = metricSum } else { metricSupp, err := rpS.statMetrics(route.StatIDs, ev.Tenant) //create metric map for route if err != nil { if extraOpts.ignoreErrors { utils.Logger.Warning( fmt.Sprintf("<%s> ignoring route with ID: %s, err: %s", utils.RouteS, route.ID, err.Error())) return nil, false, nil } return nil, false, err } //add metrics from statIDs in SortingData for key, val := range metricSupp { sortedSpl.SortingData[key] = val sortedSpl.sortingDataF64[key] = val } //check if the route have the metric from sortingParameters //in case that the metric don't exist //we use 10000000 for *pdd and -1 for others for _, metric := range extraOpts.sortingParameters { if _, hasMetric := metricSupp[metric]; !hasMetric { switch metric { default: sortedSpl.SortingData[metric] = -1.0 sortedSpl.sortingDataF64[metric] = -1.0 case utils.MetaPDD: sortedSpl.SortingData[metric] = math.MaxFloat64 sortedSpl.sortingDataF64[metric] = math.MaxFloat64 } } } } } //calculate resourceUsage if len(route.ResourceIDs) != 0 { resTotalUsage, err := rpS.resourceUsage(route.ResourceIDs, ev.Tenant) if err != nil { if extraOpts.ignoreErrors { utils.Logger.Warning( fmt.Sprintf("<%s> ignoring route with ID: %s, err: %s", utils.RouteS, route.ID, err.Error())) return nil, false, nil } return nil, false, err } sortedSpl.SortingData[utils.ResourceUsage] = resTotalUsage sortedSpl.sortingDataF64[utils.ResourceUsage] = resTotalUsage } //filter the route if len(route.lazyCheckRules) != 0 { //construct the DP and pass it to filterS dynDP := newDynamicDP(rpS.cgrcfg.FilterSCfg().ResourceSConns, rpS.cgrcfg.FilterSCfg().StatSConns, rpS.cgrcfg.FilterSCfg().ApierSConns, rpS.cgrcfg.FilterSCfg().TrendSConns, nil, ev.Tenant, utils.MapStorage{ utils.MetaReq: ev.Event, utils.MetaVars: sortedSpl.SortingData, }) for _, rule := range route.lazyCheckRules { // verify the rules remaining from PartialPass if pass, err = rule.Pass(dynDP); err != nil { return nil, false, err } else if !pass { return nil, false, nil } } } return sortedSpl, true, nil } func newOptsGetRoutes(ev *utils.CGREvent, fS *FilterS, cfgOpts *config.RoutesOpts) (opts *optsGetRoutes, err error) { var ignoreErrors bool if ignoreErrors, err = utils.GetBoolOpts(ev, cfgOpts.IgnoreErrors, utils.OptsRoutesIgnoreErrors); err != nil { return } opts = &optsGetRoutes{ ignoreErrors: ignoreErrors, paginator: &utils.Paginator{}, } var limit *int if limit, err = utils.GetIntPointerOpts(ev, cfgOpts.Limit, utils.OptsRoutesLimit); err != nil { return } if limit != nil { opts.paginator.Limit = limit } var offset *int if offset, err = utils.GetIntPointerOpts(ev, cfgOpts.Offset, utils.OptsRoutesOffset); err != nil { return } if offset != nil { opts.paginator.Offset = offset } maxCost := utils.GetInterfaceOpts(ev, cfgOpts.MaxCost, utils.OptsRoutesMaxCost) switch maxCost { case utils.EmptyString, nil: case utils.MetaEventCost: // dynamic cost needs to be calculated from event if err = ev.CheckMandatoryFields([]string{utils.AccountField, utils.Destination, utils.SetupTime, utils.Usage}); err != nil { return } cd, err := NewCallDescriptorFromCGREvent(ev, config.CgrConfig().GeneralCfg().DefaultTimezone) if err != nil { return nil, err } cc, err := cd.GetCost() if err != nil { return nil, err } opts.maxCost = cc.Cost default: if opts.maxCost, err = utils.IfaceAsFloat64(maxCost); err != nil { return nil, err } } return } type optsGetRoutes struct { ignoreErrors bool maxCost float64 paginator *utils.Paginator sortingParameters []string //used for QOS strategy sortingStrategy string } // V1GetRoutes returns the list of valid routes func (rpS *RouteService) V1GetRoutes(ctx *context.Context, args *utils.CGREvent, reply *SortedRoutesList) (err error) { if args == nil { return utils.NewErrMandatoryIeMissing(utils.CGREventString) } if missing := utils.MissingStructFields(args, []string{utils.ID}); len(missing) != 0 { return utils.NewErrMandatoryIeMissing(missing...) } else if args.Event == nil { return utils.NewErrMandatoryIeMissing(utils.Event) } tnt := args.Tenant if tnt == utils.EmptyString { tnt = rpS.cgrcfg.GeneralCfg().DefaultTenant } if len(rpS.cgrcfg.RouteSCfg().AttributeSConns) != 0 { if args.APIOpts == nil { args.APIOpts = make(map[string]any) } args.APIOpts[utils.MetaSubsys] = utils.MetaRoutes args.APIOpts[utils.OptsContext] = utils.FirstNonEmpty( utils.GetStringOpts(args, rpS.cgrcfg.RouteSCfg().Opts.Context, utils.OptsContext), utils.MetaRoutes, ) var rplyEv AttrSProcessEventReply if err := rpS.connMgr.Call(context.TODO(), rpS.cgrcfg.RouteSCfg().AttributeSConns, utils.AttributeSv1ProcessEvent, args, &rplyEv); err == nil && len(rplyEv.AlteredFields) != 0 { args = rplyEv.CGREvent } else if err.Error() != utils.ErrNotFound.Error() { return utils.NewErrRouteS(err) } } var sSps SortedRoutesList if sSps, err = rpS.sortedRoutesForEvent(tnt, args); err != nil { if err != utils.ErrNotFound { err = utils.NewErrServerError(err) } return } *reply = sSps return } // V1GetRouteProfilesForEvent returns the list of valid route profiles func (rpS *RouteService) V1GetRouteProfilesForEvent(ctx *context.Context, args *utils.CGREvent, reply *[]*RouteProfile) (err error) { if missing := utils.MissingStructFields(args, []string{utils.ID}); len(missing) != 0 { return utils.NewErrMandatoryIeMissing(missing...) } else if args.Event == nil { return utils.NewErrMandatoryIeMissing(utils.Event) } tnt := args.Tenant if tnt == utils.EmptyString { tnt = rpS.cgrcfg.GeneralCfg().DefaultTenant } sPs, err := rpS.matchingRouteProfilesForEvent(tnt, args) if err != nil { if err != utils.ErrNotFound { err = utils.NewErrServerError(err) } return err } *reply = sPs return } // sortedRoutesForEvent will return the list of valid route IDs // for event based on filters and sorting algorithms func (rpS *RouteService) sortedRoutesForProfile(tnt string, rPrfl *RouteProfile, ev *utils.CGREvent, pag utils.Paginator, extraOpts *optsGetRoutes) (sortedRoutes *SortedRoutes, err error) { extraOpts.sortingParameters = rPrfl.SortingParameters // populate sortingParameters in extraOpts extraOpts.sortingStrategy = rPrfl.Sorting // populate sortingStrategy in extraOpts //construct the DP and pass it to filterS nM := utils.MapStorage{ utils.MetaReq: ev.Event, utils.MetaOpts: ev.APIOpts, } passedRoutes := make(map[string]*Route) // apply filters for event for _, route := range rPrfl.Routes { pass, lazyCheckRules, err := rpS.filterS.LazyPass(tnt, route.FilterIDs, nM, []string{utils.DynamicDataPrefix + utils.MetaReq, utils.DynamicDataPrefix + utils.MetaAccounts, utils.DynamicDataPrefix + utils.MetaResources, utils.DynamicDataPrefix + utils.MetaStats}) if err != nil { return nil, err } else if !pass { continue } route.lazyCheckRules = lazyCheckRules if prev, has := passedRoutes[route.ID]; has && prev.Weight >= route.Weight { continue } passedRoutes[route.ID] = route } if sortedRoutes, err = rpS.sorter.SortRoutes(rPrfl.ID, rPrfl.Sorting, passedRoutes, ev, extraOpts); err != nil { return nil, err } if pag.Offset != nil { if *pag.Offset <= len(sortedRoutes.Routes) { sortedRoutes.Routes = sortedRoutes.Routes[*pag.Offset:] } } if pag.Limit != nil { if *pag.Limit <= len(sortedRoutes.Routes) { sortedRoutes.Routes = sortedRoutes.Routes[:*pag.Limit] } } return } // sortedRoutesForEvent will return the list of valid route IDs // for event based on filters and sorting algorithms func (rpS *RouteService) sortedRoutesForEvent(tnt string, args *utils.CGREvent) (sortedRoutes SortedRoutesList, err error) { if _, has := args.Event[utils.Usage]; !has { args.Event[utils.Usage] = time.Minute // make sure we have default set for Usage } var rPrfs []*RouteProfile if rPrfs, err = rpS.matchingRouteProfilesForEvent(tnt, args); err != nil { return } prfCount := len(rPrfs) // if the option is not present return for all profiles var prfCountOpt *int if prfCountOpt, err = utils.GetIntPointerOpts(args, rpS.cgrcfg.RouteSCfg().Opts.ProfileCount, utils.OptsRoutesProfileCount); err != nil { return } if prfCountOpt != nil && prfCount > *prfCountOpt { // it has the option and is smaller that the current number of profiles prfCount = *prfCountOpt } var extraOpts *optsGetRoutes if extraOpts, err = newOptsGetRoutes(args, rpS.filterS, rpS.cgrcfg.RouteSCfg().Opts); err != nil { // convert routes arguments into internal options used to limit data return } var startIdx, noSrtRoutes int if extraOpts.paginator.Offset != nil { // save the offset in a variable to not double check if we have offset and is still not 0 startIdx = *extraOpts.paginator.Offset } sortedRoutes = make(SortedRoutesList, 0, prfCount) for _, rPrfl := range rPrfs { var prfPag utils.Paginator if extraOpts.paginator.Limit != nil { // we have a limit if noSrtRoutes >= *extraOpts.paginator.Limit { // the limit was reached break } if noSrtRoutes+len(rPrfl.Routes) > *extraOpts.paginator.Limit { // the limit will be reached in this profile limit := *extraOpts.paginator.Limit - noSrtRoutes // make it relative to current profile prfPag.Limit = &limit // add the limit to the paginator } } if startIdx > 0 { // we have offset if idx := startIdx - len(rPrfl.Routes); idx >= 0 { // we still have offset so try the next profile startIdx = idx continue } // we have offset but is in the range of this profile offset := startIdx // store in a separate var so when startIdx is updated the prfPag.Offset remains the same startIdx = 0 // set it to 0 for the following loop prfPag.Offset = &offset } var sr *SortedRoutes if sr, err = rpS.sortedRoutesForProfile(tnt, rPrfl, args, prfPag, extraOpts); err != nil { return } if len(sr.Routes) != 0 { noSrtRoutes += len(sr.Routes) sortedRoutes = append(sortedRoutes, sr) if len(sortedRoutes) == prfCount { // the profile count was reached break } } } if len(sortedRoutes) == 0 { err = utils.ErrNotFound } return } // V1GetRoutesList returns the list of valid routes func (rpS *RouteService) V1GetRoutesList(ctx *context.Context, args *utils.CGREvent, reply *[]string) (err error) { sR := new(SortedRoutesList) if err = rpS.V1GetRoutes(ctx, args, sR); err != nil { return } *reply = sR.RoutesWithParams() return }