Files
cgrates/actions/actions.go
ionutboangiu c3bf93f1b6 Fix context lifecycle in scheduled actions
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.
2025-05-26 08:19:43 +02:00

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)
}