From b1b9a81fc14073c02b52ab0789182e9fa39d84ed Mon Sep 17 00:00:00 2001 From: Trial97 Date: Wed, 7 Oct 2020 18:16:59 +0300 Subject: [PATCH] Added RoundIncrement to EventCost --- engine/callcost.go | 5 +- engine/cdrs.go | 4 +- engine/eventcost.go | 221 ++++++++++++++++++++++++--------------- engine/eventcost_test.go | 43 +++++++- engine/timespans.go | 3 +- sessions/sessions.go | 83 +++++++++------ utils/consts.go | 1 + 7 files changed, 234 insertions(+), 126 deletions(-) diff --git a/engine/callcost.go b/engine/callcost.go index 884272cd4..2c2d2b3fd 100644 --- a/engine/callcost.go +++ b/engine/callcost.go @@ -203,8 +203,9 @@ func (cc *CallCost) Round() { //log.Print(cost, roundedCost, correctionCost) if correctionCost != 0 { ts.RoundIncrement = &Increment{ - Cost: correctionCost, - BalanceInfo: inc.BalanceInfo, + Cost: correctionCost, + BalanceInfo: inc.BalanceInfo, + CompressFactor: 1, } totalCorrectionCost += correctionCost ts.Cost += correctionCost diff --git a/engine/cdrs.go b/engine/cdrs.go index fd497b1dc..3c65c6450 100644 --- a/engine/cdrs.go +++ b/engine/cdrs.go @@ -174,7 +174,7 @@ func (cdrS *CDRServer) rateCDR(cdr *CDRWithArgDispatcher) ([]*CDR, error) { cdrClone.OriginID = smCost.OriginID if cdr.Usage == 0 { cdrClone.Usage = smCost.Usage - } else if smCost.CostDetails.GetUsage() != cdr.Usage { + } else if smCost.Usage != cdr.Usage { if err = cdrS.refundEventCost(smCost.CostDetails, cdrClone.RequestType, cdrClone.ToR); err != nil { return nil, err @@ -832,7 +832,7 @@ func (cdrS *CDRServer) V2StoreSessionCost(args *ArgsV2CDRSStoreSMCost, reply *st OriginID: args.Cost.OriginID, CostSource: args.Cost.CostSource, Usage: args.Cost.Usage, - CostDetails: args.Cost.CostDetails}, + CostDetails: NewEventCostFromCallCost(cc, args.Cost.CGRID, args.Cost.RunID)}, args.CheckDuplicate); err != nil { err = utils.NewErrServerError(err) return diff --git a/engine/eventcost.go b/engine/eventcost.go index 80134d5c2..c334cd04d 100644 --- a/engine/eventcost.go +++ b/engine/eventcost.go @@ -56,53 +56,77 @@ func NewEventCostFromCallCost(cc *CallCost, cgrID, runID string) (ec *EventCost) "DestinationID": ts.MatchedDestId, "RatingPlanID": ts.RatingPlanId} cIl.RatingID = ec.ratingIDForRateInterval(ts.RateInterval, rf) if len(ts.Increments) != 0 { - cIl.Increments = make([]*ChargingIncrement, len(ts.Increments)) + cIl.Increments = make([]*ChargingIncrement, 0, len(ts.Increments)+1) } - for j, incr := range ts.Increments { - cIt := &ChargingIncrement{ - Usage: incr.Duration, - Cost: incr.Cost, - CompressFactor: incr.CompressFactor} - if incr.BalanceInfo == nil { - continue - } - //AccountingID - if incr.BalanceInfo.Unit != nil { - // 2 balances work-around - ecUUID := utils.META_NONE // populate no matter what due to Unit not nil - if incr.BalanceInfo.Monetary != nil { - if uuid := ec.Accounting.GetIDWithSet( - &BalanceCharge{ - AccountID: incr.BalanceInfo.AccountID, - BalanceUUID: incr.BalanceInfo.Monetary.UUID, - Units: incr.Cost, - RatingID: ec.ratingIDForRateInterval(incr.BalanceInfo.Monetary.RateInterval, rf), - }); uuid != "" { - ecUUID = uuid - } - } - cIt.AccountingID = ec.Accounting.GetIDWithSet( - &BalanceCharge{ - AccountID: incr.BalanceInfo.AccountID, - BalanceUUID: incr.BalanceInfo.Unit.UUID, - Units: incr.BalanceInfo.Unit.Consumed, - RatingID: ec.ratingIDForRateInterval(incr.BalanceInfo.Unit.RateInterval, rf), - ExtraChargeID: ecUUID}) - } else if incr.BalanceInfo.Monetary != nil { // Only monetary - cIt.AccountingID = ec.Accounting.GetIDWithSet( - &BalanceCharge{ - AccountID: incr.BalanceInfo.AccountID, - BalanceUUID: incr.BalanceInfo.Monetary.UUID, - Units: incr.Cost, - RatingID: ec.ratingIDForRateInterval(incr.BalanceInfo.Monetary.RateInterval, rf)}) - } - cIl.Increments[j] = cIt + for _, incr := range ts.Increments { + cIl.Increments = append(cIl.Increments, ec.newChargingIncrement(incr, rf, false)) + } + if ts.RoundIncrement != nil { + rIncr := ec.newChargingIncrement(ts.RoundIncrement, rf, true) + rIncr.Cost = -rIncr.Cost + cIl.Increments = append(cIl.Increments, rIncr) } ec.Charges[i] = cIl } return } +// newChargingIncrement creates ChargingIncrement from a Increment +// special case if is the roundIncrement the rateID is *rounding +func (ec *EventCost) newChargingIncrement(incr *Increment, rf RatingMatchedFilters, roundedIncrement bool) (cIt *ChargingIncrement) { + cIt = &ChargingIncrement{ + Usage: incr.Duration, + Cost: incr.Cost, + CompressFactor: incr.CompressFactor, + } + if incr.BalanceInfo == nil { + return + } + rateID := utils.MetaRounding + //AccountingID + if incr.BalanceInfo.Unit != nil { + // 2 balances work-around + ecUUID := utils.META_NONE // populate no matter what due to Unit not nil + if incr.BalanceInfo.Monetary != nil { + if !roundedIncrement { + rateID = ec.ratingIDForRateInterval(incr.BalanceInfo.Monetary.RateInterval, rf) + } + if uuid := ec.Accounting.GetIDWithSet( + &BalanceCharge{ + AccountID: incr.BalanceInfo.AccountID, + BalanceUUID: incr.BalanceInfo.Monetary.UUID, + Units: incr.Cost, + RatingID: rateID, + }); uuid != "" { + ecUUID = uuid + } + } + if !roundedIncrement { + rateID = ec.ratingIDForRateInterval(incr.BalanceInfo.Unit.RateInterval, rf) + } + cIt.AccountingID = ec.Accounting.GetIDWithSet( + &BalanceCharge{ + AccountID: incr.BalanceInfo.AccountID, + BalanceUUID: incr.BalanceInfo.Unit.UUID, + Units: incr.BalanceInfo.Unit.Consumed, + RatingID: rateID, + ExtraChargeID: ecUUID, + }) + } else if incr.BalanceInfo.Monetary != nil { // Only monetary + if !roundedIncrement { + rateID = ec.ratingIDForRateInterval(incr.BalanceInfo.Monetary.RateInterval, rf) + } + cIt.AccountingID = ec.Accounting.GetIDWithSet( + &BalanceCharge{ + AccountID: incr.BalanceInfo.AccountID, + BalanceUUID: incr.BalanceInfo.Monetary.UUID, + Units: incr.Cost, + RatingID: rateID, + }) + } + return +} + // EventCost stores cost for an Event type EventCost struct { CGRID string @@ -343,71 +367,94 @@ func (ec *EventCost) AsCallCost(tor string) *CallCost { ToR: utils.FirstNonEmpty(tor, utils.VOICE), Cost: ec.GetCost(), RatedUsage: float64(ec.GetUsage().Nanoseconds()), - AccountSummary: ec.AccountSummary} + AccountSummary: ec.AccountSummary, + } cc.Timespans = make(TimeSpans, len(ec.Charges)) for i, cIl := range ec.Charges { - ts := &TimeSpan{Cost: cIl.Cost(), + ts := &TimeSpan{ + Cost: cIl.Cost(), DurationIndex: *cIl.Usage(), - CompressFactor: cIl.CompressFactor} + CompressFactor: cIl.CompressFactor, + } if cIl.ecUsageIdx == nil { // index was not populated yet ec.ComputeEventCostUsageIndexes() } ts.TimeStart = ec.StartTime.Add(*cIl.ecUsageIdx) ts.TimeEnd = ts.TimeStart.Add( time.Duration(cIl.Usage().Nanoseconds() * int64(cIl.CompressFactor))) - if cIl.RatingID != "" { - if ec.Rating[cIl.RatingID].RatingFiltersID != "" { - rfs := ec.RatingFilters[ec.Rating[cIl.RatingID].RatingFiltersID] - ts.MatchedSubject = rfs[utils.Subject].(string) - ts.MatchedPrefix = rfs[utils.DestinationPrefix].(string) - ts.MatchedDestId = rfs[utils.DestinationID].(string) - ts.RatingPlanId = rfs[utils.RatingPlanID].(string) - } + if cIl.RatingID != "" && + ec.Rating[cIl.RatingID].RatingFiltersID != "" { + rfs := ec.RatingFilters[ec.Rating[cIl.RatingID].RatingFiltersID] + ts.MatchedSubject = rfs[utils.Subject].(string) + ts.MatchedPrefix = rfs[utils.DestinationPrefix].(string) + ts.MatchedDestId = rfs[utils.DestinationID].(string) + ts.RatingPlanId = rfs[utils.RatingPlanID].(string) } ts.RateInterval = ec.rateIntervalForRatingID(cIl.RatingID) - if len(cIl.Increments) != 0 { - ts.Increments = make(Increments, len(cIl.Increments)) - } - for j, cInc := range cIl.Increments { - incr := &Increment{Duration: cInc.Usage, Cost: cInc.Cost, CompressFactor: cInc.CompressFactor, BalanceInfo: new(DebitInfo)} - if cInc.AccountingID != "" { - cBC := ec.Accounting[cInc.AccountingID] - incr.BalanceInfo.AccountID = cBC.AccountID - var balanceType string - if cBC.BalanceUUID != "" { - if ec.AccountSummary != nil { - for _, b := range ec.AccountSummary.BalanceSummaries { - if b.UUID == cBC.BalanceUUID { - balanceType = b.Type - break - } - } - } - } - if utils.SliceHasMember([]string{utils.DATA, utils.VOICE}, balanceType) && cBC.ExtraChargeID == "" { - cBC.ExtraChargeID = utils.META_NONE // mark the balance to be exported as Unit type - } - if cBC.ExtraChargeID != "" { // have both monetary and data - // Work around, enforce logic with 2 balances for *voice/*monetary combination - // so we can stay compatible with CallCost - incr.BalanceInfo.Unit = &UnitInfo{UUID: cBC.BalanceUUID, Consumed: cBC.Units} - incr.BalanceInfo.Unit.RateInterval = ec.rateIntervalForRatingID(cBC.RatingID) - if cBC.ExtraChargeID != utils.META_NONE { - cBC = ec.Accounting[cBC.ExtraChargeID] // overwrite original balance so we can process it in one place - } - } - if cBC.ExtraChargeID != utils.META_NONE { - incr.BalanceInfo.Monetary = &MonetaryInfo{UUID: cBC.BalanceUUID} - incr.BalanceInfo.Monetary.RateInterval = ec.rateIntervalForRatingID(cBC.RatingID) - } + + incrs := cIl.Increments + if l := len(cIl.Increments); l != 0 { + if ec.Accounting[cIl.Increments[l-1].AccountingID].RatingID == utils.MetaRounding { + // special case: if the last increment is has the ratingID equal to *roundig + // we consider it as the roundIncrement + l-- + incrs = incrs[:l] + ts.RoundIncrement = ec.newIntervalFromCharge(cIl.Increments[l-1]) + ts.RoundIncrement.Cost = -ts.RoundIncrement.Cost } - ts.Increments[j] = incr + ts.Increments = make(Increments, l) + } + for j, cInc := range incrs { + ts.Increments[j] = ec.newIntervalFromCharge(cInc) } cc.Timespans[i] = ts } return cc } +// newIntervalFromCharge creates Increment from a ChargingIncrement +func (ec *EventCost) newIntervalFromCharge(cInc *ChargingIncrement) (incr *Increment) { + incr = &Increment{ + Duration: cInc.Usage, + Cost: cInc.Cost, + CompressFactor: cInc.CompressFactor, + BalanceInfo: new(DebitInfo), + } + if len(cInc.AccountingID) == 0 { + return + } + cBC := ec.Accounting[cInc.AccountingID] + incr.BalanceInfo.AccountID = cBC.AccountID + var balanceType string + if cBC.BalanceUUID != "" { + if ec.AccountSummary != nil { + for _, b := range ec.AccountSummary.BalanceSummaries { + if b.UUID == cBC.BalanceUUID { + balanceType = b.Type + break + } + } + } + } + if utils.SliceHasMember([]string{utils.DATA, utils.VOICE}, balanceType) && cBC.ExtraChargeID == "" { + cBC.ExtraChargeID = utils.META_NONE // mark the balance to be exported as Unit type + } + if cBC.ExtraChargeID != "" { // have both monetary and data + // Work around, enforce logic with 2 balances for *voice/*monetary combination + // so we can stay compatible with CallCost + incr.BalanceInfo.Unit = &UnitInfo{UUID: cBC.BalanceUUID, Consumed: cBC.Units} + incr.BalanceInfo.Unit.RateInterval = ec.rateIntervalForRatingID(cBC.RatingID) + if cBC.ExtraChargeID != utils.META_NONE { + cBC = ec.Accounting[cBC.ExtraChargeID] // overwrite original balance so we can process it in one place + } + } + if cBC.ExtraChargeID != utils.META_NONE { + incr.BalanceInfo.Monetary = &MonetaryInfo{UUID: cBC.BalanceUUID} + incr.BalanceInfo.Monetary.RateInterval = ec.rateIntervalForRatingID(cBC.RatingID) + } + return +} + // ratingGetIDFomEventCost retrieves UUID based on data from another EventCost func (ec *EventCost) ratingGetIDFomEventCost(oEC *EventCost, oRatingID string) string { if oRatingID == "" { diff --git a/engine/eventcost_test.go b/engine/eventcost_test.go index 1f44fb7cc..3760edcfc 100644 --- a/engine/eventcost_test.go +++ b/engine/eventcost_test.go @@ -323,13 +323,13 @@ func TestNewEventCostFromCallCost(t *testing.T) { Account: "dan", Destination: "+4986517174963", ToR: utils.VOICE, - Cost: 0.85, + Cost: 0.75, RatedUsage: 120.0, Timespans: TimeSpans{ &TimeSpan{ TimeStart: time.Date(2017, 1, 9, 16, 18, 21, 0, time.UTC), TimeEnd: time.Date(2017, 1, 9, 16, 19, 21, 0, time.UTC), - Cost: 0.25, + Cost: 0.15, RateInterval: &RateInterval{ // standard rating Timing: &RITiming{ StartTime: "00:00:00", @@ -354,6 +354,16 @@ func TestNewEventCostFromCallCost(t *testing.T) { MatchedDestId: "GERMANY", RatingPlanId: "RPL_RETAIL1", CompressFactor: 1, + RoundIncrement: &Increment{ + Cost: 0.1, + BalanceInfo: &DebitInfo{ + Monetary: &MonetaryInfo{UUID: "8c54a9e9-d610-4c82-bcb5-a315b9a65010", + ID: utils.MetaDefault, + Value: 9.9}, + AccountID: "cgrates.org:dan", + }, + CompressFactor: 1, + }, Increments: Increments{ &Increment{ // ConnectFee Cost: 0.1, @@ -500,10 +510,15 @@ func TestNewEventCostFromCallCost(t *testing.T) { AccountingID: "906bfd0f-035c-40a3-93a8-46f71627983e", CompressFactor: 30, }, + { + Cost: -0.1, + AccountingID: "44e97dec-8a7e-43d0-8b0a-e34a152", + CompressFactor: 1, + }, }, CompressFactor: 1, usage: utils.DurationPointer(time.Duration(60 * time.Second)), - cost: utils.Float64Pointer(0.25), + cost: utils.Float64Pointer(0.15), ecUsageIdx: utils.DurationPointer(time.Duration(0)), }, &ChargingInterval{ @@ -568,6 +583,13 @@ func TestNewEventCostFromCallCost(t *testing.T) { Units: 1, ExtraChargeID: "*none", }, + "44e97dec-8a7e-43d0-8b0a-e34a152": &BalanceCharge{ + AccountID: "cgrates.org:dan", + BalanceUUID: "8c54a9e9-d610-4c82-bcb5-a315b9a65010", + RatingID: "*rounding", + Units: 0.1, + ExtraChargeID: "", + }, }, RatingFilters: RatingFilters{ "7e73a00d-be53-4083-a1ee-8ee0b546c62a": RatingMatchedFilters{ @@ -676,6 +698,21 @@ func TestNewEventCostFromCallCost(t *testing.T) { utils.ToJSON(eEC.Rates[eEC.Rating[eEC.Accounting[eEC.Charges[0].Increments[2].AccountingID].RatingID].RatesID]), utils.ToJSON(ec.Rates[ec.Rating[ec.Accounting[ec.Charges[0].Increments[2].AccountingID].RatingID].RatesID])) } + + // Compare to expected EC + if !reflect.DeepEqual(eEC.Accounting[eEC.Charges[0].Increments[3].AccountingID], + ec.Accounting[ec.Charges[0].Increments[3].AccountingID]) { + t.Errorf("Expecting: %s, received: %s", + utils.ToJSON(eEC.Accounting[eEC.Charges[0].Increments[3].AccountingID]), + utils.ToJSON(ec.Accounting[ec.Charges[0].Increments[3].AccountingID])) + } + ec.Charges[0].Increments[3].AccountingID = eEC.Charges[0].Increments[3].AccountingID + if !reflect.DeepEqual(eEC.Charges[0].Increments[3], + ec.Charges[0].Increments[3]) { + t.Errorf("Expecting: %s, received: %s", + utils.ToJSON(eEC.Charges[0].Increments[3]), + utils.ToJSON(ec.Charges[0].Increments[3])) + } if len(ec.Accounting) != len(eEC.Accounting) { t.Errorf("Expecting: %+v, received: %+v", eEC, ec) } diff --git a/engine/timespans.go b/engine/timespans.go index 459c2a6ee..08ac76a83 100644 --- a/engine/timespans.go +++ b/engine/timespans.go @@ -446,9 +446,8 @@ func (ts *TimeSpan) CalculateCost() float64 { return 0 } return ts.RateInterval.GetCost(ts.GetDuration(), ts.GetGroupStart()) - } else { - return ts.Increments.GetTotalCost() * float64(ts.GetCompressFactor()) } + return ts.Increments.GetTotalCost() * float64(ts.GetCompressFactor()) } func (ts *TimeSpan) setRatingInfo(rp *RatingInfo) { diff --git a/sessions/sessions.go b/sessions/sessions.go index bf3c12074..439db1a02 100644 --- a/sessions/sessions.go +++ b/sessions/sessions.go @@ -598,14 +598,8 @@ func (sS *SessionS) refundSession(s *Session, sRunIdx int, rUsage time.Duration) // storeSCost will post the session cost to CDRs // not thread safe, need to be handled in a layer above func (sS *SessionS) storeSCost(s *Session, sRunIdx int) (err error) { - if sRunIdx >= len(s.SRuns) { - return errors.New("sRunIdx out of range") - } sr := s.SRuns[sRunIdx] - if sr.EventCost == nil { - return // no costs to save, ignore the operation - } - smCost := &engine.V2SMCost{ + smCost := &engine.SMCost{ CGRID: s.CGRID, CostSource: utils.MetaSessionS, RunID: sr.Event.GetStringIgnoreErrors(utils.RunID), @@ -614,7 +608,7 @@ func (sS *SessionS) storeSCost(s *Session, sRunIdx int) (err error) { Usage: sr.TotalUsage, CostDetails: sr.EventCost, } - argSmCost := &engine.ArgsV2CDRSStoreSMCost{ + argSmCost := &engine.AttrCDRSStoreSMCost{ Cost: smCost, CheckDuplicate: true, ArgDispatcher: s.ArgDispatcher, @@ -623,23 +617,46 @@ func (sS *SessionS) storeSCost(s *Session, sRunIdx int) (err error) { }, } var reply string - if err := sS.connMgr.Call(sS.cgrCfg.SessionSCfg().CDRsConns, nil, utils.CDRsV2StoreSessionCost, - argSmCost, &reply); err != nil { - if err == utils.ErrExists { + // use the v1 because it doesn't do rounding refund + if err := sS.connMgr.Call(sS.cgrCfg.SessionSCfg().CDRsConns, nil, utils.CDRsV1StoreSessionCost, + argSmCost, &reply); err != nil && err == utils.ErrExists { + utils.Logger.Warning( + fmt.Sprintf("<%s> refunding session: <%s> error: <%s>", + utils.SessionS, s.CGRID, err.Error())) + if err = sS.refundSession(s, sRunIdx, sr.CD.GetDuration()); err != nil { // refund entire duration utils.Logger.Warning( - fmt.Sprintf("<%s> refunding session: <%s> error: <%s>", - utils.SessionS, s.CGRID, err.Error())) - if err = sS.refundSession(s, sRunIdx, sr.CD.GetDuration()); err != nil { // refund entire duration - utils.Logger.Warning( - fmt.Sprintf( - "<%s> failed refunding session: <%s>, srIdx: <%d>, error: <%s>", - utils.SessionS, s.CGRID, sRunIdx, err.Error())) - } - } else { - return err + fmt.Sprintf( + "<%s> failed refunding session: <%s>, srIdx: <%d>, error: <%s>", + utils.SessionS, s.CGRID, sRunIdx, err.Error())) + } + err = nil + } + return err +} + +// roundCost will round the EventCost and will refund the extra debited increments +// should be called only at the endSession +// not thread safe, need to be handled in a layer above +func (sS *SessionS) roundCost(s *Session, sRunIdx int) (err error) { + sr := s.SRuns[sRunIdx] + runID := sr.Event.GetStringIgnoreErrors(utils.RunID) + cc := sr.EventCost.AsCallCost(utils.EmptyString) + cc.Round() + if roundIncrements := cc.GetRoundIncrements(); len(roundIncrements) != 0 { + cd := cc.CreateCallDescriptor() + cd.CgrID = s.CGRID + cd.RunID = runID + cd.Increments = roundIncrements + var response float64 + if err = sS.connMgr.Call(sS.cgrCfg.SessionSCfg().ResSConns, nil, + utils.ResponderRefundRounding, + &engine.CallDescriptorWithArgDispatcher{CallDescriptor: cd}, + &response); err != nil { + return } } - return nil + sr.EventCost = engine.NewEventCostFromCallCost(cc, s.CGRID, runID) + return } // disconnectSession will send disconnect from SessionS to clients @@ -1478,6 +1495,20 @@ func (sS *SessionS) endSession(s *Session, tUsage, lastUsage *time.Duration, utils.SessionS, s.CGRID, sRunIdx, err.Error())) } } + if err := sS.roundCost(s, sRunIdx); err != nil { // will round the cost and refund the extra increment + utils.Logger.Warning( + fmt.Sprintf("<%s> failed rounding session cost for <%s>, srIdx: <%d>, error: <%s>", + utils.SessionS, s.CGRID, sRunIdx, err.Error())) + } + + if sS.cgrCfg.SessionSCfg().StoreSCosts { + if err := sS.storeSCost(s, sRunIdx); err != nil { + utils.Logger.Warning( + fmt.Sprintf("<%s> failed storing session cost for <%s>, srIdx: <%d>, error: <%s>", + utils.SessionS, s.CGRID, sRunIdx, err.Error())) + } + } + // set cost fields sr.Event[utils.Cost] = sr.EventCost.GetCost() sr.Event[utils.CostDetails] = utils.ToJSON(sr.EventCost) // avoid map[string]interface{} when decoding @@ -1491,14 +1522,6 @@ func (sS *SessionS) endSession(s *Session, tUsage, lastUsage *time.Duration, if aTime != nil { sr.Event[utils.AnswerTime] = *aTime } - if sS.cgrCfg.SessionSCfg().StoreSCosts { - if err := sS.storeSCost(s, sRunIdx); err != nil { - utils.Logger.Warning( - fmt.Sprintf("<%s> failed storing session cost for <%s>, srIdx: <%d>, error: <%s>", - utils.SessionS, s.CGRID, sRunIdx, err.Error())) - } - } - } engine.Cache.Set(utils.CacheClosedSessions, s.CGRID, s, nil, true, utils.NonTransactional) diff --git a/utils/consts.go b/utils/consts.go index 587581d45..83dca8dd6 100755 --- a/utils/consts.go +++ b/utils/consts.go @@ -662,6 +662,7 @@ const ( FileName = "FileName" MetaBusy = "*busy" MetaQueue = "*queue" + MetaRounding = "*rounding" ) // Migrator Action