From 20c086e42a839eeca3d5776ced5947b2afac52ef Mon Sep 17 00:00:00 2001 From: DanB Date: Thu, 12 Nov 2020 17:54:56 +0100 Subject: [PATCH] RateS computeRateSIntervals implementation, engine.CostForIntervals --- engine/datamanager.go | 3 ++ engine/rateprofile.go | 69 ++++++++++++++++++++++++++++++++++----- rates/librates.go | 73 ++++++++++++++++++++++++++++++++++-------- rates/librates_test.go | 48 ++++++++++++++++++++++++--- utils/decimal.go | 25 +++++++++++---- 5 files changed, 187 insertions(+), 31 deletions(-) diff --git a/engine/datamanager.go b/engine/datamanager.go index 90818d1b3..bfb4b8a54 100644 --- a/engine/datamanager.go +++ b/engine/datamanager.go @@ -3091,6 +3091,9 @@ func (dm *DataManager) GetRateProfile(tenant, id string, cacheRead, cacheWrite b return nil, err } } + if err = rpp.Compile(); err != nil { + return nil, err + } if cacheWrite { if errCh := Cache.Set(utils.CacheRateProfiles, tntID, rpp, nil, cacheCommit(transactionID), transactionID); errCh != nil { diff --git a/engine/rateprofile.go b/engine/rateprofile.go index 51ef388a9..1fbba3c6a 100644 --- a/engine/rateprofile.go +++ b/engine/rateprofile.go @@ -73,7 +73,7 @@ type Rate struct { Blocker bool // RateBlocker will make this rate recurrent, deactivating further intervals IntervalRates []*IntervalRate - sched cron.Schedule // compiled version of activation time as cron.Schedule interface + sched cron.Schedule // compiled version of activation times as cron.Schedule interface uID string } @@ -88,7 +88,9 @@ type IntervalRate struct { Increment time.Duration // RateIncrement Value float64 // RateValue - val *utils.Decimal // cached version of the Decimal + decVal *utils.Decimal // cached version of the Value converted to Decimal for operations + decUnit *utils.Decimal // cached version of the Unit converted to Decimal for operations + decIcrm *utils.Decimal // cached version of the Increment converted to Decimal for operations } func (rt *Rate) Compile() (err error) { @@ -99,6 +101,11 @@ func (rt *Rate) Compile() (err error) { if rt.sched, err = cron.ParseStandard(aTime); err != nil { return } + for _, iRt := range rt.IntervalRates { + iRt.decVal = utils.NewDecimalFromFloat64(iRt.Value) + iRt.decUnit = utils.NewDecimalFromUint64(uint64(iRt.Unit)) + iRt.decIcrm = utils.NewDecimalFromUint64(uint64(iRt.Increment)) + } return } @@ -126,6 +133,21 @@ func (rt *Rate) RunTimes(sTime, eTime time.Time, verbosity int) (aTimes [][]time return nil, utils.ErrMaxIterationsReached } +// DecimalValue exports the decVal variable +func (rIt *IntervalRate) DecimalValue() *utils.Decimal { + return rIt.decVal +} + +// DecimalUnit exports the decUnit variable +func (rIt *IntervalRate) DecimalUnit() *utils.Decimal { + return rIt.decUnit +} + +// DecimalIncrement exports the decUnit variable +func (rIt *IntervalRate) DecimalIncrement() *utils.Decimal { + return rIt.decIcrm +} + // RateProfileWithOpts is used in replicatorV1 for dispatcher type RateProfileWithOpts struct { *RateProfile @@ -138,16 +160,16 @@ type RateSInterval struct { Increments []*RateSIncrement CompressFactor int64 - cost *utils.Decimal // unexported total cost + cost *utils.Decimal // unexported total interval cost } type RateSIncrement struct { - Rate *Rate + UsageStart time.Duration Usage time.Duration + Rate *Rate IntervalRateIndex int CompressFactor int64 - Cost float64 cost *utils.Decimal // unexported total increment cost } @@ -165,6 +187,15 @@ func (rIv *RateSInterval) CompressEquals(rIv2 *RateSInterval) (eq bool) { return } +func (rIv *RateSInterval) Cost() *utils.Decimal { + if rIv.cost == nil { + for _, incrm := range rIv.Increments { + rIv.cost = utils.NewDecimal().Add(rIv.cost, incrm.Cost()) + } + } + return rIv.cost +} + // CompressEquals compares two RateSIncrement for Compress function func (rIcr *RateSIncrement) CompressEquals(rIcr2 *RateSIncrement) (eq bool) { if rIcr.Rate.UID() != rIcr2.Rate.UID() { @@ -173,11 +204,33 @@ func (rIcr *RateSIncrement) CompressEquals(rIcr2 *RateSIncrement) (eq bool) { if rIcr.Usage != rIcr2.Usage { return } - if rIcr.Cost != rIcr2.Cost { - return - } if rIcr.CompressFactor != rIcr2.CompressFactor { return } return true } + +// Cost computes the Cost on RateSIncrement +func (rIcr *RateSIncrement) Cost() *utils.Decimal { + if rIcr.cost == nil { + icrRt := rIcr.Rate.IntervalRates[rIcr.IntervalRateIndex] + icrCost := icrRt.DecimalValue() + if icrRt.Unit != icrRt.Increment { + icrCost = utils.NewDecimal().Divide( + utils.NewDecimal().Multiply(icrCost, icrRt.DecimalIncrement()), + icrRt.DecimalUnit()) + } + rIcr.cost = utils.NewDecimal().Multiply( + icrCost, + utils.NewDecimalFromUint64(uint64(rIcr.CompressFactor))) + } + return rIcr.cost +} + +// CostForIntervals sums the costs for all intervals +func CostForIntervals(rtIvls []*RateSInterval) (cost *utils.Decimal) { + for _, rtIvl := range rtIvls { + cost = utils.NewDecimal().Add(cost, rtIvl.Cost()) + } + return +} diff --git a/rates/librates.go b/rates/librates.go index 325d84647..e133d46c2 100644 --- a/rates/librates.go +++ b/rates/librates.go @@ -177,25 +177,72 @@ func orderRatesOnIntervals(aRts []*engine.Rate, sTime time.Time, usage time.Dura return } -// costWithRates will give out the cost projection for the given orderedRates and usage -func costWithRates(rts []*orderedRate, usage time.Duration) (rtIvls []*engine.RateSInterval, err error) { - //var usageSIdx time.Duration // usageStart for one rate +// computeRateSIntervals will give out the cost projection for the given orderedRates and usage +func computeRateSIntervals(rts []*orderedRate, usage time.Duration) (rtIvls []*engine.RateSInterval, err error) { + var rtUsageSIdx time.Duration // rtUsageSIdx for one rate for i, rt := range rts { - var usageEIdx time.Duration - if i != len(rts)-1 { - usageEIdx = rts[i+1].Duration + isLastRt := i == len(rts)-1 + var rtUsageEIdx time.Duration + if !isLastRt { + rtUsageEIdx = rts[i+1].Duration + } else { + rtUsageEIdx = usage } - var iRts []*engine.IntervalRate - for _, iRt := range rt.IntervalRates { - if usageEIdx == 0 || iRt.IntervalStart < usageEIdx { - iRts = append(iRts, iRt) + var rIcmts []*engine.RateSIncrement + iRtUsageSIdx := rtUsageSIdx + iRtUsageEIdx := rtUsageEIdx + for j, iRt := range rt.IntervalRates { + if iRtUsageSIdx >= rtUsageEIdx { // charged enough for interval + break } + // make sure we bill from start + if j == 0 && iRt.IntervalStart > iRtUsageSIdx { + return nil, fmt.Errorf("intervalStart for rate: <%s> higher than usage: %v", + rt.UID(), iRtUsageSIdx) + } + isLastIRt := j == len(rt.IntervalRates)-1 + if iRt.IntervalStart > iRtUsageSIdx || + (!isLastIRt && rt.IntervalRates[j+1].IntervalStart <= iRtUsageSIdx) { + break // the rates should be already ordered, break here + } + if !isLastIRt { + iRtUsageEIdx = rt.IntervalRates[j+1].IntervalStart + } else { + iRtUsageEIdx = rtUsageEIdx + } + iRtUsage := iRtUsageEIdx - iRtUsageSIdx + if iRtUsageEIdx == time.Duration(0) { + return nil, fmt.Errorf("zero usage to be charged with rate: <%s>", rt.UID()) + } + if iRt.Increment == time.Duration(0) { + return nil, fmt.Errorf("zero increment to be charged within rate: <%s>", rt.UID()) + } + intUsage := int64(iRtUsage) + intIncrm := int64(iRt.Increment) + cmpFactor := intUsage / intIncrm + if intUsage%intIncrm != 0 { + cmpFactor += 1 // int division has used math.Floor, need Ceil + } + rIcrm := &engine.RateSIncrement{ + UsageStart: iRtUsageSIdx, + Usage: iRtUsage, + Rate: rt.Rate, + IntervalRateIndex: j, + CompressFactor: cmpFactor, + } + rIcmts = append(rIcmts, rIcrm) + iRtUsageSIdx += iRtUsage + } - //fmt.Printf("iRts: %+v\n", iRts) - if usageEIdx == 0 { + rtIvls = append(rtIvls, + &engine.RateSInterval{ + UsageStart: rtUsageSIdx, + Increments: rIcmts, + CompressFactor: 1}) + if iRtUsageSIdx >= usage { // charged enough for the usage break } - //usageSIdx = usageEIdx // continue for the next interval + rtUsageSIdx = rtUsageEIdx // continue for the next interval } return } diff --git a/rates/librates_test.go b/rates/librates_test.go index 34536c910..4979c9306 100644 --- a/rates/librates_test.go +++ b/rates/librates_test.go @@ -1575,7 +1575,7 @@ func TestOrderRatesOnIntervalStartLowerThanEndIdx(t *testing.T) { } } -func TestCostWithRates(t *testing.T) { +func TestComputeRateSIntervals(t *testing.T) { rt0 := &engine.Rate{ ID: "RATE0", IntervalRates: []*engine.IntervalRate{ @@ -1627,9 +1627,49 @@ func TestCostWithRates(t *testing.T) { }, } - //eRtIvls := []*engine.RateSInterval{} - var eRtIvls []*engine.RateSInterval - if rtIvls, err := costWithRates(rts, time.Duration(130*time.Second)); err != nil { + eRtIvls := []*engine.RateSInterval{ + { + UsageStart: time.Duration(0), + Increments: []*engine.RateSIncrement{ + { + UsageStart: time.Duration(0), + Usage: time.Duration(time.Minute), + Rate: rt0, + IntervalRateIndex: 0, + CompressFactor: 1, + }, + { + UsageStart: time.Duration(time.Minute), + Usage: time.Duration(30 * time.Second), + Rate: rt0, + IntervalRateIndex: 1, + CompressFactor: 30, + }, + }, + CompressFactor: 1, + }, + { + UsageStart: time.Duration(90 * time.Second), + Increments: []*engine.RateSIncrement{ + { + UsageStart: time.Duration(90 * time.Second), + Usage: time.Duration(30 * time.Second), + Rate: rt1, + IntervalRateIndex: 0, + CompressFactor: 30, + }, + { + UsageStart: time.Duration(2 * time.Minute), + Usage: time.Duration(10 * time.Second), + Rate: rt1, + IntervalRateIndex: 1, + CompressFactor: 10, + }, + }, + CompressFactor: 1, + }, + } + if rtIvls, err := computeRateSIntervals(rts, time.Duration(130*time.Second)); err != nil { t.Error(err) } else if !reflect.DeepEqual(eRtIvls, rtIvls) { t.Errorf("expecting: %+v, received: %+v", eRtIvls, rtIvls) diff --git a/utils/decimal.go b/utils/decimal.go index bd64558b8..6dede3703 100644 --- a/utils/decimal.go +++ b/utils/decimal.go @@ -26,6 +26,10 @@ func NewDecimalFromFloat64(x float64) *Decimal { return &Decimal{new(decimal.Big).SetFloat64(x)} } +func NewDecimalFromUint64(x uint64) *Decimal { + return &Decimal{new(decimal.Big).SetUint64(x)} +} + func NewDecimal() *Decimal { return &Decimal{new(decimal.Big)} } @@ -41,15 +45,24 @@ func (d *Decimal) Float64() (f float64) { } func (d *Decimal) MarshalJSON() ([]byte, error) { - if d.Big == nil { - d.Big = new(decimal.Big) - } return d.Big.MarshalText() } func (d *Decimal) UnmarshalJSON(data []byte) error { - if d.Big == nil { - d.Big = new(decimal.Big) - } return d.Big.UnmarshalJSON(data) } + +func (d *Decimal) Divide(x, y *Decimal) *Decimal { + d.Big.Quo(x.Big, y.Big) + return d +} + +func (d *Decimal) Multiply(x, y *Decimal) *Decimal { + d.Big.Mul(x.Big, y.Big) + return d +} + +func (d *Decimal) Add(x, y *Decimal) *Decimal { + d.Big.Add(x.Big, y.Big) + return d +}