diff --git a/cmd/cgr-scheduler/cgr-scheduler.go b/cmd/cgr-scheduler/cgr-scheduler.go index c0c4d529d..a9dbbcdde 100644 --- a/cmd/cgr-scheduler/cgr-scheduler.go +++ b/cmd/cgr-scheduler/cgr-scheduler.go @@ -22,6 +22,8 @@ import ( "log" "github.com/cgrates/cgrates/timespans" "flag" + "time" + "sort" ) var ( @@ -31,6 +33,43 @@ var ( redispass = flag.String("pass", "", "redis database password") ) +/* +Structure to store action timings according to next activation time. +*/ +type actiontimingqueue []*timespans.ActionTiming + +func (atq actiontimingqueue) Len() int { + return len(atq) +} + +func (atq actiontimingqueue) Swap(i, j int) { + atq[i], atq[j] = atq[j], atq[i] +} + +func (atq actiontimingqueue) Less(j, i int) bool { + return atq[j].GetNextStartTime().Before(atq[i].GetNextStartTime()) +} + +type scheduler struct { + queue actiontimingqueue +} + +func (s scheduler) loop() { + for { + a0 := s.queue[0] + now := time.Now() + if a0.GetNextStartTime().Equal(now) || a0.GetNextStartTime().Before(now) { + a0.Execute() + s.queue = append(s.queue, a0) + s.queue = s.queue[1:] + sort.Sort(s.queue) + } else { + d := a0.GetNextStartTime().Sub(now) + time.Sleep(d) + } + } +} + func main() { flag.Parse() storage, err := timespans.NewRedisStorage(*redisserver, *redisdb) @@ -41,7 +80,7 @@ func main() { if err != nil { log.Fatalf("Cannot get action timings:", err) } - for _, at := range actionTimings { - log.Print(at) - } + s := scheduler{} + s.queue = append(s.queue, actionTimings...) + s.loop() } diff --git a/timespans/actions.go b/timespans/actions.go index da6de89f0..3eff6f3cb 100644 --- a/timespans/actions.go +++ b/timespans/actions.go @@ -18,7 +18,16 @@ along with this program. If not, see package timespans -import () +import ( + "time" + "log" + "fmt" + "sort" +) + +const ( + FORMAT = "2006-1-2 15:04:05 MST" +) // Amount of a trafic of a certain type (TOR) type UnitsCounter struct { @@ -95,3 +104,86 @@ type ActionTiming struct { ActionsId string actions []*Action } + +func (at *ActionTiming) getActions() (a []*Action, err error) { + if at.actions == nil { + a, err = storageGetter.GetActions(at.ActionsId) + at.actions = a + } + return +} + +func (at *ActionTiming) GetNextStartTime() (t time.Time, err error) { + i := at.Timing + if i == nil { + return + } + now := time.Now() + y, m, d := now.Date() + z, _ := now.Zone() + if i.StartTime != "" { + l := fmt.Sprintf("%d-%d-%d %s %s", y, m, d, i.StartTime, z) + t, err = time.Parse(FORMAT, l) + } + + if i.WeekDays != nil && len(i.WeekDays) > 0 { + sort.Sort(i.WeekDays) + } + + if i.MonthDays != nil && len(i.MonthDays) > 0 { + sort.Sort(i.MonthDays) + now := time.Now() + x := sort.SearchInts(i.MonthDays, now.Day()) + d = i.MonthDays[0] + if x < len(i.MonthDays) { + if i.MonthDays[x] == now.Day() { + if now.Before(t) { + h, m, s := t.Clock() + t = time.Date(now.Year(), now.Month(), now.Day(), h, m, s, 0, time.Local) + return + } + if x+1 < len(i.MonthDays) { // today was found in the list, jump to the next grater day + d = i.MonthDays[x+1] + } + } else { // today was not found in the list, x is the first greater day + d = i.MonthDays[x] + } + } + h, m, s := t.Clock() + t = time.Date(now.Year(), now.Month(), d, h, m, s, 0, time.Local) + } + + if i.Months != nil && len(i.Months) > 0 { + sort.Sort(i.Months) + now := time.Now() + x := sort.Search(len(i.Months), func(x int) bool { return i.Months[x] >= now.Month() }) + m = i.Months[0] + if x < len(i.Months) { + if i.Months[x] == now.Month() { + if now.Before(t) { + h, m, s := t.Clock() + t = time.Date(now.Year(), now.Month(), t.Day(), h, m, s, 0, time.Local) + return + } + if x+1 < len(i.Months) { // today was found in the list, jump to the next grater day + m = i.Months[x+1] + } + } else { // today was not found in the list, x is the first greater day + m = i.Months[x] + } + } + h, min, s := t.Clock() + t = time.Date(now.Year(), m, t.Day(), h, min, s, 0, time.Local) + } + return +} + +func (at *ActionTiming) Execute() { + aac, err := at.getActions() + if err != nil { + return + } + for _, a := range aac { + log.Print(a) + } +} diff --git a/timespans/actions_test.go b/timespans/actions_test.go new file mode 100644 index 000000000..e0cfc3c5e --- /dev/null +++ b/timespans/actions_test.go @@ -0,0 +1,177 @@ +/* +Rating system designed to be used in VoIP Carriers World +Copyright (C) 2012 Radu Ioan Fericean + +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 timespans + +import ( + "testing" + "time" +) + +func TestActionTimingNothing(t *testing.T) { + at := &ActionTiming{} + st, err := at.GetNextStartTime() + if err != nil { + t.Error(err) + } + expected := time.Date(1, 1, 1, 0, 0, 0, 0, time.UTC) + if !st.Equal(expected) { + t.Errorf("Expected %v was %v", expected, st) + } +} + +func TestActionTimingOnlyHour(t *testing.T) { + at := &ActionTiming{Timing: &Interval{StartTime: "10:01:00"}} + st, err := at.GetNextStartTime() + if err != nil { + t.Error(err) + } + now := time.Now() + y, m, d := now.Date() + expected := time.Date(y, m, d, 10, 1, 0, 0, time.Local) + if !st.Equal(expected) { + t.Errorf("Expected %v was %v", expected, st) + } +} + +/*func TestActionTimingOnlyWeekdays(t *testing.T) { + at := &ActionTiming{Timing: &Interval{WeekDays: []time.Weekday{time.Monday}}} + st, err := at.GetNextStartTime() + if err != nil { + t.Error(err) + } + now := time.Now() + y, m, d := now.Date() + expected := time.Date(y, m, d, 10, 1, 0, 0, time.Local) + if !st.Equal(expected) { + t.Errorf("Expected %v was %v", expected, st) + } +} + +func TestActionTimingWeekdaysHour(t *testing.T) { + at := &ActionTiming{Timing: &Interval{WeekDays: []time.Weekday{time.Monday}, StartTime: "10:01:00"}} + st, err := at.GetNextStartTime() + if err != nil { + t.Error(err) + } + now := time.Now() + y, m, d := now.Date() + expected := time.Date(y, m, d, 10, 1, 0, 0, time.Local) + if !st.Equal(expected) { + t.Errorf("Expected %v was %v", expected, st) + } +}*/ + +func TestActionTimingOnlyMonthdays(t *testing.T) { + now := time.Now() + y, m, d := now.Date() + tomorrow := time.Date(y, m, d+1, 0, 0, 0, 0, time.Local) + at := &ActionTiming{Timing: &Interval{MonthDays: MonthDays{1, 25, 2, tomorrow.Day()}}} + st, err := at.GetNextStartTime() + if err != nil { + t.Error(err) + } + expected := time.Date(y, m, tomorrow.Day(), 0, 0, 0, 0, time.Local) + if !st.Equal(expected) { + t.Errorf("Expected %v was %v", expected, st) + } +} + +func TestActionTimingHourMonthdays(t *testing.T) { + now := time.Now() + y, m, d := now.Date() + testTime := time.Date(y, m, d, 10, 1, 0, 0, time.Local) + tomorrow := time.Date(y, m, d+1, 0, 0, 0, 0, time.Local) + day := now.Day() + if now.After(testTime) { + day = tomorrow.Day() + } + at := &ActionTiming{Timing: &Interval{MonthDays: MonthDays{now.Day(), tomorrow.Day()}, StartTime: "10:01:00"}} + st, err := at.GetNextStartTime() + if err != nil { + t.Error(err) + } + expected := time.Date(y, m, day, 10, 1, 0, 0, time.Local) + if !st.Equal(expected) { + t.Errorf("Expected %v was %v", expected, st) + } +} + +func TestActionTimingOnlyMonths(t *testing.T) { + now := time.Now() + y, m, d := now.Date() + nextMonth := time.Date(y, m+1, d, 0, 0, 0, 0, time.Local) + at := &ActionTiming{Timing: &Interval{Months: Months{time.February, time.May, nextMonth.Month()}}} + st, err := at.GetNextStartTime() + if err != nil { + t.Error(err) + } + expected := time.Date(y, nextMonth.Month(), 1, 0, 0, 0, 0, time.Local) + if !st.Equal(expected) { + t.Errorf("Expected %v was %v", expected, st) + } +} + +func TestActionTimingHourMonths(t *testing.T) { + now := time.Now() + y, m, d := now.Date() + testTime := time.Date(y, m, d, 10, 1, 0, 0, time.Local) + nextMonth := time.Date(y, m+1, d, 0, 0, 0, 0, time.Local) + month := now.Month() + if now.After(testTime) { + month = nextMonth.Month() + } + at := &ActionTiming{Timing: &Interval{Months: Months{now.Month(), nextMonth.Month()}, StartTime: "10:01:00"}} + st, err := at.GetNextStartTime() + if err != nil { + t.Error(err) + } + expected := time.Date(y, month, d, 10, 1, 0, 0, time.Local) + if !st.Equal(expected) { + t.Errorf("Expected %v was %v", expected, st) + } +} + +func TestActionTimingHourMonthdaysMonths(t *testing.T) { + now := time.Now() + y, m, d := now.Date() + testTime := time.Date(y, m, d, 10, 1, 0, 0, time.Local) + nextMonth := time.Date(y, m+1, d, 0, 0, 0, 0, time.Local) + tomorrow := time.Date(y, m, d+1, 0, 0, 0, 0, time.Local) + day := now.Day() + if now.After(testTime) { + day = tomorrow.Day() + } + month := now.Month() + if now.After(testTime) { + month = nextMonth.Month() + } + at := &ActionTiming{Timing: &Interval{ + Months: Months{now.Month(), nextMonth.Month()}, + MonthDays: MonthDays{now.Day(), tomorrow.Day()}, + StartTime: "10:01:00", + }} + st, err := at.GetNextStartTime() + if err != nil { + t.Error(err) + } + expected := time.Date(y, month, day, 10, 1, 0, 0, time.Local) + if !st.Equal(expected) { + t.Errorf("Expected %v was %v", expected, st) + } +} diff --git a/timespans/dateseries.go b/timespans/dateseries.go index 1ebe0703d..7c0d2d850 100644 --- a/timespans/dateseries.go +++ b/timespans/dateseries.go @@ -28,9 +28,20 @@ import ( // Defines months series type Months []time.Month +func (m Months) Len() int { + return len(m) +} + +func (m Months) Swap(i, j int) { + m[i], m[j] = m[j], m[i] +} + +func (m Months) Less(j, i int) bool { + return m[j] < m[i] +} + // Return true if the specified date is inside the series func (m Months) Contains(month time.Month) (result bool) { - result = false for _, ms := range m { if ms == month { result = true @@ -61,6 +72,18 @@ func (m *Months) Parse(input, sep string) { // Defines month days series type MonthDays []int +func (md MonthDays) Len() int { + return len(md) +} + +func (md MonthDays) Swap(i, j int) { + md[i], md[j] = md[j], md[i] +} + +func (md MonthDays) Less(j, i int) bool { + return md[j] < md[i] +} + // Return true if the specified date is inside the series func (md MonthDays) Contains(monthDay int) (result bool) { result = false @@ -93,6 +116,18 @@ func (md *MonthDays) Parse(input, sep string) { // Defines week days series type WeekDays []time.Weekday +func (wd WeekDays) Len() int { + return len(wd) +} + +func (wd WeekDays) Swap(i, j int) { + wd[i], wd[j] = wd[j], wd[i] +} + +func (wd WeekDays) Less(j, i int) bool { + return wd[j] < wd[i] +} + // Return true if the specified date is inside the series func (wd WeekDays) Contains(weekDay time.Weekday) (result bool) { result = false diff --git a/timespans/storage_redis.go b/timespans/storage_redis.go index 9a1b42e62..4690adb0c 100644 --- a/timespans/storage_redis.go +++ b/timespans/storage_redis.go @@ -20,7 +20,7 @@ package timespans import ( "github.com/simonz05/godis" - "encoding/json" + "encoding/json" ) const ( @@ -134,7 +134,7 @@ func (rs *RedisStorage) GetAllActionTimings() (ats []*ActionTiming, err error) { return } for _, v := range values.BytesArray() { - var tempAts []*ActionTiming + var tempAts []*ActionTiming err = json.Unmarshal(v, &tempAts) ats = append(ats, tempAts...) } diff --git a/timespans/userbalance.go b/timespans/userbalance.go index 06f42649a..c8ea605c6 100644 --- a/timespans/userbalance.go +++ b/timespans/userbalance.go @@ -47,18 +47,19 @@ var ( Structure containing information about user's credit (minutes, cents, sms...).' */ type UserBalance struct { - Id string - Type string // prepaid-postpaid - BalanceMap map[string]float64 - MinuteBuckets []*MinuteBucket - UnitsCounters []*UnitsCounter - ActionTriggers []*ActionTrigger + Id string + Type string // prepaid-postpaid + BalanceMap map[string]float64 + MinuteBuckets []*MinuteBucket + UnitsCounters []*UnitsCounter + ActionTriggers []*ActionTrigger + usedActionTriggers []*ActionTrigger } /* Error type for overflowed debit methods. */ -type AmountTooBig byte +type AmountTooBig struct{} func (a AmountTooBig) Error() string { return "Amount excedes balance!" @@ -83,6 +84,11 @@ func (bs bucketsorter) Less(j, i int) bool { bs[i].Price > bs[j].Price } +func (ub *UserBalance) ResetActionTriggers() { + ub.ActionTriggers = append(ub.ActionTriggers, ub.usedActionTriggers...) + ub.usedActionTriggers = make([]*ActionTrigger, 0) +} + /* Returns user's available minutes for the specified destination */