From 1471c47bda67962e1c7cdc2adb2cf83cc7b8264e Mon Sep 17 00:00:00 2001 From: Radu Ioan Fericean Date: Thu, 9 Jan 2014 20:06:32 +0200 Subject: [PATCH] started adding test for shared group and created utility methods for getting shared balances --- engine/accountlock.go | 18 +++++++++ engine/sharedgroup.go | 55 ++++++++++++++++++++++++++++ engine/sharedgroup_test.go | 75 ++++++++++++++++++++++++++++++++++++++ engine/userbalance.go | 55 +++++++++++++++------------- 4 files changed, 178 insertions(+), 25 deletions(-) create mode 100644 engine/sharedgroup_test.go diff --git a/engine/accountlock.go b/engine/accountlock.go index 27349135b..6ea5c104d 100644 --- a/engine/accountlock.go +++ b/engine/accountlock.go @@ -68,3 +68,21 @@ func (cm *AccountLock) Guard(name string, handler func() (float64, error)) (repl <-lock return } + +func (cm *AccountLock) GuardMany(names []string, handler func() (float64, error)) (reply float64, err error) { + for _, name := range names { + cm.RLock() + lock, exists := AccLock.queue[name] + cm.RUnlock() + if !exists { + cm.Lock() + lock = make(chan bool, 1) + AccLock.queue[name] = lock + cm.Unlock() + } + lock <- true + reply, err = handler() + <-lock + } + return +} diff --git a/engine/sharedgroup.go b/engine/sharedgroup.go index 632544e1c..7980fea96 100644 --- a/engine/sharedgroup.go +++ b/engine/sharedgroup.go @@ -18,6 +18,18 @@ along with this program. If not, see package engine +import ( + "math" + "math/rand" + "time" +) + +const ( + STRATEGY_LOWEST_FIRST = "*lowest_first" + STRATEGY_HIGHEST_FIRST = "*highest_first" + STRATEGY_RANDOM = "*random" +) + type SharedGroup struct { Id string Strategy string @@ -26,3 +38,46 @@ type SharedGroup struct { Weight float64 Members []string } + +func (sg *SharedGroup) GetMembersExceptUser(ubId string) []string { + for i, m := range sg.Members { + if m == ubId { + a := make([]string, len(sg.Members)) + copy(a, sg.Members) + a[i], a = a[len(a)-1], a[:len(a)-1] + return a + } + } + return sg.Members +} + +func (sg *SharedGroup) PopBalanceByStrategy(balanceChain *BalanceChain) (bal *Balance) { + bc := *balanceChain + if len(bc) == 0 { + return + } + index := 0 + switch sg.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 { + if b.Value < minVal { + minVal = b.Value + index = i + } + } + case STRATEGY_HIGHEST_FIRST: + maxVal := math.SmallestNonzeroFloat64 + for i, b := range bc { + if b.Value > maxVal { + maxVal = b.Value + index = i + } + } + } + bal, bc[index], *balanceChain = bc[index], bc[len(bc)-1], bc[:len(bc)-1] + return +} diff --git a/engine/sharedgroup_test.go b/engine/sharedgroup_test.go new file mode 100644 index 000000000..384570dce --- /dev/null +++ b/engine/sharedgroup_test.go @@ -0,0 +1,75 @@ +/* +Rating system designed to be used in VoIP Carriers World +Copyright (C) 2013 ITsysCOM + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see +*/ + +package engine + +import ( + "reflect" + "testing" +) + +func TestSharedGroupGetMembersExcept(t *testing.T) { + sg := &SharedGroup{ + Members: []string{"1", "2", "3"}, + } + a1 := sg.GetMembersExceptUser("1") + a2 := sg.GetMembersExceptUser("2") + a3 := sg.GetMembersExceptUser("3") + if !reflect.DeepEqual(a1, []string{"3", "2"}) || + !reflect.DeepEqual(a2, []string{"1", "3"}) || + !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{Value: 3.0}, + } + sg := &SharedGroup{Strategy: STRATEGY_LOWEST_FIRST} + b := sg.PopBalanceByStrategy(&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) + } +} + +func TestSharedPopBalanceByStrategyHigh(t *testing.T) { + bc := BalanceChain{ + &Balance{Value: 2.0}, + &Balance{Value: 1.0}, + &Balance{Value: 3.0}, + } + sg := &SharedGroup{Strategy: STRATEGY_HIGHEST_FIRST} + b := sg.PopBalanceByStrategy(&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) + } +} diff --git a/engine/userbalance.go b/engine/userbalance.go index 824a5cff1..e82150589 100644 --- a/engine/userbalance.go +++ b/engine/userbalance.go @@ -189,31 +189,7 @@ func (ub *UserBalance) debitCreditBalance(cc *CallCost, count bool) error { } else { // chack if it's shared if balance.SharedGroup != "" { - sharedGroup, err := accountingStorage.GetSharedGroup(balance.SharedGroup, false) - if err != nil { - Logger.Warning(fmt.Sprintf("Could not get shared group: %s", balance.SharedGroup)) - continue - } - for _, ubId := range sharedGroup.Members { - if ubId == ub.Id { // skip the initiating user - continue - } - AccLock.Guard(ubId, func() (float64, error) { - nUb, err := accountingStorage.GetUserBalance(ubId) - if err != nil { - Logger.Warning(fmt.Sprintf("Could not get user balance: %s", ubId)) - } - sharedMinuteBalances := nUb.getBalancesForPrefix(cc.Destination, nUb.BalanceMap[MINUTES+cc.Direction], balance.SharedGroup) - sharedMoneyBalances := nUb.getBalancesForPrefix(cc.Destination, nUb.BalanceMap[CREDIT+cc.Direction], balance.SharedGroup) - for _, sharedBalance := range sharedMinuteBalances { - // FIXME: insert money balances after users balances - allMoneyBalances := append(usefulMoneyBalances, sharedMoneyBalances) - sharedBalance.DebitMinutes(cc, count, nUb, allMoneyBalances) - // FIXME: save nUb - } - return 0, nil - }) - } + ub.debitFromSharedBalances(balance.SharedGroup, cc, usefulMoneyBalances, count) } } } @@ -265,6 +241,35 @@ func (ub *UserBalance) debitCreditBalance(cc *CallCost, count bool) error { return returnError } +func (ub *UserBalance) debitFromSharedBalances(sharedGroupName string, cc *CallCost, moneyBalances BalanceChain, count bool) { + sharedGroup, err := accountingStorage.GetSharedGroup(sharedGroupName, false) + if err != nil { + Logger.Warning(fmt.Sprintf("Could not get shared group: %s", sharedGroup)) + return + } + sharingMembers := sharedGroup.GetMembersExceptUser(ub.Id) + AccLock.GuardMany(sharingMembers, func() (float64, error) { + for _, ubId := range sharingMembers { + if ubId == ub.Id { // skip the initiating user + continue + } + + nUb, err := accountingStorage.GetUserBalance(ubId) + if err != nil { + Logger.Warning(fmt.Sprintf("Could not get user balance: %s", ubId)) + } + sharedMinuteBalances := nUb.getBalancesForPrefix(cc.Destination, nUb.BalanceMap[MINUTES+cc.Direction], sharedGroupName) + sharedMoneyBalances := nUb.getBalancesForPrefix(cc.Destination, nUb.BalanceMap[CREDIT+cc.Direction], sharedGroupName) + for _, sharedBalance := range sharedMinuteBalances { + allMoneyBalances := append(moneyBalances, sharedMoneyBalances...) + sharedBalance.DebitMinutes(cc, count, nUb, allMoneyBalances) + accountingStorage.SetUserBalance(nUb) + } + } + return 0, nil + }) +} + func (ub *UserBalance) GetDefaultMoneyBalance(direction string) *Balance { for _, balance := range ub.BalanceMap[CREDIT+direction] { if balance.IsDefault() {