This commit is contained in:
DanB
2015-04-09 09:04:26 +02:00
7 changed files with 203 additions and 132 deletions

View File

@@ -9,6 +9,7 @@
cdrclient
cdrexporter
cdrstats
lcr
derived_charging
history
ratinglogic

View File

@@ -16,7 +16,6 @@ Contents:
installation
configuration
administration
lcr
advanced
tutorials
miscellaneous

View File

@@ -3,4 +3,76 @@ LCR System
In voice telecommunications, least-cost routing (LCR) is the process of selecting the path of outbound communications traffic based on cost. Within a telecoms carrier, an LCR team might periodically (monthly, weekly or even daily) choose between routes from several or even hundreds of carriers for destinations across the world. This function might also be automated by a device or software program known as a "Least Cost Router." [WIKI2015]_
Data structures
---------------
The LCR rule parameters are: Direction, Tenant, Category, Account, Subject, DestinationId, RPCategory, Strategy, StrategyParameters, ActivationTime, Weight.
The first five are used to match the rule for a specific call descriptor. They can have a value or marked as \*any.
The DestinationId can be used to filter the LCR rules entries or to make the rule more specific.
RPCategory is used to indicate the rating profile category.
Strategy indicates supplier selection algorithm and StrategyParams will be specific to each strategy. Strategy can be one of the following:
\*static (filter)
Will use the suppliers provided as params.
StrategyParams: suppier1;supplier2;etc
\*lowest_cost (sorting)
Matching suppliers will be sorted by ascending cost.
StrategyParams: None
\*highest_cost (sorting)
Matching suppliers will be sorted by descending cost.
StrategyParams: None
\*qos_with_threshold (filter)
The system will reject the suppliers that have out of bounds average success ratio or average call duration.
StrategyParams: min_asr;max_asr;min_acd;max_acd
\*qos (sorting)
The system will sort by metrics in the order of appearance.
StrategyParams: metric1;metric2;etc
ActivationTime is the date/time when the LCR entry starts to be active.
Weight is used to sort the rules with the same activation time.
Example
+++++++
::
*in, cgrates.org,call,*any,*any,EU_LANDLINE,LCR_STANDARD,*static,ivo;dan;rif,2012-01-01T00:00:00Z,10
Code implementation
-------------------
The general process of getting LCRs is this.
The LCR rules for a specific call descriptor are searched using direction, tenant, category, account and subject of the call descriptor matched as strictly as possible with LCR rules.
Because a rule can have several entries they will be sorted by activation time.
Next the system will find out the most recent LCR entry that applies to this call considering entries activation times.
The LCR entry is processed according to it's strategy. For static strategy the cost is calculated for each supplier found in the parameters and the suppliers are listed as they are found.
For the QOS strategies the suppliers are searched using call descriptor parameters (direction, tenant, category, account, subject), than the cdrstats module is queried for the QOS values and the suppliers are filtered or sorted according to the StrategyParameters field.
For the lowest/highest cost strategies the matched suppliers are sorted ascending/descending on cost.
::
{
"Entry": {
"DestinationId": "*any",
"RPCategory": "LCR_STANDARD",
"Strategy": "*lowest_cost",
"StrategyParams": "",
"Weight": 20
},
"SupplierCosts": [{"Supplier":"rif", Cost:"2.0"},{"Supplier":"dan", Cost:"1.0"}]
}
.. [WIKI2015] http://en.wikipedia.org/wiki/Least-cost_routing

View File

@@ -671,144 +671,143 @@ func (cd *CallDescriptor) GetLCRFromStorage() (*LCR, error) {
return nil, errors.New(utils.ERR_NOT_FOUND)
}
func (cd *CallDescriptor) GetLCR(stats StatsInterface) (LCRCost, error) {
func (cd *CallDescriptor) GetLCR(stats StatsInterface) (*LCRCost, error) {
lcr, err := cd.GetLCRFromStorage()
if err != nil {
return nil, err
}
// sort by activation time
lcr.Sort()
lcrCost := LCRCost{&LCRTimeSpan{StartTime: cd.TimeStart}}
// find if one ore more entries apply to this cd (create lcr timespans)
// create timespans and attach lcr entries to them
lcrCost := &LCRCost{}
for _, lcrActivation := range lcr.Activations {
//log.Printf("Activation: %+v", lcrActivation)
lcrEntry := lcrActivation.GetLCREntryForPrefix(cd.Destination)
//log.Printf("Entry: %+v", lcrEntry)
if lcrActivation.ActivationTime.Before(cd.TimeStart) ||
lcrActivation.ActivationTime.Equal(cd.TimeStart) {
lcrCost[0].Entry = lcrEntry
lcrCost.Entry = lcrEntry
} else {
if lcrActivation.ActivationTime.Before(cd.TimeEnd) {
// add lcr timespan
lcrCost = append(lcrCost, &LCRTimeSpan{
StartTime: lcrActivation.ActivationTime,
Entry: lcrEntry,
// because lcr is sorted the folowing ones will
// only activate later than cd.Timestart
break
}
}
if lcrCost.Entry == nil {
return lcrCost, nil
}
//log.Printf("Entry: %+v", ts.Entry)
if lcrCost.Entry.Strategy == LCR_STRATEGY_STATIC {
for _, supplier := range lcrCost.Entry.GetParams() {
lcrCD := cd.Clone()
lcrCD.Subject = supplier
if cd.account, err = accountingStorage.GetAccount(cd.GetAccountKey()); err != nil {
continue
}
if cc, err := lcrCD.debit(cd.account, true, true); err != nil || cc == nil {
lcrCost.SupplierCosts = append(lcrCost.SupplierCosts, &LCRSupplierCost{
Supplier: supplier,
Error: err,
})
} else {
lcrCost.SupplierCosts = append(lcrCost.SupplierCosts, &LCRSupplierCost{
Supplier: supplier,
Cost: cc.Cost,
Duration: cc.GetDuration().String(),
})
}
}
}
for _, ts := range lcrCost {
//log.Printf("TS: %+v", ts)
if ts.Entry == nil {
continue
}
//log.Printf("Entry: %+v", ts.Entry)
if ts.Entry.Strategy == LCR_STRATEGY_STATIC {
for _, supplier := range ts.Entry.GetParams() {
lcrCD := cd.Clone()
lcrCD.Subject = supplier
if cd.account, err = accountingStorage.GetAccount(cd.GetAccountKey()); err != nil {
continue
}
if cc, err := lcrCD.debit(cd.account, true, true); err != nil || cc == nil {
ts.SupplierCosts = append(ts.SupplierCosts, &LCRSupplierCost{
Supplier: supplier,
Error: err,
})
} else {
ts.SupplierCosts = append(ts.SupplierCosts, &LCRSupplierCost{
Supplier: supplier,
Cost: cc.Cost,
})
}
} else {
// find rating profiles
ratingProfileSearchKey := utils.ConcatenatedKey(lcr.Direction, lcr.Tenant, lcrCost.Entry.RPCategory)
suppliers := cache2go.GetEntriesKeys(RATING_PROFILE_PREFIX + ratingProfileSearchKey)
for _, supplier := range suppliers {
split := strings.Split(supplier, ":")
supplier = split[len(split)-1]
lcrCD := cd.Clone()
lcrCD.Subject = supplier
if cd.account, err = accountingStorage.GetAccount(cd.GetAccountKey()); err != nil {
continue
}
} else {
// find rating profiles
ratingProfileSearchKey := utils.ConcatenatedKey(lcr.Direction, lcr.Tenant, ts.Entry.RPCategory)
suppliers := cache2go.GetEntriesKeys(RATING_PROFILE_PREFIX + ratingProfileSearchKey)
for _, supplier := range suppliers {
split := strings.Split(supplier, ":")
supplier = split[len(split)-1]
lcrCD := cd.Clone()
lcrCD.Subject = supplier
if cd.account, err = accountingStorage.GetAccount(cd.GetAccountKey()); err != nil {
continue
}
var asr, acd float64
var qosSortParams []string
if ts.Entry.Strategy == LCR_STRATEGY_QOS || ts.Entry.Strategy == LCR_STRATEGY_QOS_WITH_THRESHOLD {
rpfKey := utils.ConcatenatedKey(ratingProfileSearchKey, supplier)
if rpf, err := dataStorage.GetRatingProfile(rpfKey, false); err == nil || rpf != nil {
rpf.RatingPlanActivations.Sort()
activeRas := rpf.RatingPlanActivations.GetActiveForCall(cd)
var cdrStatsQueueIds []string
for _, ra := range activeRas {
for _, qId := range ra.CdrStatQueueIds {
if qId != "" {
cdrStatsQueueIds = append(cdrStatsQueueIds, qId)
}
}
}
var asrValues sort.Float64Slice
var acdValues sort.Float64Slice
for _, qId := range cdrStatsQueueIds {
statValues := make(map[string]float64)
if err := stats.GetValues(qId, &statValues); err != nil {
Logger.Warning(fmt.Sprintf("Error getting stats values for queue id %s: %v", qId, err))
}
if asr, exists := statValues[ASR]; exists {
asrValues = append(asrValues, asr)
}
if acd, exists := statValues[ACD]; exists {
acdValues = append(acdValues, acd)
}
}
asrValues.Sort()
acdValues.Sort()
asr = utils.Avg(asrValues)
acd = utils.Avg(acdValues)
if ts.Entry.Strategy == LCR_STRATEGY_QOS_WITH_THRESHOLD {
qosSortParams = ts.Entry.GetParams()
}
if ts.Entry.Strategy == LCR_STRATEGY_QOS_WITH_THRESHOLD {
// filter suppliers by qos thresholds
asrMin, asrMax, acdMin, acdMax := ts.Entry.GetQOSLimits()
// skip current supplier if off limits
if asrMin > 0 && asrValues[0] < asrMin {
continue
}
if asrMax > 0 && asrValues[len(asrValues)-1] > asrMax {
continue
}
if acdMin > 0 && acdValues[0] < float64(acdMin) {
continue
}
if acdMax > 0 && acdValues[len(acdValues)-1] > float64(acdMax) {
continue
var asr, acd float64
var qosSortParams []string
if lcrCost.Entry.Strategy == LCR_STRATEGY_QOS || lcrCost.Entry.Strategy == LCR_STRATEGY_QOS_WITH_THRESHOLD {
rpfKey := utils.ConcatenatedKey(ratingProfileSearchKey, supplier)
if rpf, err := dataStorage.GetRatingProfile(rpfKey, false); err == nil || rpf != nil {
rpf.RatingPlanActivations.Sort()
activeRas := rpf.RatingPlanActivations.GetActiveForCall(cd)
var cdrStatsQueueIds []string
for _, ra := range activeRas {
for _, qId := range ra.CdrStatQueueIds {
if qId != "" {
cdrStatsQueueIds = append(cdrStatsQueueIds, qId)
}
}
}
}
if cc, err := lcrCD.debit(cd.account, true, true); err != nil || cc == nil {
ts.SupplierCosts = append(ts.SupplierCosts, &LCRSupplierCost{
Supplier: supplier,
Error: err,
})
} else {
ts.SupplierCosts = append(ts.SupplierCosts, &LCRSupplierCost{
Supplier: supplier,
Cost: cc.Cost,
QOS: map[string]float64{
"ASR": asr,
"ACD": acd,
},
qosSortParams: qosSortParams,
})
var asrValues sort.Float64Slice
var acdValues sort.Float64Slice
for _, qId := range cdrStatsQueueIds {
statValues := make(map[string]float64)
if err := stats.GetValues(qId, &statValues); err != nil {
Logger.Warning(fmt.Sprintf("Error getting stats values for queue id %s: %v", qId, err))
}
if asr, exists := statValues[ASR]; exists {
asrValues = append(asrValues, asr)
}
if acd, exists := statValues[ACD]; exists {
acdValues = append(acdValues, acd)
}
}
asrValues.Sort()
acdValues.Sort()
asr = utils.Avg(asrValues)
acd = utils.Avg(acdValues)
if lcrCost.Entry.Strategy == LCR_STRATEGY_QOS_WITH_THRESHOLD {
qosSortParams = lcrCost.Entry.GetParams()
}
if lcrCost.Entry.Strategy == LCR_STRATEGY_QOS_WITH_THRESHOLD {
// filter suppliers by qos thresholds
asrMin, asrMax, acdMin, acdMax := lcrCost.Entry.GetQOSLimits()
// skip current supplier if off limits
if asrMin > 0 && asrValues[0] < asrMin {
continue
}
if asrMax > 0 && asrValues[len(asrValues)-1] > asrMax {
continue
}
if acdMin > 0 && acdValues[0] < float64(acdMin) {
continue
}
if acdMax > 0 && acdValues[len(acdValues)-1] > float64(acdMax) {
continue
}
}
}
}
// sort according to strategy
ts.Sort()
if cc, err := lcrCD.debit(cd.account, true, true); err != nil || cc == nil {
lcrCost.SupplierCosts = append(lcrCost.SupplierCosts, &LCRSupplierCost{
Supplier: supplier,
Error: err,
})
} else {
lcrCost.SupplierCosts = append(lcrCost.SupplierCosts, &LCRSupplierCost{
Supplier: supplier,
Cost: cc.Cost,
Duration: cc.GetDuration().String(),
QOS: map[string]float64{
"ASR": asr,
"ACD": acd,
},
qosSortParams: qosSortParams,
})
}
}
// sort according to strategy
lcrCost.Sort()
}
return lcrCost, nil
}

View File

@@ -57,10 +57,7 @@ type LCREntry struct {
precision int
}
type LCRCost []*LCRTimeSpan
type LCRTimeSpan struct {
StartTime time.Time
type LCRCost struct {
SupplierCosts []*LCRSupplierCost
Entry *LCREntry
}
@@ -68,6 +65,7 @@ type LCRTimeSpan struct {
type LCRSupplierCost struct {
Supplier string
Cost float64
Duration string
Error error
QOS map[string]float64
qosSortParams []string
@@ -142,8 +140,8 @@ func (es LCREntriesSorter) Swap(i, j int) {
}
func (es LCREntriesSorter) Less(j, i int) bool {
return es[i].precision < es[j].precision ||
(es[i].precision == es[j].precision && es[i].Weight < es[j].Weight)
return es[i].Weight < es[j].Weight ||
(es[i].Weight == es[j].Weight && es[i].precision < es[j].precision)
}
@@ -182,14 +180,14 @@ func (lcra *LCRActivation) GetLCREntryForPrefix(destination string) *LCREntry {
return nil
}
func (lts *LCRTimeSpan) Sort() {
switch lts.Entry.Strategy {
func (lc *LCRCost) Sort() {
switch lc.Entry.Strategy {
case LCR_STRATEGY_LOWEST:
sort.Sort(LowestSupplierCostSorter(lts.SupplierCosts))
sort.Sort(LowestSupplierCostSorter(lc.SupplierCosts))
case LCR_STRATEGY_HIGHEST:
sort.Sort(HighestSupplierCostSorter(lts.SupplierCosts))
sort.Sort(HighestSupplierCostSorter(lc.SupplierCosts))
case LCR_STRATEGY_QOS:
sort.Sort(QOSSorter(lts.SupplierCosts))
sort.Sort(QOSSorter(lc.SupplierCosts))
}
}

View File

@@ -182,8 +182,10 @@ func TestLcrGet(t *testing.T) {
Account: "rif",
Subject: "rif",
}
lcrs, err := cd.GetLCR(nil)
if err != nil || len(lcrs) != 1 {
t.Errorf("Bad lcr: %+v, %v", lcrs, err)
lcr, err := cd.GetLCR(nil)
//jsn, _ := json.Marshal(lcr)
//log.Print("LCR: ", string(jsn))
if err != nil || lcr == nil {
t.Errorf("Bad lcr: %+v, %v", lcr, err)
}
}

View File

@@ -237,7 +237,7 @@ func (rs *Responder) ProcessCdr(cdr *StoredCdr, reply *string) error {
func (rs *Responder) GetLCR(cd *CallDescriptor, reply *LCRCost) error {
lcrCost, err := cd.GetLCR(rs.Stats)
*reply = lcrCost
*reply = *lcrCost
return err
}