/* 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" "strconv" "strings" "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 RateProfileIDs []string // used when computing price ResourceIDs []string // queried in some strategies StatIDs []string // queried in some strategies Weights utils.DynamicWeights Blocker bool // do not process further route after this one RouteParameters string cacheRoute map[string]interface{} // cache["*ratio"]=ratio } // RouteProfile represents the configuration of a Route profile type RouteProfile struct { Tenant string ID string // LCR Profile ID FilterIDs []string Sorting string // Sorting strategy SortingParameters []string Routes []*Route Weights utils.DynamicWeights } // RouteProfileWithAPIOpts is used in replicatorV1 for dispatcher type RouteProfileWithAPIOpts struct { *RouteProfile APIOpts map[string]interface{} } 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]interface{}) 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) } // NewRouteService initializes the Route Service func NewRouteService(dm *DataManager, filterS *FilterS, cfg *config.CGRConfig, connMgr *ConnManager) (rS *RouteService) { rS = &RouteService{ dm: dm, filterS: filterS, cfg: cfg, connMgr: connMgr, sorter: RouteSortDispatcher{ utils.MetaWeight: NewWeightSorter(cfg), utils.MetaLC: NewLeastCostSorter(cfg, connMgr), utils.MetaHC: NewHighestCostSorter(cfg, connMgr), utils.MetaQOS: NewQOSRouteSorter(cfg, connMgr), utils.MetaReas: NewResourceAscendetSorter(cfg, connMgr), utils.MetaReds: NewResourceDescendentSorter(cfg, connMgr), utils.MetaLoad: NewLoadDistributionSorter(cfg, connMgr), }, } return } // RouteService is the service computing route queries type RouteService struct { dm *DataManager filterS *FilterS cfg *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(ctx *context.Context, tnt string, ev *utils.CGREvent) (matchingRPrf RouteProfilesWithWeight, err error) { evNm := utils.MapStorage{ utils.MetaReq: ev.Event, utils.MetaOpts: ev.APIOpts, } rPrfIDs, err := MatchingItemIDsForEvent(ctx, evNm, rpS.cfg.RouteSCfg().StringIndexedFields, rpS.cfg.RouteSCfg().PrefixIndexedFields, rpS.cfg.RouteSCfg().SuffixIndexedFields, rpS.dm, utils.CacheRouteFilterIndexes, tnt, rpS.cfg.RouteSCfg().IndexedSelects, rpS.cfg.RouteSCfg().NestedFields, ) if err != nil { return nil, err } matchingRPrf = make(RouteProfilesWithWeight, 0, len(rPrfIDs)) for lpID := range rPrfIDs { var rPrf *RouteProfile if rPrf, err = rpS.dm.GetRouteProfile(ctx, tnt, lpID, true, true, utils.NonTransactional); err != nil { if err == utils.ErrNotFound { continue } return } var pass bool if pass, err = rpS.filterS.Pass(ctx, tnt, rPrf.FilterIDs, evNm); err != nil { return } else if !pass { continue } var weight float64 if weight, err = WeightFromDynamics(ctx, rPrf.Weights, rpS.filterS, ev.Tenant, evNm); err != nil { return } matchingRPrf = append(matchingRPrf, &RouteProfileWithWeight{RouteProfile: rPrf, Weight: weight}) } if len(matchingRPrf) == 0 { return nil, utils.ErrNotFound } matchingRPrf.Sort() return } func newOptsGetRoutes(ctx *context.Context, ev *utils.CGREvent, fS *FilterS, cfgOpts *config.RoutesOpts) (opts *optsGetRoutes, err error) { var ignoreErrors bool if ignoreErrors, err = GetBoolOpts(ctx, ev.Tenant, ev, fS, cfgOpts.IgnoreErrors, config.RoutesIgnoreErrorsDftOpt, utils.OptsRoutesIgnoreErrors); err != nil { return } opts = &optsGetRoutes{ ignoreErrors: ignoreErrors, paginator: &utils.Paginator{}, } var limit *int if limit, err = GetIntPointerOpts(ctx, ev.Tenant, ev, fS, cfgOpts.Limit, utils.OptsRoutesLimit); err != nil { if err != utils.ErrNotFound { return } } else { opts.paginator.Limit = limit } var offset *int if offset, err = GetIntPointerOpts(ctx, ev.Tenant, ev, fS, cfgOpts.Offset, utils.OptsRoutesOffset); err != nil { if err != utils.ErrNotFound { return } } else { opts.paginator.Offset = offset } var maxCost interface{} if maxCost, err = GetInterfaceOpts(ctx, ev.Tenant, ev, fS, cfgOpts.MaxCost, config.RoutesMaxCostDftOpt, utils.OptsRoutesMaxCost); err != nil { return } switch maxCost { case utils.EmptyString, nil: case utils.MetaEventCost: if err = ev.CheckMandatoryFields([]string{utils.AccountField, utils.Destination, utils.SetupTime, utils.Usage}); err != nil { return } // ToDoNext: rates.V1CostForEvent // cd, err := NewCallDescriptorFromCGREvent(attr.CGREvent, // 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 sortingStragety string } // V1GetRoutes returns the list of valid routes func (rpS *RouteService) V1GetRoutes(ctx *context.Context, args *utils.CGREvent, reply *SortedRoutesList) (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.cfg.GeneralCfg().DefaultTenant } if len(rpS.cfg.RouteSCfg().AttributeSConns) != 0 { if args.APIOpts == nil { args.APIOpts = make(map[string]interface{}) } args.APIOpts[utils.Subsys] = utils.MetaRoutes var context string if context, err = GetStringOpts(ctx, tnt, args, rpS.filterS, rpS.cfg.RouteSCfg().Opts.Context, config.RoutesContextDftOpt, utils.OptsContext); err != nil { return } args.APIOpts[utils.OptsContext] = context var rplyEv AttrSProcessEventReply if err := rpS.connMgr.Call(ctx, rpS.cfg.RouteSCfg().AttributeSConns, utils.AttributeSv1ProcessEvent, args, &rplyEv); err == nil && len(rplyEv.AlteredFields) != 0 { args = rplyEv.CGREvent args.APIOpts = rplyEv.APIOpts } else if err.Error() != utils.ErrNotFound.Error() { return utils.NewErrRouteS(err) } } var sSps SortedRoutesList if sSps, err = rpS.sortedRoutesForEvent(ctx, 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) (_ 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.cfg.GeneralCfg().DefaultTenant } sPs, err := rpS.matchingRouteProfilesForEvent(ctx, tnt, args) if err != nil { if err != utils.ErrNotFound { err = utils.NewErrServerError(err) } return err } *reply = make([]*RouteProfile, len(sPs)) for i, sP := range sPs { (*reply)[i] = sP.RouteProfile } return } var lazyRouteFltrPrfxs = []string{utils.DynamicDataPrefix + utils.MetaReq, utils.DynamicDataPrefix + utils.MetaAccounts, utils.DynamicDataPrefix + utils.MetaResources, utils.DynamicDataPrefix + utils.MetaStats} // sortedRoutesForEvent will return the list of valid route IDs // for event based on filters and sorting algorithms func (rpS *RouteService) sortedRoutesForProfile(ctx *context.Context, 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.sortingStragety = 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]*RouteWithWeight) // apply filters for event for _, route := range rPrfl.Routes { var pass bool var lazyCheckRules []*FilterRule if pass, lazyCheckRules, err = rpS.filterS.LazyPass(ctx, tnt, route.FilterIDs, nM, lazyRouteFltrPrfxs); err != nil { return } else if !pass { continue } var weight float64 if weight, err = WeightFromDynamics(ctx, route.Weights, rpS.filterS, ev.Tenant, nM); err != nil { return } if prev, has := passedRoutes[route.ID]; !has || prev.Weight < weight { passedRoutes[route.ID] = &RouteWithWeight{ Route: route, lazyCheckRules: lazyCheckRules, Weight: weight, } } } if sortedRoutes, err = rpS.sorter.SortRoutes(ctx, 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 sortedRoutes // for event based on filters and sorting algorithms func (rpS *RouteService) sortedRoutesForEvent(ctx *context.Context, tnt string, args *utils.CGREvent) (sortedRoutes SortedRoutesList, err error) { var rPrfs RouteProfilesWithWeight if rPrfs, err = rpS.matchingRouteProfilesForEvent(ctx, tnt, args); err != nil { return } prfCount := len(rPrfs) // if the option is not present return for all profiles var prfCountOpt int if prfCountOpt, err = GetIntOpts(ctx, tnt, args, rpS.filterS, rpS.cfg.RouteSCfg().Opts.ProfileCount, config.RoutesProfileCountDftOpt, utils.OptsRoutesProfileCount); err != nil { return } if prfCount > prfCountOpt { // it has the option and is smaller that the current number of profiles prfCount = prfCountOpt } var extraOpts *optsGetRoutes if extraOpts, err = newOptsGetRoutes(ctx, args, rpS.filterS, rpS.cfg.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 varible to not duble 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 offest 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 seoarate 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(ctx, tnt, rPrfl.RouteProfile, 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 }