mirror of
https://github.com/cgrates/cgrates.git
synced 2026-02-16 13:49:53 +05:00
Remove ctx field from scheduledActs struct and create a fresh context when actions execute via cron. This prevents "context canceled" errors that occurred when stored contexts from API calls were used for delayed execution. The context is now properly received from the caller in case of "*asap" actions.
284 lines
8.4 KiB
Go
284 lines
8.4 KiB
Go
/*
|
|
Real-time Online/Offline Charging System (OCS) for Telecom & ISP environments
|
|
Copyright (C) ITsysCOM GmbH
|
|
|
|
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 <http://www.gnu.org/licenses/>
|
|
*/
|
|
|
|
package actions
|
|
|
|
import (
|
|
"cmp"
|
|
"fmt"
|
|
"slices"
|
|
"sync"
|
|
|
|
"github.com/cgrates/birpc/context"
|
|
"github.com/cgrates/cgrates/config"
|
|
"github.com/cgrates/cgrates/engine"
|
|
"github.com/cgrates/cgrates/utils"
|
|
"github.com/cgrates/cron"
|
|
"github.com/cgrates/guardian"
|
|
)
|
|
|
|
// NewActionS instantiates the ActionS
|
|
func NewActionS(cfg *config.CGRConfig, fltrS *engine.FilterS, dm *engine.DataManager, connMgr *engine.ConnManager) (aS *ActionS) {
|
|
aS = &ActionS{
|
|
cfg: cfg,
|
|
connMgr: connMgr,
|
|
fltrS: fltrS,
|
|
dm: dm,
|
|
crnLk: new(sync.RWMutex),
|
|
}
|
|
aS.schedInit() // initialize cron and schedule actions
|
|
return
|
|
}
|
|
|
|
// ActionS manages exection of Actions
|
|
type ActionS struct {
|
|
cfg *config.CGRConfig
|
|
connMgr *engine.ConnManager
|
|
fltrS *engine.FilterS
|
|
dm *engine.DataManager
|
|
crn *cron.Cron
|
|
crnLk *sync.RWMutex
|
|
}
|
|
|
|
// ListenAndServe keeps the service alive
|
|
func (aS *ActionS) ListenAndServe(stopChan, cfgRld chan struct{}) {
|
|
for {
|
|
select {
|
|
case <-stopChan:
|
|
return
|
|
case rld := <-cfgRld: // configuration was reloaded
|
|
cfgRld <- rld
|
|
}
|
|
}
|
|
}
|
|
|
|
// Shutdown is called to shutdown the service
|
|
func (aS *ActionS) Shutdown() {
|
|
aS.crnLk.RLock()
|
|
aS.crn.Stop()
|
|
aS.crnLk.RUnlock()
|
|
}
|
|
|
|
// schedInit is called at service start
|
|
func (aS *ActionS) schedInit() {
|
|
utils.Logger.Info(fmt.Sprintf("<%s> initializing scheduler.", utils.ActionS))
|
|
tnts := []string{aS.cfg.GeneralCfg().DefaultTenant}
|
|
if aS.cfg.ActionSCfg().Tenants != nil {
|
|
tnts = *aS.cfg.ActionSCfg().Tenants
|
|
}
|
|
cgrEvs := make([]*utils.CGREvent, len(tnts))
|
|
for i, tnt := range tnts {
|
|
cgrEvs[i] = &utils.CGREvent{
|
|
Tenant: tnt,
|
|
APIOpts: map[string]any{
|
|
utils.EventType: utils.SchedulerInit,
|
|
utils.NodeID: aS.cfg.GeneralCfg().NodeID,
|
|
},
|
|
}
|
|
}
|
|
aS.scheduleActions(context.Background(), cgrEvs, nil, false, true)
|
|
}
|
|
|
|
// scheduleActions will set up cron and load the matching data
|
|
func (aS *ActionS) scheduleActions(ctx *context.Context, cgrEvs []*utils.CGREvent, aPrflIDs []string, ignFilters, crnReset bool) (err error) {
|
|
aS.crnLk.Lock() // make sure we don't have parallel processes running setu
|
|
defer aS.crnLk.Unlock()
|
|
crn := aS.crn
|
|
if crnReset {
|
|
crn = cron.New()
|
|
}
|
|
var partExec bool
|
|
for _, cgrEv := range cgrEvs {
|
|
var schedActSet []*scheduledActs
|
|
if schedActSet, err = aS.scheduledActions(ctx, cgrEv.Tenant, cgrEv, aPrflIDs, ignFilters, false); err != nil {
|
|
utils.Logger.Warning(
|
|
fmt.Sprintf(
|
|
"<%s> scheduler init, ignoring tenant: <%s>, error: <%s>",
|
|
utils.ActionS, cgrEv.Tenant, err))
|
|
partExec = true
|
|
continue
|
|
}
|
|
for _, sActs := range schedActSet {
|
|
if sActs.schedule == utils.MetaASAP {
|
|
go aS.asapExecuteActions(context.Background(), sActs)
|
|
continue
|
|
}
|
|
if _, err = crn.AddFunc(sActs.schedule, sActs.ScheduledExecute); err != nil {
|
|
utils.Logger.Warning(
|
|
fmt.Sprintf(
|
|
"<%s> scheduling ActionProfile with id: <%s:%s>, error: <%s>",
|
|
utils.ActionS, sActs.tenant, sActs.apID, err))
|
|
partExec = true
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
if partExec {
|
|
err = utils.ErrPartiallyExecuted
|
|
}
|
|
if crnReset {
|
|
if aS.crn != nil {
|
|
aS.crn.Stop()
|
|
}
|
|
aS.crn = crn
|
|
aS.crn.Start()
|
|
}
|
|
return
|
|
}
|
|
|
|
// matchingActionProfilesForEvent returns the matched ActionProfiles for the given event
|
|
func (aS *ActionS) matchingActionProfilesForEvent(ctx *context.Context, tnt string,
|
|
evNm utils.MapStorage, aPrflIDs []string, ignoreFilters bool) (aPfs []*utils.ActionProfile, err error) {
|
|
if len(aPrflIDs) == 0 {
|
|
ignoreFilters = false
|
|
var aPfIDMp utils.StringSet
|
|
if aPfIDMp, err = engine.MatchingItemIDsForEvent(
|
|
ctx,
|
|
evNm,
|
|
aS.cfg.ActionSCfg().StringIndexedFields,
|
|
aS.cfg.ActionSCfg().PrefixIndexedFields,
|
|
aS.cfg.ActionSCfg().SuffixIndexedFields,
|
|
aS.cfg.ActionSCfg().ExistsIndexedFields,
|
|
aS.cfg.ActionSCfg().NotExistsIndexedFields,
|
|
aS.dm,
|
|
utils.CacheActionProfilesFilterIndexes,
|
|
tnt,
|
|
aS.cfg.ActionSCfg().IndexedSelects,
|
|
aS.cfg.ActionSCfg().NestedFields,
|
|
); err != nil {
|
|
return
|
|
}
|
|
aPrflIDs = aPfIDMp.AsSlice()
|
|
}
|
|
weights := make(map[string]float64) // stores sorting weights by profile ID
|
|
for _, aPfID := range aPrflIDs {
|
|
var aPf *utils.ActionProfile
|
|
if aPf, err = aS.dm.GetActionProfile(ctx, tnt, aPfID,
|
|
true, true, utils.NonTransactional); err != nil {
|
|
if err == utils.ErrNotFound {
|
|
err = nil
|
|
continue
|
|
}
|
|
return
|
|
}
|
|
if !ignoreFilters {
|
|
var pass bool
|
|
if pass, err = aS.fltrS.Pass(ctx, tnt, aPf.FilterIDs, evNm); err != nil {
|
|
return
|
|
} else if !pass {
|
|
continue
|
|
}
|
|
}
|
|
weight, err := engine.WeightFromDynamics(ctx, aPf.Weights, aS.fltrS, tnt, evNm)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
weights[aPf.ID] = weight
|
|
aPfs = append(aPfs, aPf)
|
|
}
|
|
if len(aPfs) == 0 {
|
|
return nil, utils.ErrNotFound
|
|
}
|
|
|
|
// Sort by weight (higher values first).
|
|
slices.SortFunc(aPfs, func(a, b *utils.ActionProfile) int {
|
|
return cmp.Compare(weights[b.ID], weights[a.ID])
|
|
})
|
|
|
|
for i, aPf := range aPfs {
|
|
var blocker bool
|
|
if blocker, err = engine.BlockerFromDynamics(ctx, aPf.Blockers, aS.fltrS, tnt, evNm); err != nil {
|
|
return
|
|
}
|
|
if blocker {
|
|
aPfs = aPfs[0 : i+1]
|
|
break
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
// scheduledActions is responsible for scheduling the action profiles matching cgrEv
|
|
func (aS *ActionS) scheduledActions(ctx *context.Context, tnt string, cgrEv *utils.CGREvent, aPrflIDs []string,
|
|
ignoreFilters, forceASAP bool) (schedActs []*scheduledActs, err error) {
|
|
evNm := cgrEv.AsDataProvider()
|
|
aPfs, err := aS.matchingActionProfilesForEvent(ctx, tnt, evNm, aPrflIDs, ignoreFilters)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
for _, aPf := range aPfs {
|
|
trgActs := map[string][]actioner{} // build here the list of actioners based on the trgKey
|
|
var partExec bool
|
|
for _, aCfg := range aPf.Actions { // create actioners and attach them to the right target
|
|
if act, errAct := newActioner(ctx, cgrEv, aS.cfg, aS.fltrS, aS.dm, aS.connMgr, aCfg, tnt); errAct != nil {
|
|
utils.Logger.Warning(
|
|
fmt.Sprintf(
|
|
"<%s> ignoring ActionProfile with id: <%s:%s> creating action: <%s>, error: <%s>",
|
|
utils.ActionS, aPf.Tenant, aPf.ID, aCfg.ID, errAct))
|
|
partExec = true
|
|
break
|
|
} else {
|
|
trgTyp := actionTarget(aCfg.Type)
|
|
trgActs[trgTyp] = append(trgActs[trgTyp], act)
|
|
}
|
|
}
|
|
if partExec {
|
|
continue // skip this profile from processing further
|
|
}
|
|
for trg, acts := range trgActs {
|
|
if trg == utils.MetaNone { // only one scheduledActs set
|
|
schedActs = append(schedActs, newScheduledActs(ctx, aPf.Tenant, aPf.ID, trg, utils.EmptyString, aPf.Schedule,
|
|
evNm, acts))
|
|
continue
|
|
}
|
|
for trgID := range aPf.Targets[trg] {
|
|
schedActs = append(schedActs, newScheduledActs(ctx, aPf.Tenant, aPf.ID, trg, trgID, aPf.Schedule,
|
|
evNm, acts))
|
|
}
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
// asapExecuteActions executes the scheduledActs and removes the executed from database
|
|
// uses locks to avoid concurrent access
|
|
func (aS *ActionS) asapExecuteActions(ctx *context.Context, sActs *scheduledActs) error {
|
|
return guardian.Guardian.Guard(ctx, func(ctx *context.Context) (err error) {
|
|
var ap *utils.ActionProfile
|
|
if ap, err = aS.dm.GetActionProfile(ctx, sActs.tenant, sActs.apID, true, true, utils.NonTransactional); err != nil {
|
|
utils.Logger.Warning(
|
|
fmt.Sprintf(
|
|
"<%s> querying ActionProfile with id: <%s:%s>, error: <%s>",
|
|
utils.ActionS, sActs.tenant, sActs.apID, err))
|
|
return
|
|
}
|
|
if err = sActs.Execute(ctx); err != nil { // cannot remove due to errors on execution
|
|
return
|
|
}
|
|
delete(ap.Targets[sActs.trgTyp], sActs.trgID)
|
|
if err = aS.dm.SetActionProfile(ctx, ap, true); err != nil {
|
|
utils.Logger.Warning(
|
|
fmt.Sprintf(
|
|
"<%s> saving ActionProfile with id: <%s:%s>, error: <%s>",
|
|
utils.ActionS, sActs.tenant, sActs.apID, err))
|
|
}
|
|
return
|
|
}, aS.cfg.GeneralCfg().LockingTimeout, utils.ActionProfilePrefix+sActs.apID)
|
|
}
|