started work on rating plan activation time handling

This commit is contained in:
Radu Ioan Fericean
2013-11-12 23:57:40 +02:00
parent 5e7f7f6663
commit c6752be776
6 changed files with 167 additions and 79 deletions

View File

@@ -25,7 +25,6 @@ import (
"github.com/cgrates/cgrates/history"
"github.com/cgrates/cgrates/utils"
"log/syslog"
"strings"
"time"
)
@@ -107,7 +106,7 @@ type CallDescriptor struct {
CallDuration time.Duration // the call duration so far (till TimeEnd)
Amount float64
FallbackSubject string // the subject to check for destination if not found on primary subject
RatingInfos []*RatingInfo
RatingInfos RatingInfos
Increments Increments
userBalance *UserBalance
}
@@ -137,55 +136,84 @@ func (cd *CallDescriptor) getUserBalance() (ub *UserBalance, err error) {
/*
Restores the activation periods for the specified prefix from storage.
*/
func (cd *CallDescriptor) LoadRatingPlans() (destPrefixes []string, matchedSubject string, err error) {
matchedSubject = cd.GetKey()
/*if val, err := cache2go.GetXCached(cd.GetKey() + cd.Destination); err == nil {
xaps := val.(xCachedRatingPlans)
cd.RatingPlans = xaps.aps
return xaps.destPrefix, matchedSubject, nil
}*/
destPrefixes, matchedSubject, values, err := cd.getRatingPlansForPrefix(cd.GetKey(), 1)
func (cd *CallDescriptor) LoadRatingPlans() (err error) {
err = cd.getRatingPlansForPrefix(cd.GetKey(), 1)
if err != nil {
// try general fallback
fallbackKey := fmt.Sprintf("%s:%s:%s:%s", cd.Direction, cd.Tenant, cd.TOR, FALLBACK_SUBJECT)
// use the default subject
destPrefixes, matchedSubject, values, err = cd.getRatingPlansForPrefix(fallbackKey, 1)
err = cd.getRatingPlansForPrefix(fallbackKey, 1)
}
//load the rating plans
if err == nil && len(values) > 0 {
/*
xaps := xCachedRatingPlans{destPrefix, values, new(cache2go.XEntry)}
xaps.XCache(cd.GetKey()+cd.Destination, debitPeriod+5*time.Second, xaps)
*/
cd.RatingInfos = values
if err != nil || !cd.continousRatingInfos() {
err = errors.New("Could not determine rating plans for call")
return
}
return
}
func (cd *CallDescriptor) getRatingPlansForPrefix(key string, recursionDepth int) (foundPrefixes []string, matchedSubject string, ris []*RatingInfo, err error) {
matchedSubject = key
func (cd *CallDescriptor) getRatingPlansForPrefix(key string, recursionDepth int) (err error) {
if recursionDepth > RECURSION_MAX_DEPTH {
err = errors.New("Max fallback recursion depth reached!" + key)
return
}
rp, err := storageGetter.GetRatingProfile(key)
if err != nil || rp == nil {
return nil, "", nil, err
return err
}
foundPrefixes, ris, err = rp.GetRatingPlansForPrefix(cd)
if err != nil {
if rp.FallbackKey != "" {
if err = rp.GetRatingPlansForPrefix(cd); err != nil {
// try rating profile fallback
if len(rp.FallbackKeys) > 0 {
recursionDepth++
for _, fbk := range strings.Split(rp.FallbackKey, FALLBACK_SEP) {
if destPrefix, matchedSubject, values, err := cd.getRatingPlansForPrefix(fbk, recursionDepth); err == nil {
return destPrefix, matchedSubject, values, err
for _, fbk := range rp.FallbackKeys {
if err := cd.getRatingPlansForPrefix(fbk, recursionDepth); err == nil {
return err
}
}
}
}
return
}
// checks if there is rating info for the entire call duration
func (cd *CallDescriptor) continousRatingInfos() bool {
if len(cd.RatingInfos) == 0 || cd.RatingInfos[0].ActivationTime.After(cd.TimeStart) {
return false
}
for _, ri := range cd.RatingInfos {
if ri.RateIntervals == nil {
return false
}
}
return true
}
// adds a rating infos only if that call period is not already covered
// returns true if added
func (cd *CallDescriptor) addRatingInfos(ris RatingInfos) bool {
if len(cd.RatingInfos) == 0 {
cd.RatingInfos = append(cd.RatingInfos, ris...)
return true
}
cd.RatingInfos.Sort()
// check if we dont have the start covered
if cd.RatingInfos[0].ActivationTime.After(cd.TimeStart) {
if ris[0].ActivationTime.Before(cd.RatingInfos[0].ActivationTime) {
cd.RatingInfos = append(cd.RatingInfos, ris[0])
cd.RatingInfos.Sort()
}
}
for _, ri := range cd.RatingInfos {
if ri.RateIntervals == nil {
for i, new_ri := range ris {
_ = i
_ = new_ri
}
}
}
return true
}
/*
Constructs the key for the storage lookup.
The prefixLen is limiting the length of the destination prefix.
@@ -307,7 +335,7 @@ func (cd *CallDescriptor) roundTimeSpansToIncrement(timespans []*TimeSpan) []*Ti
Creates a CallCost structure with the cost information calculated for the received CallDescriptor.
*/
func (cd *CallDescriptor) GetCost() (*CallCost, error) {
destPrefix, matchedSubject, err := cd.LoadRatingPlans()
err := cd.LoadRatingPlans()
if err != nil {
Logger.Err(fmt.Sprintf("error getting cost for key %v: %v", cd.GetUserBalanceKey(), err))
return &CallCost{Cost: -1}, err
@@ -325,17 +353,19 @@ func (cd *CallDescriptor) GetCost() (*CallCost, error) {
}
// global rounding
cost = utils.Round(cost, roundingDecimals, roundingMethod)
startIndex := len(fmt.Sprintf("%s:%s:%s:", cd.Direction, cd.Tenant, cd.TOR))
//startIndex := len(fmt.Sprintf("%s:%s:%s:", cd.Direction, cd.Tenant, cd.TOR))
cc := &CallCost{
Direction: cd.Direction,
TOR: cd.TOR,
Tenant: cd.Tenant,
Subject: matchedSubject[startIndex:],
Account: cd.Account,
Destination: strings.Join(destPrefix, ";"),
Cost: cost,
ConnectFee: connectionFee,
Timespans: timespans}
Direction: cd.Direction,
TOR: cd.TOR,
Tenant: cd.Tenant,
// TODO, FIXME: find out where to put matched subject
//Subject: matchedSubject[startIndex:],
Account: cd.Account,
// TODO, FIXME: find out where to put matched prfixes
//Destination: strings.Join(destPrefix, ";"),
Cost: cost,
ConnectFee: connectionFee,
Timespans: timespans}
//Logger.Info(fmt.Sprintf("<Rater> Get Cost: %s => %v", cd.GetKey(), cc))
return cc, err
}
@@ -350,7 +380,7 @@ func (cd *CallDescriptor) GetMaxSessionTime(startTime time.Time) (seconds float6
if cd.CallDuration == 0 {
cd.CallDuration = cd.TimeEnd.Sub(cd.TimeStart)
}
_, _, err = cd.LoadRatingPlans()
err = cd.LoadRatingPlans()
if err != nil {
Logger.Err(fmt.Sprintf("error getting cost for key %v: %v", cd.GetUserBalanceKey(), err))
return 0, err

View File

@@ -349,17 +349,20 @@ func (csvr *CSVReader) LoadRatingProfiles() (err error) {
if !exists {
return errors.New(fmt.Sprintf("Could not load destination rate timings for tag: %v", record[5]))
}
rp.RatingPlanActivations = append(rp.RatingPlanActivations, &RatingPlanActivation{at, record[5]})
rpa := &RatingPlanActivation{
ActivationTime: at,
RatingPlanId: record[5],
}
if fallbacksubject != "" {
var sslice utils.StringSlice = rpa.FallbackKeys
for _, fbs := range strings.Split(fallbacksubject, ";") {
newKey := fmt.Sprintf("%s:%s:%s:%s", direction, tenant, tor, fbs)
var sslice utils.StringSlice = strings.Split(rp.FallbackKey, ";")
if !sslice.Contains(newKey) {
rp.FallbackKey += newKey + ";"
rpa.FallbackKeys = append(rpa.FallbackKeys, newKey)
}
}
rp.FallbackKey = strings.TrimRight(rp.FallbackKey, ";")
}
rp.RatingPlanActivations = append(rp.RatingPlanActivations, rpa)
csvr.ratingProfiles[rp.Id] = rp
}
return

View File

@@ -560,11 +560,11 @@ func TestLoadRatingProfiles(t *testing.T) {
}
rp := csvr.ratingProfiles["*out:test:0:trp"]
expected := &RatingProfile{
Id: "*out:test:0:trp",
FallbackKey: "*out:test:0:rif;*out:test:0:danb",
Id: "*out:test:0:trp",
RatingPlanActivations: RatingPlanActivations{&RatingPlanActivation{
ActivationTime: time.Date(2013, 10, 1, 0, 0, 0, 0, time.UTC),
RatingPlanId: "TDRT",
FallbackKeys: []string{"*out:test:0:rif", "*out:test:0:danb"},
}},
}
if !reflect.DeepEqual(rp, expected) {

View File

@@ -35,8 +35,8 @@ func TestApRestoreFromStorage(t *testing.T) {
Subject: "rif:from:tm",
Destination: "49"}
cd.LoadRatingPlans()
if len(cd.RatingInfos) != 2 {
t.Error("Error restoring activation periods: ", cd.RatingInfos)
if len(cd.RatingInfos) != 1 {
t.Errorf("Error restoring activation periods: %+v", cd.RatingInfos[0])
}
}
@@ -94,7 +94,7 @@ func TestFallbackMultiple(t *testing.T) {
Subject: "fall",
Destination: "0723045"}
cd.LoadRatingPlans()
if len(cd.RatingInfos) != 2 {
if len(cd.RatingInfos) != 1 {
t.Errorf("Error restoring rating plans: %+v", cd.RatingInfos)
}
}
@@ -200,6 +200,22 @@ func TestApAddRateIntervalGroups(t *testing.T) {
}
}
func TestGetActiveForCall(t *testing.T) {
rpas := RatingPlanActivations{
&RatingPlanActivation{ActivationTime: time.Date(2013, 1, 1, 0, 0, 0, 0, time.UTC)},
&RatingPlanActivation{ActivationTime: time.Date(2013, 11, 12, 11, 40, 0, 0, time.UTC)},
&RatingPlanActivation{ActivationTime: time.Date(2013, 11, 13, 0, 0, 0, 0, time.UTC)},
}
cd := &CallDescriptor{
TimeStart: time.Date(2013, 11, 12, 11, 39, 0, 0, time.UTC),
TimeEnd: time.Date(2013, 11, 12, 11, 45, 0, 0, time.UTC),
}
active := rpas.GetActiveForCall(cd)
if len(active) != 2 {
t.Errorf("Error getting active rating plans: %+v", active)
}
}
/**************************** Benchmarks *************************************/
func BenchmarkRatingPlanMarshalJson(b *testing.B) {

View File

@@ -27,15 +27,16 @@ import (
type RatingProfile struct {
Id string
FallbackKey string // FallbackKey is used as complete combination of Tenant:TOR:Direction:Subject
RatingPlanActivations RatingPlanActivations
Tag, Tenant, TOR, Direction, Subject string // used only for loading
DestRatesTimingTag, RatesFallbackSubject, ActivationTime string // used only for loading
Tag, Tenant, TOR, Direction, Subject string // used only for loading
DestRatesTimingTag, RatesFallbackSubject, ActivationTime string // used only for loading
FallbackKeys []string // used only for loading
}
type RatingPlanActivation struct {
ActivationTime time.Time
RatingPlanId string
FallbackKeys []string
}
func (rpa *RatingPlanActivation) Equal(orpa *RatingPlanActivation) bool {
@@ -60,45 +61,84 @@ func (rpas RatingPlanActivations) Sort() {
sort.Sort(rpas)
}
func (rpas RatingPlanActivations) GetActiveForCall(cd *CallDescriptor) RatingPlanActivations {
rpas.Sort()
lastBeforeCallStart := 0
firstAfterCallEnd := len(rpas)
for index, rpa := range rpas {
if rpa.ActivationTime.Before(cd.TimeStart) || rpa.ActivationTime.Equal(cd.TimeStart) {
lastBeforeCallStart = index
}
if rpa.ActivationTime.After(cd.TimeEnd) {
firstAfterCallEnd = index
break
}
}
return rpas[lastBeforeCallStart:firstAfterCallEnd]
}
type RatingInfo struct {
MatchedSubject string
MatchedPrefix string
ActivationTime time.Time
RateIntervals RateIntervalList
}
type RatingInfos []*RatingInfo
func (ris RatingInfos) Len() int {
return len(ris)
}
func (ris RatingInfos) Swap(i, j int) {
ris[i], ris[j] = ris[j], ris[i]
}
func (ris RatingInfos) Less(i, j int) bool {
return ris[i].ActivationTime.Before(ris[j].ActivationTime)
}
func (ris RatingInfos) Sort() {
sort.Sort(ris)
}
// TODO: what happens if there is no match for part of the call
func (rp *RatingProfile) GetRatingPlansForPrefix(cd *CallDescriptor) (foundPrefixes []string, ris []*RatingInfo, err error) {
rp.RatingPlanActivations.Sort()
for _, rpa := range rp.RatingPlanActivations {
if rpa.ActivationTime.Before(cd.TimeEnd) {
rpl, err := storageGetter.GetRatingPlan(rpa.RatingPlanId)
if err != nil || rpl == nil {
func (rp *RatingProfile) GetRatingPlansForPrefix(cd *CallDescriptor) (err error) {
var ris RatingInfos
for _, rpa := range rp.RatingPlanActivations.GetActiveForCall(cd) {
rpl, err := storageGetter.GetRatingPlan(rpa.RatingPlanId)
if err != nil || rpl == nil {
Logger.Err(fmt.Sprintf("Error checking destination: %v", err))
continue
}
bestPrecision := 0
var rps RateIntervalList
for dId, _ := range rpl.DestinationRates {
//precision, err := storageGetter.DestinationContainsPrefix(dId, cd.Destination)
d, err := storageGetter.GetDestination(dId)
if err != nil {
Logger.Err(fmt.Sprintf("Error checking destination: %v", err))
continue
}
bestPrecision := 0
var rps RateIntervalList
for dId, _ := range rpl.DestinationRates {
//precision, err := storageGetter.DestinationContainsPrefix(dId, cd.Destination)
d, err := storageGetter.GetDestination(dId)
if err != nil {
Logger.Err(fmt.Sprintf("Error checking destination: %v", err))
continue
}
precision := d.containsPrefix(cd.Destination)
if precision > bestPrecision {
bestPrecision = precision
rps = rpl.RateIntervalList(dId)
}
precision := d.containsPrefix(cd.Destination)
if precision > bestPrecision {
bestPrecision = precision
rps = rpl.RateIntervalList(dId)
}
if bestPrecision > 0 {
ris = append(ris, &RatingInfo{rpa.ActivationTime, rps})
foundPrefixes = append(foundPrefixes, cd.Destination[:bestPrecision])
}
if bestPrecision > 0 {
ris = append(ris, &RatingInfo{rp.Id, cd.Destination[:bestPrecision], rpa.ActivationTime, rps})
} else {
// mark the end of previous!
if len(cd.RatingInfos) > 0 {
ris = append(ris, &RatingInfo{"", "", rpa.ActivationTime, nil})
}
}
}
if len(ris) > 0 {
return foundPrefixes, ris, nil
cd.addRatingInfos(ris)
return
}
return nil, nil, errors.New("not found")
return errors.New("not found")
}

View File

@@ -1104,12 +1104,11 @@ func (self *SQLStorage) GetTpRatingProfiles(tpid, tag string) (map[string]*Ratin
if fallback_subject != "" {
for _, fbs := range strings.Split(fallback_subject, ";") {
newKey := fmt.Sprintf("%s:%s:%s:%s", direction, tenant, tor, fbs)
var sslice utils.StringSlice = strings.Split(rp.FallbackKey, ";")
var sslice utils.StringSlice = rp.FallbackKeys
if !sslice.Contains(newKey) {
rp.FallbackKey += newKey + ";"
rp.FallbackKeys = append(rp.FallbackKeys, newKey)
}
}
rp.FallbackKey = strings.TrimRight(rp.FallbackKey, ";")
}
}
return rpfs, nil