diff --git a/engine/account.go b/engine/account.go index 53eac07fc..dfe1a6a8b 100644 --- a/engine/account.go +++ b/engine/account.go @@ -173,14 +173,13 @@ func (ub *Account) debitCreditBalance(cc *CallCost, count bool) (err error) { defaultMoneyBalance := ub.GetDefaultMoneyBalance(cc.Direction) // debit minutes for _, balance := range usefulMinuteBalances { - balance.DebitMinutes(cc, count, ub, usefulMoneyBalances) + if balance.SharedGroup != "" { + ub.debitMinutesFromSharedBalances(balance, cc, usefulMoneyBalances, count) + } else { + balance.DebitMinutes(cc, count, ub, usefulMoneyBalances) + } if cc.IsPaid() { goto CONNECT_FEE - } else { - // chack if it's shared - if balance.SharedGroup != "" { - ub.debitMinutesFromSharedBalances(balance.SharedGroup, cc, usefulMoneyBalances, count) - } } } for tsIndex := 0; tsIndex < len(cc.Timespans); tsIndex++ { @@ -198,14 +197,13 @@ func (ub *Account) debitCreditBalance(cc *CallCost, count bool) (err error) { // debit money for _, balance := range usefulMoneyBalances { - balance.DebitMoney(cc, count, ub) + if balance.SharedGroup != "" { + ub.debitMoneyFromSharedBalances(balance, cc, count) + } else { + balance.DebitMoney(cc, count, ub) + } if cc.IsPaid() { goto CONNECT_FEE - } else { - // chack if it's shared - if balance.SharedGroup != "" { - ub.debitMoneyFromSharedBalances(balance.SharedGroup, cc, count) - } } } // get the highest priority money balanance @@ -263,33 +261,34 @@ CONNECT_FEE: return } -func (ub *Account) debitMinutesFromSharedBalances(sharedGroupName string, cc *CallCost, moneyBalances BalanceChain, count bool) { +func (ub *Account) debitMinutesFromSharedBalances(myBalance *Balance, cc *CallCost, moneyBalances BalanceChain, count bool) { + sharedGroupName := myBalance.SharedGroup sharedGroup, err := accountingStorage.GetSharedGroup(sharedGroupName, false) if err != nil { Logger.Warning(fmt.Sprintf("Could not get shared group: %v", sharedGroupName)) return } - sharingMembers := sharedGroup.GetMembersExceptUser(ub.Id) - AccLock.GuardMany(sharingMembers, func() (float64, error) { + sharingMembers := sharedGroup.Members + AccLock.GuardMany(sharedGroup.GetMembersExceptUser(ub.Id), func() (float64, error) { var allMinuteSharedBalances BalanceChain for _, ubId := range sharingMembers { + var nUb *Account if ubId == ub.Id { // skip the initiating user - continue - } - - nUb, err := accountingStorage.GetAccount(ubId) - if err != nil { - Logger.Warning(fmt.Sprintf("Could not get user balance: %s", ubId)) - } - if nUb.Disabled { - Logger.Warning(fmt.Sprintf("Disabled user in shared group: %s (%s)", ubId, sharedGroupName)) - continue + nUb = ub + } else { + nUb, err = accountingStorage.GetAccount(ubId) + if err != nil { + Logger.Warning(fmt.Sprintf("Could not get user balance: %s", ubId)) + } + if nUb.Disabled { + Logger.Warning(fmt.Sprintf("Disabled user in shared group: %s (%s)", ubId, sharedGroupName)) + continue + } } sharedMinuteBalances := nUb.getBalancesForPrefix(cc.Destination, nUb.BalanceMap[MINUTES+cc.Direction], sharedGroupName) allMinuteSharedBalances = append(allMinuteSharedBalances, sharedMinuteBalances...) } - for sharedBalance := sharedGroup.PopBalanceByStrategy(ub.Id, &allMinuteSharedBalances); sharedBalance != nil; sharedBalance = sharedGroup.PopBalanceByStrategy(ub.Id, - &allMinuteSharedBalances) { + for _, sharedBalance := range sharedGroup.GetBalancesByStrategy(myBalance, allMinuteSharedBalances) { initialValue := sharedBalance.Value sharedBalance.DebitMinutes(cc, count, sharedBalance.account, moneyBalances) if sharedBalance.Value != initialValue { @@ -303,32 +302,34 @@ func (ub *Account) debitMinutesFromSharedBalances(sharedGroupName string, cc *Ca }) } -func (ub *Account) debitMoneyFromSharedBalances(sharedGroupName string, cc *CallCost, count bool) { +func (ub *Account) debitMoneyFromSharedBalances(myBalance *Balance, cc *CallCost, count bool) { + sharedGroupName := myBalance.SharedGroup sharedGroup, err := accountingStorage.GetSharedGroup(sharedGroupName, false) if err != nil { Logger.Warning(fmt.Sprintf("Could not get shared group: %v", sharedGroup)) return } - sharingMembers := sharedGroup.GetMembersExceptUser(ub.Id) - AccLock.GuardMany(sharingMembers, func() (float64, error) { + sharingMembers := sharedGroup.Members + AccLock.GuardMany(sharedGroup.GetMembersExceptUser(ub.Id), func() (float64, error) { var allMoneySharedBalances BalanceChain for _, ubId := range sharingMembers { + var nUb *Account if ubId == ub.Id { // skip the initiating user - continue - } - - nUb, err := accountingStorage.GetAccount(ubId) - if err != nil { - Logger.Warning(fmt.Sprintf("Could not get user balance: %s", ubId)) - } - if nUb.Disabled { - Logger.Warning(fmt.Sprintf("Disabled user in shared group: %s (%s)", ubId, sharedGroupName)) - continue + nUb = ub + } else { + nUb, err = accountingStorage.GetAccount(ubId) + if err != nil { + Logger.Warning(fmt.Sprintf("Could not get user balance: %s", ubId)) + } + if nUb.Disabled { + Logger.Warning(fmt.Sprintf("Disabled user in shared group: %s (%s)", ubId, sharedGroupName)) + continue + } } sharedMoneyBalances := nUb.getBalancesForPrefix(cc.Destination, nUb.BalanceMap[CREDIT+cc.Direction], sharedGroupName) allMoneySharedBalances = append(allMoneySharedBalances, sharedMoneyBalances...) } - for sharedBalance := sharedGroup.PopBalanceByStrategy(ub.Id, &allMoneySharedBalances); sharedBalance != nil; sharedBalance = sharedGroup.PopBalanceByStrategy(ub.Id, &allMoneySharedBalances) { + for _, sharedBalance := range sharedGroup.GetBalancesByStrategy(myBalance, allMoneySharedBalances) { initialValue := sharedBalance.Value sharedBalance.DebitMoney(cc, count, sharedBalance.account) if sharedBalance.Value != initialValue { diff --git a/engine/account_test.go b/engine/account_test.go index 3bce742b7..89a570ef3 100644 --- a/engine/account_test.go +++ b/engine/account_test.go @@ -1058,7 +1058,7 @@ func TestDebitShared(t *testing.T) { CREDIT + OUTBOUND: BalanceChain{&Balance{Uuid: "moneyc", Value: 70, SharedGroup: "SG_TEST"}}, }} - sg := &SharedGroup{Id: "SG_TEST", Members: []string{rif.Id, groupie.Id}, AccountParameters: map[string]*SharingParameters{"*any": &SharingParameters{Strategy: STRATEGY_RANDOM}}} + sg := &SharedGroup{Id: "SG_TEST", Members: []string{rif.Id, groupie.Id}, AccountParameters: map[string]*SharingParameters{"*any": &SharingParameters{Strategy: STRATEGY_MINE_RANDOM}}} accountingStorage.SetAccount(groupie) accountingStorage.SetSharedGroup("SG_TEST", sg) diff --git a/engine/sharedgroup.go b/engine/sharedgroup.go index 5643d2c8e..1a3964448 100644 --- a/engine/sharedgroup.go +++ b/engine/sharedgroup.go @@ -19,17 +19,22 @@ along with this program. If not, see package engine import ( - "math" "math/rand" + "sort" + "strings" "time" "github.com/cgrates/cgrates/utils" ) const ( - STRATEGY_LOWEST_FIRST = "*lowest_first" - STRATEGY_HIGHEST_FIRST = "*highest_first" - STRATEGY_RANDOM = "*random" + MINE_PREFIX = "*mine_" + STRATEGY_MINE_LOWEST = "*mine_lowest" + STRATEGY_MINE_HIGHEST = "*mine_highest" + STRATEGY_MINE_RANDOM = "*mine_random" + STRATEGY_LOWEST = "*lowest" + STRATEGY_HIGHEST = "*highest" + STRATEGY_RANDOM = "*random" ) type SharedGroup struct { @@ -55,43 +60,79 @@ func (sg *SharedGroup) GetMembersExceptUser(ubId string) []string { return sg.Members } -func (sg *SharedGroup) PopBalanceByStrategy(account string, balanceChain *BalanceChain) (bal *Balance) { - bc := *balanceChain - if len(bc) == 0 { - return - } - index := 0 +func (sg *SharedGroup) GetBalancesByStrategy(myBalance *Balance, bc BalanceChain) BalanceChain { sharingParameters := sg.AccountParameters[utils.ANY] - if sp, hasParamsForAccount := sg.AccountParameters[account]; hasParamsForAccount { + if sp, hasParamsForAccount := sg.AccountParameters[myBalance.account.Id]; hasParamsForAccount { sharingParameters = sp } - strategy := STRATEGY_RANDOM + + strategy := STRATEGY_MINE_RANDOM if sharingParameters != nil { strategy = sharingParameters.Strategy } switch strategy { - case STRATEGY_RANDOM: - rand.Seed(time.Now().Unix()) - index = rand.Intn(len(bc)) - case STRATEGY_LOWEST_FIRST: - minVal := math.MaxFloat64 - for i, b := range bc { - b.RateSubject = sharingParameters.RateSubject - if b.Value < minVal { - minVal = b.Value - index = i - } - } - case STRATEGY_HIGHEST_FIRST: - maxVal := math.SmallestNonzeroFloat64 - for i, b := range bc { - b.RateSubject = sharingParameters.RateSubject - if b.Value > maxVal { - maxVal = b.Value - index = i - } - } + case STRATEGY_LOWEST, STRATEGY_MINE_LOWEST: + sort.Sort(LowestBalanceChainSorter(bc)) + case STRATEGY_HIGHEST, STRATEGY_MINE_HIGHEST: + sort.Sort(HighestBalanceChainSorter(bc)) + case STRATEGY_RANDOM, STRATEGY_MINE_RANDOM: + rbc := RandomBalanceChainSorter(bc) + (&rbc).Sort() + bc = BalanceChain(rbc) } - bal, bc[index], *balanceChain = bc[index], bc[len(bc)-1], bc[:len(bc)-1] - return + if strings.HasPrefix(strategy, MINE_PREFIX) { + // find index of my balance + index := 0 + for i, b := range bc { + if b.Uuid == myBalance.Uuid { + index = i + break + } + } + // move my balance first + bc[0], bc[index] = bc[index], bc[0] + } + return bc +} + +type LowestBalanceChainSorter []*Balance + +func (lbcs LowestBalanceChainSorter) Len() int { + return len(lbcs) +} + +func (lbcs LowestBalanceChainSorter) Swap(i, j int) { + lbcs[i], lbcs[j] = lbcs[j], lbcs[i] +} + +func (lbcs LowestBalanceChainSorter) Less(i, j int) bool { + return lbcs[i].Value < lbcs[j].Value +} + +type HighestBalanceChainSorter []*Balance + +func (hbcs HighestBalanceChainSorter) Len() int { + return len(hbcs) +} + +func (hbcs HighestBalanceChainSorter) Swap(i, j int) { + hbcs[i], hbcs[j] = hbcs[j], hbcs[i] +} + +func (hbcs HighestBalanceChainSorter) Less(i, j int) bool { + return hbcs[i].Value > hbcs[j].Value +} + +type RandomBalanceChainSorter []*Balance + +func (rbcs *RandomBalanceChainSorter) Sort() { + src := *rbcs + // randomize balance chain + dest := make([]*Balance, len(src)) + rand.Seed(time.Now().Unix()) + perm := rand.Perm(len(src)) + for i, v := range perm { + dest[v] = src[i] + } + *rbcs = dest } diff --git a/engine/sharedgroup_test.go b/engine/sharedgroup_test.go index b893e903e..292d7aaaa 100644 --- a/engine/sharedgroup_test.go +++ b/engine/sharedgroup_test.go @@ -35,45 +35,74 @@ func TestSharedGroupGetMembersExcept(t *testing.T) { !reflect.DeepEqual(a3, []string{"1", "2"}) { t.Error("Error getting shared group members: ", a1, a2, a3) } - } func TestSharedPopBalanceByStrategyLow(t *testing.T) { bc := BalanceChain{ &Balance{Value: 2.0}, - &Balance{Value: 1.0}, + &Balance{Uuid: "uuuu", Value: 1.0, account: &Account{Id: "test"}}, &Balance{Value: 3.0}, } sg := &SharedGroup{AccountParameters: map[string]*SharingParameters{ - "test": &SharingParameters{Strategy: STRATEGY_LOWEST_FIRST}}, + "test": &SharingParameters{Strategy: STRATEGY_LOWEST}}, } - b := sg.PopBalanceByStrategy("test", &bc) - if b.Value != 1.0 { - t.Error("Error popping the right balance according to strategy: ", b, bc) - } - if len(bc) != 2 || - bc[0].Value != 2.0 || - bc[1].Value != 3.0 { - t.Error("Error removing balance from chain: ", bc) + sbc := sg.GetBalancesByStrategy(bc[1], bc) + if len(sbc) != 3 || + sbc[0].Value != 1.0 || + sbc[1].Value != 2.0 { + t.Error("Error sorting balance chain: ", sbc[0].Value) } } func TestSharedPopBalanceByStrategyHigh(t *testing.T) { bc := BalanceChain{ - &Balance{Value: 2.0}, + &Balance{Uuid: "uuuu", Value: 2.0, account: &Account{Id: "test"}}, &Balance{Value: 1.0}, &Balance{Value: 3.0}, } sg := &SharedGroup{AccountParameters: map[string]*SharingParameters{ - "test": &SharingParameters{Strategy: STRATEGY_HIGHEST_FIRST}}, + "test": &SharingParameters{Strategy: STRATEGY_HIGHEST}}, } - b := sg.PopBalanceByStrategy("test", &bc) - if b.Value != 3.0 { - t.Error("Error popping the right balance according to strategy: ", b, bc) - } - if len(bc) != 2 || - bc[0].Value != 2.0 || - bc[1].Value != 1.0 { - t.Error("Error removing balance from chain: ", bc) + sbc := sg.GetBalancesByStrategy(bc[0], bc) + if len(sbc) != 3 || + sbc[0].Value != 3.0 || + sbc[1].Value != 2.0 { + t.Error("Error sorting balance chain: ", sbc) + } +} + +func TestSharedPopBalanceByStrategyMineHigh(t *testing.T) { + bc := BalanceChain{ + &Balance{Uuid: "uuuu", Value: 2.0, account: &Account{Id: "test"}}, + &Balance{Value: 1.0}, + &Balance{Value: 3.0}, + } + sg := &SharedGroup{AccountParameters: map[string]*SharingParameters{ + "test": &SharingParameters{Strategy: STRATEGY_MINE_HIGHEST}}, + } + sbc := sg.GetBalancesByStrategy(bc[0], bc) + if len(sbc) != 3 || + sbc[0].Value != 2.0 || + sbc[1].Value != 3.0 { + t.Error("Error sorting balance chain: ", sbc) + } +} + +func TestSharedPopBalanceByStrategyRandomHigh(t *testing.T) { + bc := BalanceChain{ + &Balance{Uuid: "uuuu", Value: 2.0, account: &Account{Id: "test"}}, + &Balance{Value: 1.0}, + &Balance{Value: 3.0}, + } + sg := &SharedGroup{AccountParameters: map[string]*SharingParameters{ + "test": &SharingParameters{Strategy: STRATEGY_RANDOM}}, + } + x := bc[0] + sbc := sg.GetBalancesByStrategy(bc[0], bc) + firstTest := (sbc[0].Uuid == x.Uuid) + sbc = sg.GetBalancesByStrategy(bc[0], bc) + secondTest := (sbc[0].Uuid == x.Uuid) + if firstTest && secondTest { + t.Error("Something is wrong with balance randomizer") } }