diff --git a/apier/v1/apier.go b/apier/v1/apier.go index fd910da87..04845a8d8 100644 --- a/apier/v1/apier.go +++ b/apier/v1/apier.go @@ -734,60 +734,7 @@ func (apierSv1 *APIerSv1) GetActions(ctx *context.Context, actsId *string, reply return nil } -type AttrSetActionPlan struct { - Id string // Profile id - ActionPlan []*AttrActionPlan // Set of actions this Actions profile will perform - Overwrite bool // If previously defined, will be overwritten - ReloadScheduler bool // Enables automatic reload of the scheduler (eg: useful when adding a single action timing) -} - -type AttrActionPlan struct { - ActionsId string // Actions id - TimingID string // timingID is used to specify the ID of the timing for a corner case ( e.g. *monthly_estimated ) - Years string // semicolon separated list of years this timing is valid on, *any or empty supported - Months string // semicolon separated list of months this timing is valid on, *any or empty supported - MonthDays string // semicolon separated list of month's days this timing is valid on, *any or empty supported - WeekDays string // semicolon separated list of week day names this timing is valid on *any or empty supported - Time string // String representing the time this timing starts on, *asap supported - Weight float64 // Binding's weight -} - -func (attr *AttrActionPlan) getRITiming(dm *engine.DataManager) (timing *engine.RITiming, err error) { - if dfltTiming, isDefault := checkDefaultTiming(attr.Time); isDefault { - return dfltTiming, nil - } - timing = new(engine.RITiming) - - if attr.TimingID != utils.EmptyString && - !strings.HasPrefix(attr.TimingID, utils.Meta) { // in case of dynamic timing - if dbTiming, err := dm.GetTiming(attr.TimingID, false, utils.NonTransactional); err != nil { - if err != utils.ErrNotFound { // if not found let the user to populate all the timings values - return nil, err - } - } else { - timing.ID = dbTiming.ID - timing.Years = dbTiming.Years - timing.Months = dbTiming.Months - timing.MonthDays = dbTiming.MonthDays - timing.WeekDays = dbTiming.WeekDays - timing.StartTime = dbTiming.StartTime - timing.EndTime = dbTiming.EndTime - } - } - timing.ID = attr.TimingID - timing.Years.Parse(attr.Years, ";") - timing.Months.Parse(attr.Months, ";") - timing.MonthDays.Parse(attr.MonthDays, ";") - timing.WeekDays.Parse(attr.WeekDays, ";") - if !verifyFormat(attr.Time) { - err = fmt.Errorf("%s:%s", utils.ErrUnsupportedFormat.Error(), attr.Time) - return - } - timing.StartTime = attr.Time - return -} - -func (apierSv1 *APIerSv1) SetActionPlan(ctx *context.Context, attrs *AttrSetActionPlan, reply *string) (err error) { +func (apierSv1 *APIerSv1) SetActionPlan(ctx *context.Context, attrs *engine.AttrSetActionPlan, reply *string) (err error) { if missing := utils.MissingStructFields(attrs, []string{"Id", "ActionPlan"}); len(missing) != 0 { return utils.NewErrMandatoryIeMissing(missing...) } @@ -815,7 +762,7 @@ func (apierSv1 *APIerSv1) SetActionPlan(ctx *context.Context, attrs *AttrSetActi } else if !exists { return fmt.Errorf("%s:%s", utils.ErrBrokenReference.Error(), apiAtm.ActionsId) } - timing, err := apiAtm.getRITiming(apierSv1.DataManager) + timing, err := apiAtm.GetRITiming(apierSv1.DataManager) if err != nil { return err } @@ -868,119 +815,6 @@ func (apierSv1 *APIerSv1) SetActionPlan(ctx *context.Context, attrs *AttrSetActi return nil } -func verifyFormat(tStr string) bool { - if tStr == utils.EmptyString || - tStr == utils.MetaASAP { - return true - } - - if len(tStr) > 8 { // hh:mm:ss - return false - } - if a := strings.Split(tStr, utils.InInFieldSep); len(a) != 3 { - return false - } else { - if _, err := strconv.Atoi(a[0]); err != nil { - return false - } else if _, err := strconv.Atoi(a[1]); err != nil { - return false - } else if _, err := strconv.Atoi(a[2]); err != nil { - return false - } - } - return true -} - -// checkDefaultTiming will check the tStr if it's of the the default timings ( the same as in TPReader ) -// and will compute it properly -func checkDefaultTiming(tStr string) (rTm *engine.RITiming, isDefault bool) { - currentTime := time.Now() - fmtTime := currentTime.Format("15:04:05") - switch tStr { - case utils.MetaEveryMinute: - return &engine.RITiming{ - ID: utils.MetaEveryMinute, - Years: utils.Years{}, - Months: utils.Months{}, - MonthDays: utils.MonthDays{}, - WeekDays: utils.WeekDays{}, - StartTime: utils.ConcatenatedKey(utils.Meta, utils.Meta, strconv.Itoa(currentTime.Second())), - EndTime: "", - }, true - case utils.MetaHourly: - return &engine.RITiming{ - ID: utils.MetaHourly, - Years: utils.Years{}, - Months: utils.Months{}, - MonthDays: utils.MonthDays{}, - WeekDays: utils.WeekDays{}, - StartTime: utils.ConcatenatedKey(utils.Meta, strconv.Itoa(currentTime.Minute()), strconv.Itoa(currentTime.Second())), - EndTime: "", - }, true - case utils.MetaDaily: - return &engine.RITiming{ - ID: utils.MetaDaily, - Years: utils.Years{}, - Months: utils.Months{}, - MonthDays: utils.MonthDays{}, - WeekDays: utils.WeekDays{}, - StartTime: fmtTime, - EndTime: ""}, true - case utils.MetaWeekly: - return &engine.RITiming{ - ID: utils.MetaWeekly, - Years: utils.Years{}, - Months: utils.Months{}, - MonthDays: utils.MonthDays{}, - WeekDays: utils.WeekDays{currentTime.Weekday()}, - StartTime: fmtTime, - EndTime: "", - }, true - case utils.MetaMonthly: - return &engine.RITiming{ - ID: utils.MetaMonthly, - Years: utils.Years{}, - Months: utils.Months{}, - MonthDays: utils.MonthDays{currentTime.Day()}, - WeekDays: utils.WeekDays{}, - StartTime: fmtTime, - EndTime: "", - }, true - case utils.MetaMonthlyEstimated: - return &engine.RITiming{ - ID: utils.MetaMonthlyEstimated, - Years: utils.Years{}, - Months: utils.Months{}, - MonthDays: utils.MonthDays{currentTime.Day()}, - WeekDays: utils.WeekDays{}, - StartTime: fmtTime, - EndTime: "", - }, true - case utils.MetaMonthEnd: - return &engine.RITiming{ - ID: utils.MetaMonthEnd, - Years: utils.Years{}, - Months: utils.Months{}, - MonthDays: utils.MonthDays{-1}, - WeekDays: utils.WeekDays{}, - StartTime: fmtTime, - EndTime: "", - }, true - case utils.MetaYearly: - return &engine.RITiming{ - ID: utils.MetaYearly, - Years: utils.Years{}, - Months: utils.Months{currentTime.Month()}, - MonthDays: utils.MonthDays{currentTime.Day()}, - WeekDays: utils.WeekDays{}, - StartTime: fmtTime, - EndTime: "", - }, true - default: - return nil, false - } -} - type AttrGetActionPlan struct { ID string } diff --git a/console/actionplan_set.go b/console/actionplan_set.go index e837ec2fd..941f108da 100644 --- a/console/actionplan_set.go +++ b/console/actionplan_set.go @@ -19,7 +19,7 @@ along with this program. If not, see package console import ( - "github.com/cgrates/cgrates/apier/v1" + "github.com/cgrates/cgrates/engine" "github.com/cgrates/cgrates/utils" ) @@ -27,7 +27,7 @@ func init() { c := &CmdSetActionPlan{ name: "actionplan_set", rpcMethod: utils.APIerSv1SetActionPlan, - rpcParams: &v1.AttrSetActionPlan{}, + rpcParams: &engine.AttrSetActionPlan{}, } commands[c.Name()] = c c.CommandExecuter = &CommandExecuter{c} @@ -37,7 +37,7 @@ func init() { type CmdSetActionPlan struct { name string rpcMethod string - rpcParams *v1.AttrSetActionPlan + rpcParams *engine.AttrSetActionPlan *CommandExecuter } @@ -51,7 +51,7 @@ func (self *CmdSetActionPlan) RpcMethod() string { func (self *CmdSetActionPlan) RpcParams(reset bool) any { if reset || self.rpcParams == nil { - self.rpcParams = &v1.AttrSetActionPlan{} + self.rpcParams = &engine.AttrSetActionPlan{} } return self.rpcParams } diff --git a/engine/action.go b/engine/action.go index 340903887..263e32066 100644 --- a/engine/action.go +++ b/engine/action.go @@ -125,14 +125,19 @@ func newActionConnCfg(source, action string, cfg *config.CGRConfig) ActionConnCf utils.MetaAlterSessions, utils.MetaForceDisconnectSessions, } + dynamicActions := []string{ + utils.MetaDynamicThreshold, utils.MetaDynamicStats, + utils.MetaDynamicAttribute, utils.MetaDynamicActionPlan, + utils.MetaDynamicAction, utils.MetaDynamicDestination, + utils.MetaDynamicAccountAction, + } act := ActionConnCfg{} switch source { case utils.ThresholdS: switch { case slices.Contains(sessionActions, action): act.ConnIDs = cfg.ThresholdSCfg().SessionSConns - case utils.MetaDynamicThreshold == action || utils.MetaDynamicStats == action || - utils.MetaDynamicAttribute == action: + case slices.Contains(dynamicActions, action): act.ConnIDs = cfg.ThresholdSCfg().ApierSConns } case utils.RALs: @@ -187,6 +192,7 @@ func init() { actionFuncMap[utils.MetaDynamicThreshold] = dynamicThreshold actionFuncMap[utils.MetaDynamicStats] = dynamicStats actionFuncMap[utils.MetaDynamicAttribute] = dynamicAttribute + actionFuncMap[utils.MetaDynamicActionPlan] = dynamicActionPlan } func getActionFunc(typ string) (f actionTypeFunc, exists bool) { @@ -990,7 +996,7 @@ func alterSessionsAction(_ *Account, act *Action, _ Actions, _ *FilterS, _ any, // Parse action parameters based on the predefined format. params := strings.Split(act.ExtraParameters, ";") if len(params) != 5 { - return errors.New("invalid number of parameters; expected 5") + return errors.New(fmt.Sprintf("invalid number of parameters <%d> expected 5", len(params))) } // If conversion fails, limit will default to 0. @@ -1035,7 +1041,7 @@ func forceDisconnectSessionsAction(_ *Account, act *Action, _ Actions, _ *Filter // Parse action parameters based on the predefined format. params := strings.Split(act.ExtraParameters, ";") if len(params) != 5 { - return errors.New("invalid number of parameters; expected 5") + return errors.New(fmt.Sprintf("invalid number of parameters <%d> expected 5", len(params))) } // If conversion fails, limit will default to 0. @@ -1468,7 +1474,7 @@ func dynamicThreshold(_ *Account, act *Action, _ Actions, _ *FilterS, ev any, // Parse action parameters based on the predefined format. params := strings.Split(act.ExtraParameters, utils.InfieldSep) if len(params) != 12 { - return errors.New("invalid number of parameters; expected 12") + return errors.New(fmt.Sprintf("invalid number of parameters <%d> expected 12", len(params))) } // parse dynamic parameters for i := range params { @@ -1597,7 +1603,7 @@ func dynamicStats(_ *Account, act *Action, _ Actions, _ *FilterS, ev any, // Parse action parameters based on the predefined format. params := strings.Split(act.ExtraParameters, utils.InfieldSep) if len(params) != 14 { - return errors.New("invalid number of parameters; expected 14") + return errors.New(fmt.Sprintf("invalid number of parameters <%d> expected 14", len(params))) } // parse dynamic parameters for i := range params { @@ -1817,3 +1823,90 @@ func dynamicAttribute(_ *Account, act *Action, _ Actions, _ *FilterS, ev any, var reply string return connMgr.Call(context.Background(), connCfg.ConnIDs, utils.APIerSv1SetAttributeProfile, attrP, &reply) } + +// dynamicActionPlan processes the `ExtraParameters` field from the action to construct an ActionPlan +// +// The ExtraParameters field format is expected as follows: +// +// 0 Id: string +// 1 ActionsId: string +// 2 TimingId: string +// 3 Weight: float +// 4 Overwrite: bool +// +// Parameters are separated by ";" and must be provided in the specified order. +func dynamicActionPlan(_ *Account, act *Action, _ Actions, _ *FilterS, ev any, + _ SharedActionsData, connCfg ActionConnCfg) (err error) { + cgrEv, canCast := ev.(*utils.CGREvent) + if !canCast { + return errors.New("Couldn't cast event to CGREvent") + } + dP := utils.MapStorage{ // create DataProvider from event + utils.MetaReq: cgrEv.Event, + utils.MetaTenant: cgrEv.Tenant, + utils.MetaNow: time.Now(), + utils.MetaOpts: cgrEv.APIOpts, + } + // Parse action parameters based on the predefined format. + params := strings.Split(act.ExtraParameters, utils.InfieldSep) + if len(params) != 5 { + return errors.New(fmt.Sprintf("invalid number of parameters <%d> expected 5", len(params))) + } + // parse dynamic parameters + for i := range params { + if params[i], err = utils.ParseParamForDataProvider(params[i], dP); err != nil { + return err + } + } + // Prepare request arguments based on provided parameters. + ap := &AttrSetActionPlan{ + Id: params[0], + ReloadScheduler: true, + } + // populate ActionPlan's ActionsId + if params[1] == utils.EmptyString { + return fmt.Errorf("empty ActionsId for <%s> dynamic_action_plan", params[0]) + } + // Make sure ActionsId exists in DataDB + var actsRply []*utils.TPAction + if err := connMgr.Call(context.Background(), connCfg.ConnIDs, utils.APIerSv1GetActions, params[1], &actsRply); err != nil { + return err + } + ap.ActionPlan = append(ap.ActionPlan, &AttrActionPlan{}) + ap.ActionPlan[0].ActionsId = params[1] + if params[2] != utils.EmptyString { + // Make sure TimingID exists in DataDB and use it for the action plan + var tpTiming utils.TPTiming + if err := connMgr.Call(context.Background(), connCfg.ConnIDs, utils.APIerSv1GetTiming, &utils.ArgsGetTimingID{ID: params[2]}, &tpTiming); err != nil { + return err + } + ap.ActionPlan[0].TimingID = tpTiming.ID + ap.ActionPlan[0].Years = tpTiming.Years.Serialize(";") + ap.ActionPlan[0].Months = tpTiming.Months.Serialize(";") + ap.ActionPlan[0].MonthDays = tpTiming.MonthDays.Serialize(";") + ap.ActionPlan[0].WeekDays = tpTiming.WeekDays.Serialize(";") + if tpTiming.EndTime != utils.EmptyString { + ap.ActionPlan[0].Time = utils.InfieldJoin(tpTiming.StartTime, tpTiming.EndTime) + } else { + ap.ActionPlan[0].Time = tpTiming.StartTime + } + } + // populate ActionPlan's Weight + if params[3] != utils.EmptyString { + ap.ActionPlan[0].Weight, err = strconv.ParseFloat(params[3], 64) + if err != nil { + return err + } + } + // populate ActionPlan's Overwrite + if params[4] != utils.EmptyString { + ap.Overwrite, err = strconv.ParseBool(params[4]) + if err != nil { + return err + } + } + + // create the ActionPlan based on the populated parameters + var reply string + return connMgr.Call(context.Background(), connCfg.ConnIDs, utils.APIerSv1SetActionPlan, ap, &reply) +} diff --git a/engine/action_plan.go b/engine/action_plan.go index 5165b5c58..af03e22dc 100644 --- a/engine/action_plan.go +++ b/engine/action_plan.go @@ -21,6 +21,8 @@ package engine import ( "fmt" "sort" + "strconv" + "strings" "time" "github.com/cgrates/cgrates/config" @@ -129,6 +131,14 @@ func (at *ActionTiming) GetNextStartTime(refTime time.Time) time.Time { if !at.stCache.IsZero() { return at.stCache } + // Put action schedule time in stCache for 1 time actions + if at.Timing != nil && at.Timing.Timing != nil && + strings.HasPrefix(at.Timing.Timing.StartTime, utils.PlusChar) { + tmStrTmp, _ := time.ParseDuration(strings.TrimPrefix( + at.Timing.Timing.StartTime, utils.PlusChar)) + at.stCache = time.Now().Add(tmStrTmp) + return at.stCache + } rateIvl := at.Timing if rateIvl == nil || rateIvl.Timing == nil { return time.Time{} @@ -174,6 +184,10 @@ func (at *ActionTiming) GetNextStartTime(refTime time.Time) time.Time { } func (at *ActionTiming) ResetStartTimeCache() { + if at.Timing != nil && at.Timing.Timing != nil && strings.HasPrefix(at.Timing.Timing.StartTime, + utils.PlusChar) { + return // dont reset time for 1 time action plans starting with "+" + } at.stCache = time.Date(1, 1, 1, 0, 0, 0, 0, time.UTC) } @@ -375,3 +389,169 @@ func (atpl ActionTimingWeightOnlyPriorityList) Less(i, j int) bool { func (atpl ActionTimingWeightOnlyPriorityList) Sort() { sort.Sort(atpl) } + +type AttrSetActionPlan struct { + Id string // Profile id + ActionPlan []*AttrActionPlan // Set of actions this Actions profile will perform + Overwrite bool // If previously defined, will be overwritten + ReloadScheduler bool // Enables automatic reload of the scheduler (eg: useful when adding a single action timing) +} + +type AttrActionPlan struct { + ActionsId string // Actions id + TimingID string // timingID is used to specify the ID of the timing for a corner case ( e.g. *monthly_estimated ) + Years string // semicolon separated list of years this timing is valid on, *any or empty supported + Months string // semicolon separated list of months this timing is valid on, *any or empty supported + MonthDays string // semicolon separated list of month's days this timing is valid on, *any or empty supported + WeekDays string // semicolon separated list of week day names this timing is valid on *any or empty supported + Time string // String representing the time this timing starts on, *asap supported + Weight float64 // Binding's weight +} + +func (attr *AttrActionPlan) GetRITiming(dm *DataManager) (timing *RITiming, err error) { + if dfltTiming, isDefault := checkDefaultTiming(attr.Time); isDefault { + return dfltTiming, nil + } + timing = new(RITiming) + + if attr.TimingID != utils.EmptyString && + !strings.HasPrefix(attr.TimingID, utils.Meta) { // in case of dynamic timing + if dbTiming, err := dm.GetTiming(attr.TimingID, false, utils.NonTransactional); err != nil { + if err != utils.ErrNotFound { // if not found let the user to populate all the timings values + return nil, err + } + } else { + timing.ID = dbTiming.ID + timing.Years = dbTiming.Years + timing.Months = dbTiming.Months + timing.MonthDays = dbTiming.MonthDays + timing.WeekDays = dbTiming.WeekDays + timing.StartTime = dbTiming.StartTime + timing.EndTime = dbTiming.EndTime + } + } + timing.ID = attr.TimingID + timing.Years.Parse(attr.Years, ";") + timing.Months.Parse(attr.Months, ";") + timing.MonthDays.Parse(attr.MonthDays, ";") + timing.WeekDays.Parse(attr.WeekDays, ";") + if !verifyFormat(attr.Time) { + err = fmt.Errorf("%s:%s", utils.ErrUnsupportedFormat.Error(), attr.Time) + return + } + timing.StartTime = attr.Time + return +} + +// checkDefaultTiming will check the tStr if it's of the the default timings ( the same as in TPReader ) +// and will compute it properly +func checkDefaultTiming(tStr string) (rTm *RITiming, isDefault bool) { + currentTime := time.Now() + fmtTime := currentTime.Format("15:04:05") + switch tStr { + case utils.MetaEveryMinute: + return &RITiming{ + ID: utils.MetaEveryMinute, + Years: utils.Years{}, + Months: utils.Months{}, + MonthDays: utils.MonthDays{}, + WeekDays: utils.WeekDays{}, + StartTime: utils.ConcatenatedKey(utils.Meta, utils.Meta, strconv.Itoa(currentTime.Second())), + EndTime: "", + }, true + case utils.MetaHourly: + return &RITiming{ + ID: utils.MetaHourly, + Years: utils.Years{}, + Months: utils.Months{}, + MonthDays: utils.MonthDays{}, + WeekDays: utils.WeekDays{}, + StartTime: utils.ConcatenatedKey(utils.Meta, strconv.Itoa(currentTime.Minute()), strconv.Itoa(currentTime.Second())), + EndTime: "", + }, true + case utils.MetaDaily: + return &RITiming{ + ID: utils.MetaDaily, + Years: utils.Years{}, + Months: utils.Months{}, + MonthDays: utils.MonthDays{}, + WeekDays: utils.WeekDays{}, + StartTime: fmtTime, + EndTime: ""}, true + case utils.MetaWeekly: + return &RITiming{ + ID: utils.MetaWeekly, + Years: utils.Years{}, + Months: utils.Months{}, + MonthDays: utils.MonthDays{}, + WeekDays: utils.WeekDays{currentTime.Weekday()}, + StartTime: fmtTime, + EndTime: "", + }, true + case utils.MetaMonthly: + return &RITiming{ + ID: utils.MetaMonthly, + Years: utils.Years{}, + Months: utils.Months{}, + MonthDays: utils.MonthDays{currentTime.Day()}, + WeekDays: utils.WeekDays{}, + StartTime: fmtTime, + EndTime: "", + }, true + case utils.MetaMonthlyEstimated: + return &RITiming{ + ID: utils.MetaMonthlyEstimated, + Years: utils.Years{}, + Months: utils.Months{}, + MonthDays: utils.MonthDays{currentTime.Day()}, + WeekDays: utils.WeekDays{}, + StartTime: fmtTime, + EndTime: "", + }, true + case utils.MetaMonthEnd: + return &RITiming{ + ID: utils.MetaMonthEnd, + Years: utils.Years{}, + Months: utils.Months{}, + MonthDays: utils.MonthDays{-1}, + WeekDays: utils.WeekDays{}, + StartTime: fmtTime, + EndTime: "", + }, true + case utils.MetaYearly: + return &RITiming{ + ID: utils.MetaYearly, + Years: utils.Years{}, + Months: utils.Months{currentTime.Month()}, + MonthDays: utils.MonthDays{currentTime.Day()}, + WeekDays: utils.WeekDays{}, + StartTime: fmtTime, + EndTime: "", + }, true + default: + return nil, false + } +} + +func verifyFormat(tStr string) bool { + if tStr == utils.EmptyString || tStr == utils.MetaASAP || + strings.HasPrefix(tStr, utils.PlusChar) { + return true + } + + if len(tStr) > 8 { // hh:mm:ss + return false + } + if a := strings.Split(tStr, utils.InInFieldSep); len(a) != 3 { + return false + } else { + if _, err := strconv.Atoi(a[0]); err != nil { + return false + } else if _, err := strconv.Atoi(a[1]); err != nil { + return false + } else if _, err := strconv.Atoi(a[2]); err != nil { + return false + } + } + return true +} diff --git a/engine/datamanager.go b/engine/datamanager.go index 3d3e8c152..3a42613e1 100644 --- a/engine/datamanager.go +++ b/engine/datamanager.go @@ -1807,7 +1807,8 @@ func (dm *DataManager) SetTiming(t *utils.TPTiming) (err error) { return utils.ErrNoDatabaseConn } // Check if time strings can be split in a time format before storing in db - if t.StartTime != utils.EmptyString && t.StartTime != utils.MetaASAP && !utils.IsTimeFormated(t.StartTime) { + if t.StartTime != utils.EmptyString && t.StartTime != utils.MetaASAP && + !strings.HasPrefix(t.StartTime, utils.PlusChar) && !utils.IsTimeFormated(t.StartTime) { return utils.ErrInvalidTime(t.StartTime) } if t.EndTime != utils.EmptyString && t.EndTime != utils.MetaASAP && !utils.IsTimeFormated(t.EndTime) { diff --git a/scheduler/scheduler.go b/scheduler/scheduler.go index 3c4c69459..db7636b08 100644 --- a/scheduler/scheduler.go +++ b/scheduler/scheduler.go @@ -106,15 +106,19 @@ func (s *Scheduler) Loop() { go a0.Execute(s.fltrS, utils.SchedulerS) // if after execute the next start time is in the past then // do not add it to the queue - a0.ResetStartTimeCache() - now = time.Now().Add(time.Second) - start = a0.GetNextStartTime(now) - if start.Before(now) { + if strings.HasPrefix(a0.Timing.Timing.StartTime, utils.PlusChar) { s.queue = s.queue[1:] } else { - s.queue = append(s.queue, a0) - s.queue = s.queue[1:] - sort.Sort(s.queue) + a0.ResetStartTimeCache() + now = time.Now().Add(time.Second) + start = a0.GetNextStartTime(now) + if start.Before(now) { + s.queue = s.queue[1:] + } else { + s.queue = append(s.queue, a0) + s.queue = s.queue[1:] + sort.Sort(s.queue) + } } s.Unlock() } else { diff --git a/utils/apitpdata.go b/utils/apitpdata.go index fdcae4068..abe3b4a5f 100644 --- a/utils/apitpdata.go +++ b/utils/apitpdata.go @@ -367,8 +367,14 @@ func (t *TPTiming) IsActiveAt(tm time.Time) bool { return true } -// Returns true if string can be split in 3 sperated by ":" signs. "00:00:00" +// Returns true if string can be split in 3 sperated by ":" signs. "00:00:00", or if +// its parsable to a duration func IsTimeFormated(t string) bool { + if strings.HasPrefix(t, PlusChar) { + if _, err := time.ParseDuration(strings.TrimPrefix(t, PlusChar)); err != nil { + return false + } + } return len(strings.Split(t, ":")) == 3 } diff --git a/utils/consts.go b/utils/consts.go index 7fcd8d9d9..7461dfe1a 100644 --- a/utils/consts.go +++ b/utils/consts.go @@ -199,6 +199,7 @@ const ( FilterValStart = "(" FilterValEnd = ")" PlusChar = "+" + MinusChar = "-" JSON = "json" JSONCaps = "JSON" GOBCaps = "GOB" @@ -1183,6 +1184,7 @@ const ( MetaDynamicThreshold = "*dynamic_threshold" MetaDynamicStats = "*dynamic_stats" MetaDynamicAttribute = "*dynamic_attribute" + MetaDynamicActionPlan = "*dynamic_action_plan" ActionID = "ActionID" ActionType = "ActionType" ActionValue = "ActionValue"