From 4147dc6018a6236f1d59c3077258ae970c05bb82 Mon Sep 17 00:00:00 2001 From: DanB Date: Sat, 19 Jan 2019 21:13:58 +0100 Subject: [PATCH] Partial refactoring for SessionS component --- config/cfg_data.json | 4 + config/config.go | 7 +- config/config_defaults.go | 4 +- config/config_json_test.go | 22 +- config/config_test.go | 7 +- engine/mapevent.go | 19 + engine/safevent.go | 6 + sessions/session.go | 432 ++---- sessions/sessions.go | 2685 ++++++++++++++++++------------------ sessions/sessions_test.go | 2 + utils/consts.go | 28 +- utils/slice.go | 13 +- 12 files changed, 1557 insertions(+), 1672 deletions(-) diff --git a/config/cfg_data.json b/config/cfg_data.json index 9d2d9cf93..b7c2aa1cd 100644 --- a/config/cfg_data.json +++ b/config/cfg_data.json @@ -63,4 +63,8 @@ "enabled": true, }, +"chargers": { + "enabled": true, +}, + } diff --git a/config/config.go b/config/config.go index b3d75d8a1..4e8bacfd0 100755 --- a/config/config.go +++ b/config/config.go @@ -480,12 +480,15 @@ func (self *CGRConfig) checkConfigSanity() error { // SessionS checks if self.sessionSCfg.Enabled { if len(self.sessionSCfg.RALsConns) == 0 { - return errors.New(" RALs definition is mandatory!") + return errors.New(" RALs definition is mandatory") + } + if len(self.sessionSCfg.ChargerSConns) == 0 { + return fmt.Errorf("<%s> %s connection is mandatory", utils.SessionS, utils.ChargerS) } if !self.chargerSCfg.Enabled { for _, conn := range self.sessionSCfg.ChargerSConns { if conn.Address == utils.MetaInternal { - return errors.New(" ChargerS not enabled but requested") + return errors.New(" ChargerS not enabled") } } } diff --git a/config/config_defaults.go b/config/config_defaults.go index 7ab651924..92342365e 100755 --- a/config/config_defaults.go +++ b/config/config_defaults.go @@ -293,7 +293,9 @@ const CGRATES_CFG_JSON = ` "sessions": { "enabled": false, // starts session manager service: "listen_bijson": "127.0.0.1:2014", // address where to listen for bidirectional JSON-RPC requests - "chargers_conns": [], // address where to reach the charger service, empty to disable charger functionality: <""|*internal|x.y.z.y:1234> + "chargers_conns": [ + {"address": "*internal"} // address where to reach the charger service <*internal|x.y.z.y:1234> + ], "rals_conns": [ {"address": "*internal"} // address where to reach the RALs <""|*internal|127.0.0.1:2013> ], diff --git a/config/config_json_test.go b/config/config_json_test.go index 8ba4f71b8..b621e3712 100755 --- a/config/config_json_test.go +++ b/config/config_json_test.go @@ -469,17 +469,17 @@ func TestDfCdrcJsonCfg(t *testing.T) { func TestSmgJsonCfg(t *testing.T) { eCfg := &SessionSJsonCfg{ - Enabled: utils.BoolPointer(false), - Listen_bijson: utils.StringPointer("127.0.0.1:2014"), - Chargers_conns: &[]*HaPoolJsonCfg{}, - Rals_conns: &[]*HaPoolJsonCfg{ - { - Address: utils.StringPointer(utils.MetaInternal), - }}, - Cdrs_conns: &[]*HaPoolJsonCfg{ - { - Address: utils.StringPointer(utils.MetaInternal), - }}, + Enabled: utils.BoolPointer(false), + Listen_bijson: utils.StringPointer("127.0.0.1:2014"), + Chargers_conns: &[]*HaPoolJsonCfg{{ + Address: utils.StringPointer(utils.MetaInternal), + }}, + Rals_conns: &[]*HaPoolJsonCfg{{ + Address: utils.StringPointer(utils.MetaInternal), + }}, + Cdrs_conns: &[]*HaPoolJsonCfg{{ + Address: utils.StringPointer(utils.MetaInternal), + }}, Resources_conns: &[]*HaPoolJsonCfg{}, Thresholds_conns: &[]*HaPoolJsonCfg{}, Stats_conns: &[]*HaPoolJsonCfg{}, diff --git a/config/config_test.go b/config/config_test.go index 37049a869..23c23bd04 100755 --- a/config/config_test.go +++ b/config/config_test.go @@ -634,9 +634,10 @@ func TestCgrCfgJSONDefaultsCdreProfiles(t *testing.T) { func TestCgrCfgJSONDefaultsSMGenericCfg(t *testing.T) { eSessionSCfg := &SessionSCfg{ - Enabled: false, - ListenBijson: "127.0.0.1:2014", - ChargerSConns: []*HaPoolConfig{}, + Enabled: false, + ListenBijson: "127.0.0.1:2014", + ChargerSConns: []*HaPoolConfig{ + {Address: "*internal"}}, RALsConns: []*HaPoolConfig{ {Address: "*internal"}}, CDRsConns: []*HaPoolConfig{ diff --git a/engine/mapevent.go b/engine/mapevent.go index 684be871c..831a64885 100644 --- a/engine/mapevent.go +++ b/engine/mapevent.go @@ -101,6 +101,25 @@ func (me MapEvent) GetDurationIgnoreErrors(fldName string) (d time.Duration) { return } +// GetDurationPointer returns pointer towards duration, useful to detect presence of duration +func (me MapEvent) GetDurationPtr(fldName string) (d *time.Duration, err error) { + fldIface, has := me[fldName] + if !has { + return nil, utils.ErrNotFound + } + var dReal time.Duration + if dReal, err = utils.IfaceAsDuration(fldIface); err != nil { + return + } + return &dReal, nil +} + +// GetDurationPointer returns pointer towards duration, useful to detect presence of duration +func (me MapEvent) GetDurationPtrIgnoreErrors(fldName string) (d *time.Duration) { + d, _ = me.GetDurationPtr(fldName) + return +} + // GetTime returns a field as Time func (me MapEvent) GetTime(fldName string, timezone string) (t time.Time, err error) { fldIface, has := me[fldName] diff --git a/engine/safevent.go b/engine/safevent.go index dfddfa4a2..a253d60a0 100644 --- a/engine/safevent.go +++ b/engine/safevent.go @@ -167,6 +167,12 @@ func (se *SafEvent) GetDurationPtr(fldName string) (d *time.Duration, err error) return &dReal, nil } +// GetDurationPointer returns pointer towards duration, useful to detect presence of duration +func (se *SafEvent) GetDurationPtrIgnoreErrors(fldName string) (d *time.Duration) { + d, _ = se.GetDurationPtr(fldName) + return +} + // GetDurationPointer returns pointer towards duration, useful to detect presence of duration func (se *SafEvent) GetDurationPtrOrDefault(fldName string, dflt *time.Duration) (d *time.Duration, err error) { fldIface, has := se.Get(fldName) diff --git a/sessions/session.go b/sessions/session.go index 77bb1ceef..af56da1bf 100644 --- a/sessions/session.go +++ b/sessions/session.go @@ -19,54 +19,13 @@ along with this program. If not, see package sessions import ( - "errors" - "fmt" - "reflect" "sync" "time" "github.com/cgrates/cgrates/engine" "github.com/cgrates/cgrates/utils" - "github.com/cgrates/rpcclient" ) -// One session handled by SM -type SMGSession struct { - sync.RWMutex // protects the SMGSession in places where is concurrently accessed - stopDebit chan struct{} // Channel to communicate with debit loops when closing the session - clntConn rpcclient.RpcClientConnection // Reference towards client connection on SMG side so we can disconnect. - rals rpcclient.RpcClientConnection // Connector to rals service - cdrsrv rpcclient.RpcClientConnection // Connector to CDRS service - clientProto float64 - - Tenant string // store original Tenant so we can use it in API calls - CGRID string // Unique identifier for this session - RunID string // Keep a reference for the derived run - Timezone string - ResourceID string - - EventStart *engine.SafEvent // Event which started the session - CD *engine.CallDescriptor // initial CD used for debits, updated on each debit - EventCost *engine.EventCost - - ExtraDuration time.Duration // keeps the current duration debited on top of what has been asked - LastUsage time.Duration // last requested Duration - LastDebit time.Duration // last real debited duration - TotalUsage time.Duration // sum of lastUsage -} - -// Clone returns the cloned version of SMGSession -func (s *SMGSession) Clone() *SMGSession { - return &SMGSession{CGRID: s.CGRID, RunID: s.RunID, - Timezone: s.Timezone, ResourceID: s.ResourceID, - EventStart: s.EventStart.Clone(), - CD: s.CD.Clone(), - EventCost: s.EventCost.Clone(), - ExtraDuration: s.ExtraDuration, LastUsage: s.LastUsage, - LastDebit: s.LastDebit, TotalUsage: s.TotalUsage, - } -} - type SessionID struct { OriginHost string OriginID string @@ -76,268 +35,15 @@ func (s *SessionID) CGRID() string { return utils.Sha1(s.OriginID, s.OriginHost) } -// Called in case of automatic debits -func (self *SMGSession) debitLoop(debitInterval time.Duration) { - loopIndex := 0 - sleepDur := time.Duration(0) // start with empty duration for debit - for { - select { - case <-self.stopDebit: - return - case <-time.After(sleepDur): - if maxDebit, err := self.debit(debitInterval, nil); err != nil { - utils.Logger.Err(fmt.Sprintf("<%s> Could not complete debit operation on session: %s, error: %s", utils.SessionS, self.CGRID, err.Error())) - disconnectReason := utils.ErrServerError.Error() - if err.Error() == utils.ErrUnauthorizedDestination.Error() { - disconnectReason = err.Error() - } - if err := self.disconnectSession(disconnectReason); err != nil { - utils.Logger.Err(fmt.Sprintf("<%s> Could not disconnect session: %s, error: %s", utils.SessionS, self.CGRID, err.Error())) - } - return - } else if maxDebit < debitInterval { - time.Sleep(maxDebit) - if err := self.disconnectSession(utils.ErrInsufficientCredit.Error()); err != nil { - utils.Logger.Err(fmt.Sprintf("<%s> Could not disconnect session: %s, error: %s", utils.SessionS, self.CGRID, err.Error())) - } - return - } - sleepDur = debitInterval - loopIndex++ - } - } -} - -// Attempts to debit a duration, returns maximum duration which can be debitted or error -func (self *SMGSession) debit(dur time.Duration, lastUsed *time.Duration) (time.Duration, error) { - self.Lock() - defer self.Unlock() - requestedDuration := dur - if lastUsed != nil { - self.ExtraDuration = self.LastDebit - *lastUsed - if *lastUsed != self.LastUsage { - // total usage correction - self.TotalUsage -= self.LastUsage - self.TotalUsage += *lastUsed - } - } - // apply correction from previous run - if self.ExtraDuration < dur { - dur -= self.ExtraDuration - } else { - self.LastUsage = requestedDuration - self.TotalUsage += self.LastUsage - self.ExtraDuration -= dur - return requestedDuration, nil - } - initialExtraDuration := self.ExtraDuration - self.ExtraDuration = 0 - if self.CD.LoopIndex > 0 { - self.CD.TimeStart = self.CD.TimeEnd - } - self.CD.TimeEnd = self.CD.TimeStart.Add(dur) - self.CD.DurationIndex += dur - cc := &engine.CallCost{} - if err := self.rals.Call("Responder.MaxDebit", self.CD, cc); err != nil || cc.GetDuration() == 0 { - self.LastUsage = 0 - self.LastDebit = 0 - return 0, err - } - // cd corrections - self.CD.TimeEnd = cc.GetEndTime() // set debited timeEnd - // update call duration with real debited duration - ccDuration := cc.GetDuration() - if ccDuration > dur { - self.ExtraDuration = ccDuration - dur - } - if ccDuration >= dur { - self.LastUsage = requestedDuration - } else { - self.LastUsage = ccDuration - } - self.CD.DurationIndex -= dur - self.CD.DurationIndex += ccDuration - self.CD.MaxCostSoFar += cc.Cost - self.CD.LoopIndex += 1 - self.LastDebit = initialExtraDuration + ccDuration - self.TotalUsage += self.LastUsage - ec := engine.NewEventCostFromCallCost(cc, self.CGRID, self.RunID) - if self.EventCost == nil { - self.EventCost = ec - } else { - self.EventCost.Merge(ec) - } - if ccDuration < dur { - return initialExtraDuration + ccDuration, nil - } - return requestedDuration, nil -} - -// Send disconnect order to remote connection -func (self *SMGSession) disconnectSession(reason string) error { - if self.clntConn == nil || reflect.ValueOf(self.clntConn).IsNil() { - return errors.New("Calling SMGClientV1.DisconnectSession requires bidirectional JSON connection") - } - self.EventStart.Set(utils.Usage, self.TotalUsage) // Set the usage to total one debitted - var reply string - servMethod := "SessionSv1.DisconnectSession" - if self.clientProto == 0 { // competibility with OpenSIPS - servMethod = "SMGClientV1.DisconnectSession" - } - if err := self.clntConn.Call(servMethod, - utils.AttrDisconnectSession{EventStart: self.EventStart.AsMapInterface(), - Reason: reason}, - &reply); err != nil { - if err != utils.ErrNotImplemented { - return err - } - err = nil - } else if reply != utils.OK { - return errors.New(fmt.Sprintf("Unexpected disconnect reply: %s", reply)) - } - return nil -} - -// Session has ended, check debits and refund the extra charged duration -func (self *SMGSession) close(usage time.Duration) (err error) { - self.Lock() - defer self.Unlock() - if self.EventCost == nil { - return - } - if notCharged := usage - self.EventCost.GetUsage(); notCharged > 0 { // we did not charge enough, make a manual debit here - if self.CD.LoopIndex > 0 { - self.CD.TimeStart = self.CD.TimeEnd - } - self.CD.TimeEnd = self.CD.TimeStart.Add(notCharged) - self.CD.DurationIndex += notCharged - cc := &engine.CallCost{} - if err = self.rals.Call("Responder.Debit", self.CD, cc); err == nil { - self.EventCost.Merge( - engine.NewEventCostFromCallCost(cc, self.CGRID, self.RunID)) - } - } else if notCharged < 0 { // charged too much, try refund - err = self.refund(usage) - } - - return -} - -// Attempts to refund a duration, error on failure -// usage represents the real usage -func (self *SMGSession) refund(usage time.Duration) (err error) { - if self.EventCost == nil { - return - } - srplsEC, err := self.EventCost.Trim(usage) - if err != nil { - return err - } - if srplsEC == nil { - return - } - - cc := srplsEC.AsCallCost() - var incrmts engine.Increments - for _, tmspn := range cc.Timespans { - for _, incr := range tmspn.Increments { - if incr.BalanceInfo == nil || - (incr.BalanceInfo.Unit == nil && - incr.BalanceInfo.Monetary == nil) { - continue // not enough information for refunds, most probably free units uncounted - } - incrmts = append(incrmts, incr) - } - } - cd := &engine.CallDescriptor{ - CgrID: self.CGRID, - RunID: self.RunID, - Direction: self.CD.Direction, - Category: self.CD.Category, - Tenant: self.CD.Tenant, - Subject: self.CD.Subject, - Account: self.CD.Account, - Destination: self.CD.Destination, - TOR: self.CD.TOR, - Increments: incrmts, - } - var acnt engine.Account - err = self.rals.Call("Responder.RefundIncrements", cd, &acnt) - if acnt.ID != "" { // Account info updated, update also cached AccountSummary - self.EventCost.AccountSummary = acnt.AsAccountSummary() - } - return -} - -// storeSMCost will send the SMCost to CDRs for storing -func (self *SMGSession) storeSMCost() error { - if self.EventCost == nil { - return nil // There are no costs to save, ignore the operation - } - self.Lock() - self.Unlock() - smCost := &engine.V2SMCost{ - CGRID: self.CGRID, - CostSource: utils.MetaSessionS, - RunID: self.RunID, - OriginHost: self.EventStart.GetStringIgnoreErrors(utils.OriginHost), - OriginID: self.EventStart.GetStringIgnoreErrors(utils.OriginID), - Usage: self.TotalUsage, - CostDetails: self.EventCost, - } - var reply string - if err := self.cdrsrv.Call("CdrsV2.StoreSMCost", - engine.ArgsV2CDRSStoreSMCost{Cost: smCost, - CheckDuplicate: true}, &reply); err != nil { - if err == utils.ErrExists { - self.refund(self.CD.GetDuration()) // Refund entire duration - } else { - return err - } - } - return nil -} - -func (self *SMGSession) AsActiveSession(timezone string) *ActiveSession { - self.RLock() - defer self.RUnlock() - aSession := &ActiveSession{ - CGRID: self.CGRID, - TOR: self.EventStart.GetStringIgnoreErrors(utils.ToR), - RunID: self.RunID, - OriginID: self.EventStart.GetStringIgnoreErrors(utils.OriginID), - CdrHost: self.EventStart.GetStringIgnoreErrors(utils.OriginHost), - CdrSource: utils.SessionS + "_" + self.EventStart.GetStringIgnoreErrors(utils.EVENT_NAME), - ReqType: self.EventStart.GetStringIgnoreErrors(utils.RequestType), - Tenant: self.EventStart.GetStringIgnoreErrors(utils.Tenant), - Category: self.EventStart.GetStringIgnoreErrors(utils.Category), - Account: self.EventStart.GetStringIgnoreErrors(utils.Account), - Subject: self.EventStart.GetStringIgnoreErrors(utils.Subject), - Destination: self.EventStart.GetStringIgnoreErrors(utils.Destination), - SetupTime: self.EventStart.GetTimeIgnoreErrors(utils.SetupTime, self.Timezone), - AnswerTime: self.EventStart.GetTimeIgnoreErrors(utils.AnswerTime, self.Timezone), - Usage: self.TotalUsage, - ExtraFields: self.EventStart.AsMapStringIgnoreErrors(utils.MainCDRFieldsMap), - SMId: "CGR-DA", - } - if self.CD != nil { - aSession.LoopIndex = self.CD.LoopIndex - aSession.DurationIndex = self.CD.DurationIndex - aSession.MaxRate = self.CD.MaxRate - aSession.MaxRateUnit = self.CD.MaxRateUnit - aSession.MaxCostSoFar = self.CD.MaxCostSoFar - } - return aSession -} - // Will be used when displaying active sessions via RPC type ActiveSession struct { CGRID string - TOR string // type of record, meta-field, should map to one of the TORs hardcoded inside the server <*voice|*data|*sms|*generic> + RunID string + ToR string // type of record, meta-field, should map to one of the TORs hardcoded inside the server <*voice|*data|*sms|*generic> OriginID string // represents the unique accounting id given by the telecom switch generating the CDR - CdrHost string // represents the IP address of the host generating the CDR (automatically populated by the server) - CdrSource string // formally identifies the source of the CDR (free form field) - ReqType string // matching the supported request types by the **CGRateS**, accepted values are hardcoded in the server + OriginHost string // represents the IP address of the host generating the CDR (automatically populated by the server) + Source string // formally identifies the source of the CDR (free form field) + RequestType string // matching the supported request types by the **CGRateS**, accepted values are hardcoded in the server Tenant string // tenant whom this record belongs Category string // free-form filter for this record, matching the category defined in rating profiles. Account string // account id (accounting subsystem) the record should be attached to @@ -347,12 +53,134 @@ type ActiveSession struct { AnswerTime time.Time // answer time of the event. Supported formats: datetime RFC3339 compatible, SQL datetime (eg: MySQL), unix timestamp. Usage time.Duration // event usage information (eg: in case of tor=*voice this will represent the total duration of a call) ExtraFields map[string]string // Extra fields to be stored in CDR - SMId string - SMConnId string - RunID string + NodeID string LoopIndex float64 // indicates the position of this segment in a cost request loop DurationIndex time.Duration // the call duration so far (till TimeEnd) MaxRate float64 MaxRateUnit time.Duration MaxCostSoFar float64 } + +type Session struct { + sync.RWMutex + + CGRID string + Tenant string + ResourceID string + ClientConnID string // connection ID towards the client so we can recover from passive + EventStart *engine.SafEvent // Event which started the session + SRuns []*SRun // forked based on ChargerS + + debitStop chan struct{} + sTerminator *sTerminator // automatic timeout for the session +} + +// CGRid is a thread-safe method to return the CGRID of a session +func (s *Session) CGRid() (cgrID string) { + s.RLock() + cgrID = s.CGRID + s.RUnlock() + return +} + +// Clone is a thread safe method to clone the sessions information +func (s Session) Clone() (cln *Session) { + s.RLock() + cln = &Session{ + CGRID: s.CGRID, + Tenant: s.Tenant, + ResourceID: s.ResourceID, + EventStart: s.EventStart.Clone(), + ClientConnID: s.ClientConnID, + } + if s.SRuns != nil { + cln.SRuns = make([]*SRun, len(s.SRuns)) + for i, sR := range s.SRuns { + cln.SRuns[i] = sR.Clone() + } + } + s.RUnlock() + return +} + +func (s *Session) AsActiveSessions(tmz, nodeID string) (aSs []*ActiveSession) { + s.RLock() + aSs = make([]*ActiveSession, len(s.SRuns)) + for i, sr := range s.SRuns { + aSs[i] = &ActiveSession{ + CGRID: s.CGRID, + RunID: sr.Event.GetStringIgnoreErrors(utils.RunID), + ToR: sr.Event.GetStringIgnoreErrors(utils.ToR), + OriginID: s.EventStart.GetStringIgnoreErrors(utils.OriginID), + OriginHost: s.EventStart.GetStringIgnoreErrors(utils.OriginHost), + Source: utils.SessionS + "_" + s.EventStart.GetStringIgnoreErrors(utils.EVENT_NAME), + RequestType: sr.Event.GetStringIgnoreErrors(utils.RequestType), + Tenant: s.Tenant, + Category: sr.Event.GetStringIgnoreErrors(utils.Category), + Account: sr.Event.GetStringIgnoreErrors(utils.Account), + Subject: sr.Event.GetStringIgnoreErrors(utils.Subject), + Destination: sr.Event.GetStringIgnoreErrors(utils.Destination), + SetupTime: sr.Event.GetTimeIgnoreErrors(utils.SetupTime, tmz), + AnswerTime: sr.Event.GetTimeIgnoreErrors(utils.AnswerTime, tmz), + Usage: sr.TotalUsage, + ExtraFields: sr.Event.AsMapStringIgnoreErrors( + utils.NewStringMap(utils.PrimaryCdrFields...)), + NodeID: nodeID, + } + if sr.CD != nil { + aSs[i].LoopIndex = sr.CD.LoopIndex + aSs[i].DurationIndex = sr.CD.DurationIndex + aSs[i].MaxRate = sr.CD.MaxRate + aSs[i].MaxRateUnit = sr.CD.MaxRateUnit + aSs[i].MaxCostSoFar = sr.CD.MaxCostSoFar + } + } + s.RUnlock() + return +} + +// SRun is one billing run for the Session +type SRun struct { + Event engine.MapEvent // Event received from ChargerS + CD *engine.CallDescriptor // initial CD used for debits, updated on each debit + EventCost *engine.EventCost + + ExtraDuration time.Duration // keeps the current duration debited on top of what has been asked + LastUsage time.Duration // last requested Duration + TotalUsage time.Duration // sum of lastUsage +} + +// Clone returns the cloned version of SRun +func (sr *SRun) Clone() *SRun { + return &SRun{ + CD: sr.CD.Clone(), + EventCost: sr.EventCost.Clone(), + ExtraDuration: sr.ExtraDuration, + LastUsage: sr.LastUsage, + TotalUsage: sr.TotalUsage, + } +} + +// debitReserve attempty to debit from ExtraDuration and returns remaining duration +// if lastUsage is not nil, the ExtraDuration is corrected +func (sr *SRun) debitReserve(dur time.Duration, lastUsage *time.Duration) (rDur time.Duration) { + if lastUsage != nil && + sr.LastUsage != *lastUsage { + sr.ExtraDuration -= sr.LastUsage + sr.ExtraDuration += *lastUsage + sr.TotalUsage -= sr.LastUsage + sr.TotalUsage += *lastUsage + sr.LastUsage = *lastUsage + } + // debit from reserved + if sr.ExtraDuration >= dur { + sr.ExtraDuration -= dur + sr.LastUsage = dur + sr.TotalUsage += dur + rDur = time.Duration(0) // complete debit from reserve + } else { + rDur = dur - sr.ExtraDuration + sr.ExtraDuration = 0 + } + return +} diff --git a/sessions/sessions.go b/sessions/sessions.go index b74279b7a..deaeef2e2 100644 --- a/sessions/sessions.go +++ b/sessions/sessions.go @@ -30,7 +30,7 @@ import ( "github.com/cenkalti/rpc2" "github.com/cgrates/cgrates/config" "github.com/cgrates/cgrates/engine" - "github.com/cgrates/cgrates/guardian" + "github.com/cgrates/cgrates/utils" "github.com/cgrates/rpcclient" ) @@ -41,38 +41,43 @@ const ( ) var ( - ErrPartiallyExecuted = errors.New("Partially executed") + ErrPartiallyExecuted = errors.New("PARTIALLY_EXECUTED") ErrActiveSession = errors.New("ACTIVE_SESSION") + ErrForcedDisconnect = errors.New("FORCED_DISCONNECT") debug bool ) -func NewSessionReplicationConns(conns []*config.HaPoolConfig, reconnects int, - connTimeout, replyTimeout time.Duration) (smgConns []*SMGReplicationConn, err error) { - smgConns = make([]*SMGReplicationConn, len(conns)) +// NewSReplConns initiates the connections configured for session replication +func NewSReplConns(conns []*config.HaPoolConfig, reconnects int, + connTimeout, replyTimeout time.Duration) (sReplConns []*SReplConn, err error) { + sReplConns = make([]*SReplConn, len(conns)) for i, replConnCfg := range conns { if replCon, err := rpcclient.NewRpcClient("tcp", replConnCfg.Address, replConnCfg.Tls, "", "", "", 0, reconnects, connTimeout, replyTimeout, replConnCfg.Transport[1:], nil, true); err != nil { return nil, err } else { - smgConns[i] = &SMGReplicationConn{Connection: replCon, Synchronous: replConnCfg.Synchronous} + sReplConns[i] = &SReplConn{Connection: replCon, Synchronous: replConnCfg.Synchronous} } } return } -// ReplicationConnection represents one connection to a passive node where we will replicate session data -type SMGReplicationConn struct { +// SReplConn represents one connection to a passive node where we will replicate session data +type SReplConn struct { Connection rpcclient.RpcClientConnection Synchronous bool } -func NewSMGeneric(cgrCfg *config.CGRConfig, rals, resS, thdS, - statS, splS, attrS, cdrsrv, chargerS rpcclient.RpcClientConnection, - smgReplConns []*SMGReplicationConn, timezone string) *SMGeneric { - ssIdxCfg := cgrCfg.SessionSCfg().SessionIndexes - ssIdxCfg[utils.OriginID] = true // Make sure we have indexing for OriginID since it is a requirement on prefix searching - if rals != nil && reflect.ValueOf(rals).IsNil() { - rals = nil +// NewSessionS constructs a new SessionS instance +func NewSessionS(cgrCfg *config.CGRConfig, ralS, resS, thdS, + statS, splS, attrS, cdrS, chargerS rpcclient.RpcClientConnection, + sReplConns []*SReplConn, tmz string) *SessionS { + cgrCfg.SessionSCfg().SessionIndexes[utils.OriginID] = true // Make sure we have indexing for OriginID since it is a requirement on prefix searching + if chargerS != nil && reflect.ValueOf(chargerS).IsNil() { + chargerS = nil + } + if ralS != nil && reflect.ValueOf(ralS).IsNil() { + ralS = nil } if resS != nil && reflect.ValueOf(resS).IsNil() { resS = nil @@ -89,64 +94,165 @@ func NewSMGeneric(cgrCfg *config.CGRConfig, rals, resS, thdS, if attrS != nil && reflect.ValueOf(attrS).IsNil() { attrS = nil } - if cdrsrv != nil && reflect.ValueOf(cdrsrv).IsNil() { - cdrsrv = nil + if cdrS != nil && reflect.ValueOf(cdrS).IsNil() { + cdrS = nil } - if chargerS != nil && reflect.ValueOf(chargerS).IsNil() { - chargerS = nil - } - return &SMGeneric{ - cgrCfg: cgrCfg, - chargerS: chargerS, - rals: rals, - resS: resS, - thdS: thdS, - statS: statS, - splS: splS, - attrS: attrS, - cdrsrv: cdrsrv, - smgReplConns: smgReplConns, - Timezone: timezone, - biJsonConns: make(map[*rpc2.Client]struct{}), - activeSessions: make(map[string][]*SMGSession), - ssIdxCfg: ssIdxCfg, - aSessionsIndex: make(map[string]map[string]map[string]utils.StringMap), - aSessionsRIndex: make(map[string][]*riFieldNameVal), - passiveSessions: make(map[string][]*SMGSession), - pSessionsIndex: make(map[string]map[string]map[string]utils.StringMap), - pSessionsRIndex: make(map[string][]*riFieldNameVal), - sessionTerminators: make(map[string]*smgSessionTerminator), - responseCache: utils.NewResponseCache(cgrCfg.GeneralCfg().ResponseCacheTTL)} + return &SessionS{ + cgrCfg: cgrCfg, + chargerS: chargerS, + ralS: ralS, + resS: resS, + thdS: thdS, + statS: statS, + splS: splS, + attrS: attrS, + cdrS: cdrS, + respCache: utils.NewResponseCache(cgrCfg.GeneralCfg().ResponseCacheTTL), + sReplConns: sReplConns, + biJClnts: make(map[rpcclient.RpcClientConnection]string), + biJIDs: make(map[string]*biJClient), + aSessions: make(map[string]*Session), + aSessionsIdx: make(map[string]map[string]map[string]utils.StringMap), + aSessionsRIdx: make(map[string][]*riFieldNameVal), + pSessions: make(map[string]*Session), + pSessionsIdx: make(map[string]map[string]map[string]utils.StringMap), + pSessionsRIdx: make(map[string][]*riFieldNameVal)} } -type SMGeneric struct { - cgrCfg *config.CGRConfig // Separate from smCfg since there can be multiple - chargerS rpcclient.RpcClientConnection - rals rpcclient.RpcClientConnection // RALs connections - resS rpcclient.RpcClientConnection // ResourceS connections - thdS rpcclient.RpcClientConnection // ThresholdS connections - statS rpcclient.RpcClientConnection // StatS connections - splS rpcclient.RpcClientConnection // SupplierS connections - attrS rpcclient.RpcClientConnection // AttributeS connections - cdrsrv rpcclient.RpcClientConnection // CDR server connections - smgReplConns []*SMGReplicationConn // list of connections where we will replicate our session data - Timezone string - intBiJSONConns []rpcclient.RpcClientConnection - biJsonConns map[*rpc2.Client]struct{} // index BiJSONConnection so we can sync them later - activeSessions map[string][]*SMGSession // group sessions per sessionId, multiple runs based on derived charging - aSessionsMux sync.RWMutex - ssIdxCfg utils.StringMap // index configuration - aSessionsIndex map[string]map[string]map[string]utils.StringMap // map[fieldName]map[fieldValue][runID]utils.StringMap[cgrID] - aSessionsRIndex map[string][]*riFieldNameVal // reverse indexes for active sessions, used on remove - aSIMux sync.RWMutex // protects aSessionsIndex - passiveSessions map[string][]*SMGSession // group passive sessions - pSessionsMux sync.RWMutex - pSessionsIndex map[string]map[string]map[string]utils.StringMap // map[fieldName]map[fieldValue][runID]utils.StringMap[cgrID] - pSessionsRIndex map[string][]*riFieldNameVal // reverse indexes for active sessions, used on remove - pSIMux sync.RWMutex // protects pSessionsIndex - sessionTerminators map[string]*smgSessionTerminator // terminate and cleanup the session if timer expires - sTsMux sync.RWMutex // protects sessionTerminators - responseCache *utils.ResponseCache // cache replies here +// biJClient contains info we need to reach back a bidirectional json client +type biJClient struct { + conn rpcclient.RpcClientConnection // connection towards BiJ client + proto float64 // client protocol version +} + +// SessionS represents the session service +type SessionS struct { + cgrCfg *config.CGRConfig // Separate from smCfg since there can be multiple + + chargerS rpcclient.RpcClientConnection + ralS rpcclient.RpcClientConnection // RALs connections + resS rpcclient.RpcClientConnection // ResourceS connections + thdS rpcclient.RpcClientConnection // ThresholdS connections + statS rpcclient.RpcClientConnection // StatS connections + splS rpcclient.RpcClientConnection // SupplierS connections + attrS rpcclient.RpcClientConnection // AttributeS connections + cdrS rpcclient.RpcClientConnection // CDR server connections + + respCache *utils.ResponseCache // cache replies + sReplConns []*SReplConn // list of connections where we will replicate our session data + + biJMux sync.RWMutex // mux protecting BI-JSON connections + biJClnts map[rpcclient.RpcClientConnection]string // index BiJSONConnection so we can sync them later + biJIDs map[string]*biJClient // identifiers of bidirectional JSON conns, used to call RPC based on connIDs + + aSsMux sync.RWMutex // protects aSessions + aSessions map[string]*Session // group sessions per sessionId, multiple runs based on derived charging + + aSIMux sync.RWMutex // protects aSessionsIdx + aSessionsIdx map[string]map[string]map[string]utils.StringMap // map[fieldName]map[fieldValue][runID]utils.StringMap[cgrID] + aSessionsRIdx map[string][]*riFieldNameVal // reverse indexes for active sessions, used on remove + + pSsMux sync.RWMutex // protects pSessions + pSessions map[string]*Session // group passive sessions based on cgrID + + pSIMux sync.RWMutex // protects pSessionsIdx + pSessionsIdx map[string]map[string]map[string]utils.StringMap // map[fieldName]map[fieldValue][runID]utils.StringMap[cgrID] + pSessionsRIdx map[string][]*riFieldNameVal // reverse indexes for passive sessions, used on remove + +} + +// ListenAndServe starts the service and binds it to the listen loop +func (sS *SessionS) ListenAndServe(exitChan chan bool) (err error) { + if sS.cgrCfg.SessionSCfg().ChannelSyncInterval != 0 { + go func() { + for { // Schedule sync channels to run repetately + select { + case e := <-exitChan: + exitChan <- e + break + case <-time.After(sS.cgrCfg.SessionSCfg().ChannelSyncInterval): + sS.syncSessions() + } + } + + }() + } + e := <-exitChan // block here until shutdown request + exitChan <- e // put back for the others listening for shutdown request + return +} + +// Shutdown is called by engine to clear states +func (sS *SessionS) Shutdown() (err error) { + for _, s := range sS.getSessions("", false) { // Force sessions shutdown + sS.endSession(s, nil) + } + return +} + +// OnBiJSONConnect is called by rpc2.Client on each new connection +func (sS *SessionS) OnBiJSONConnect(c *rpc2.Client) { + sS.biJMux.Lock() + nodeID := utils.UUIDSha1Prefix() // connection identifier, should be later updated as login procedure + sS.biJClnts[c] = nodeID + sS.biJIDs[nodeID] = &biJClient{conn: c, + proto: sS.cgrCfg.SessionSCfg().ClientProtocol} + sS.biJMux.Unlock() +} + +// OnBiJSONDisconnect is called by rpc2.Client on each client disconnection +func (sS *SessionS) OnBiJSONDisconnect(c *rpc2.Client) { + sS.biJMux.Lock() + if nodeID, has := sS.biJClnts[c]; has { + delete(sS.biJClnts, c) + delete(sS.biJIDs, nodeID) + } + sS.biJMux.Unlock() +} + +// RegisterIntBiJConn is called on each internal BiJ connection towards SessionS +func (sS *SessionS) RegisterIntBiJConn(c rpcclient.RpcClientConnection) { + sS.biJMux.Lock() + nodeID := sS.cgrCfg.GeneralCfg().NodeID + sS.biJClnts[c] = nodeID + sS.biJIDs[nodeID] = &biJClient{conn: c, + proto: sS.cgrCfg.SessionSCfg().ClientProtocol} + sS.biJMux.Unlock() +} + +// biJClnt returns a bidirectional JSON client based on connection ID +func (sS *SessionS) biJClnt(connID string) (clnt *biJClient) { + if connID == "" { + return nil + } + sS.biJMux.RLock() + clnt = sS.biJIDs[connID] + sS.biJMux.RUnlock() + return +} + +// biJClnt returns connection ID based on bidirectional connection received +func (sS *SessionS) biJClntID(c rpcclient.RpcClientConnection) (clntConnID string) { + if c == nil { + return + } + sS.biJMux.RLock() + clntConnID = sS.biJClnts[c] + sS.biJMux.RUnlock() + return +} + +// biJClnts is a thread-safe method to return the list of active clients for BiJson +func (sS *SessionS) biJClients() (clnts []*biJClient) { + sS.biJMux.RLock() + clnts = make([]*biJClient, len(sS.biJIDs)) + i := 0 + for _, clnt := range sS.biJIDs { + clnts[i] = clnt + i++ + } + sS.biJMux.RUnlock() + return } // riFieldNameVal is a reverse index entry @@ -154,126 +260,134 @@ type riFieldNameVal struct { runID, fieldName, fieldValue string } -type smgSessionTerminator struct { +// sTerminator holds the info needed to force-terminate sessions based on timer +type sTerminator struct { timer *time.Timer - endChan chan bool + endChan chan struct{} ttl time.Duration ttlLastUsed *time.Duration ttlUsage *time.Duration } -// setSessionTerminator installs a new terminator for a session -func (smg *SMGeneric) setSessionTerminator(s *SMGSession) { +// setSTerminator installs a new terminator for a session +func (sS *SessionS) setSTerminator(s *Session) { ttl, err := s.EventStart.GetDuration(utils.SessionTTL) switch err { case nil: // all good case utils.ErrNotFound: - ttl = smg.cgrCfg.SessionSCfg().SessionTTL + ttl = sS.cgrCfg.SessionSCfg().SessionTTL default: // not nil utils.Logger.Warning( - fmt.Sprintf("<%s>, cannot extract %s from event, disabling session timeout for event: <%s>", - utils.SessionS, utils.SessionTTL, s.EventStart.String())) - ttl = time.Duration(0) + fmt.Sprintf("<%s>, cannot extract %s from event: %s, err: %s", + utils.SessionS, utils.SessionTTL, s.EventStart.String(), err.Error())) } - if ttl == 0 { - return + s.Lock() + defer s.Unlock() + if ttl == 0 && s.sTerminator == nil { + return // nothing to set up } // random delay computation - var sessionTTLMaxDelay int64 + var sTLMaxDelay int64 maxDelay, err := s.EventStart.GetDuration(utils.SessionTTLMaxDelay) switch err { case nil: // all good case utils.ErrNotFound: - if smg.cgrCfg.SessionSCfg().SessionTTLMaxDelay != nil { - maxDelay = *smg.cgrCfg.SessionSCfg().SessionTTLMaxDelay + if sS.cgrCfg.SessionSCfg().SessionTTLMaxDelay != nil { + maxDelay = *sS.cgrCfg.SessionSCfg().SessionTTLMaxDelay } default: // not nil utils.Logger.Warning( - fmt.Sprintf("<%s>, cannot extract %s from event, disabling session timeout for event: <%s>", - utils.SessionS, utils.SessionTTLMaxDelay, s.EventStart.String())) + fmt.Sprintf("<%s>, cannot extract %s from event %s, err: %s", + utils.SessionS, utils.SessionTTLMaxDelay, s.EventStart.String(), err.Error())) return } - sessionTTLMaxDelay = maxDelay.Nanoseconds() / 1000000 // Milliseconds precision for randomness - if sessionTTLMaxDelay != 0 { + sTTLMaxDelay := maxDelay.Nanoseconds() / time.Millisecond.Nanoseconds() // Milliseconds precision for randomness + if sTTLMaxDelay != 0 { rand.Seed(time.Now().Unix()) - ttl += time.Duration(rand.Int63n(sessionTTLMaxDelay) * 1000000) + ttl += time.Duration(rand.Int63n(sTTLMaxDelay) * time.Millisecond.Nanoseconds()) } - ttlLastUsed, err := s.EventStart.GetDurationPtrOrDefault(utils.SessionTTLLastUsed, smg.cgrCfg.SessionSCfg().SessionTTLLastUsed) + ttlLastUsed, err := s.EventStart.GetDurationPtrOrDefault(utils.SessionTTLLastUsed, sS.cgrCfg.SessionSCfg().SessionTTLLastUsed) if err != nil { utils.Logger.Warning( fmt.Sprintf("<%s>, cannot extract %s from event, disabling session timeout for event: <%s>", utils.SessionS, utils.SessionTTLLastUsed, s.EventStart.String())) return } - ttlUsage, err := s.EventStart.GetDurationPtrOrDefault(utils.SessionTTLUsage, smg.cgrCfg.SessionSCfg().SessionTTLUsage) + ttlUsage, err := s.EventStart.GetDurationPtrOrDefault(utils.SessionTTLUsage, sS.cgrCfg.SessionSCfg().SessionTTLUsage) if err != nil { utils.Logger.Warning( fmt.Sprintf("<%s>, cannot extract %s from event, disabling session timeout for event: <%s>", utils.SessionS, utils.SessionTTLUsage, s.EventStart.String())) return } - // add to sessionTerimnations - smg.sTsMux.Lock() - defer smg.sTsMux.Unlock() - if _, found := smg.sessionTerminators[s.CGRID]; found { // already there, no need to set up + if s.sTerminator != nil { + if ttl != 0 { // only change if different than 0 + s.sTerminator.ttl = ttl + if ttlLastUsed != nil { + s.sTerminator.ttlLastUsed = ttlLastUsed + } + if ttlUsage != nil { + s.sTerminator.ttlUsage = ttlUsage + } + s.sTerminator.timer.Reset(s.sTerminator.ttl) + } return } timer := time.NewTimer(ttl) - endChan := make(chan bool, 1) - terminator := &smgSessionTerminator{ + endChan := make(chan struct{}, 1) + s.sTerminator = &sTerminator{ timer: timer, endChan: endChan, ttl: ttl, ttlLastUsed: ttlLastUsed, ttlUsage: ttlUsage, } - smg.sessionTerminators[s.CGRID] = terminator - go func(cgrID string) { + go func() { // schedule automatic termination select { case <-timer.C: - smg.ttlTerminate(s, terminator) + eUsage := s.sTerminator.ttl + if s.sTerminator.ttlUsage != nil { + eUsage = *s.sTerminator.ttlUsage + } + sS.forceSTerminate(s.CGRid(), eUsage, + s.sTerminator.ttlLastUsed) case <-endChan: - timer.Stop() + s.sTerminator.timer.Stop() } - smg.sTsMux.Lock() - delete(smg.sessionTerminators, cgrID) - smg.sTsMux.Unlock() - }(s.CGRID) // Need to pass cgrID since the one from session will change during rename + s.Lock() + s.sTerminator = nil + s.Unlock() + }() } -// resetTerminatorTimer updates the timer for the session to a new ttl and terminate info -func (smg *SMGeneric) resetTerminatorTimer(cgrID string, ttl time.Duration, ttlLastUsed, ttlUsage *time.Duration) { - smg.sTsMux.RLock() - if st, found := smg.sessionTerminators[cgrID]; found { - if ttl != 0 { - st.ttl = ttl +// forceSTerminate is called when a session times-out or it is forced from CGRateS side +func (sS *SessionS) forceSTerminate(cgrID string, extraDebit time.Duration, lastUsed *time.Duration) (err error) { + if cgrID == "" { // can only work for one session since we consider tenant and other info from within + return utils.ErrMandatoryIeMissing + } + ss := sS.getSessions(cgrID, false) + if len(ss) == 0 { // will not continue if the session is not longer active + utils.Logger.Warning( + fmt.Sprintf("<%s> could not force session terminate for cgrID %s, no active session found", + utils.SessionS, cgrID)) + return utils.ErrNoActiveSession + } + s := ss[0] + if extraDebit != 0 { + for i := range s.SRuns { + if err = sS.debit(s, i, extraDebit, extraDebit, nil); err != nil { + utils.Logger.Warning( + fmt.Sprintf("<%s> failed debitting cgrID %s, sRunIdx: %d, err: %s", + utils.SessionS, cgrID, i, err.Error())) + } } - if ttlLastUsed != nil { - st.ttlLastUsed = ttlLastUsed - } - if ttlUsage != nil { - st.ttlUsage = ttlUsage - } - st.timer.Reset(st.ttl) } - smg.sTsMux.RUnlock() -} - -// ttlTerminate is called when a session times-out -func (smg *SMGeneric) ttlTerminate(s *SMGSession, tmtr *smgSessionTerminator) { - debitUsage := tmtr.ttl - if tmtr.ttlUsage != nil { - debitUsage = *tmtr.ttlUsage + if sS.sessionEnd(s, s.TotalUsage); err != nil { + utils.Logger.Warning( + fmt.Sprintf("<%s> failed force ending session with ID %s, err: %s", + utils.SessionS, cgrID, err.Error())) } - aSessions := smg.getSessions(s.CGRID, false) - if len(aSessions) == 0 { // will not continue if the session is not longer active - return - } - for _, s := range aSessions[s.CGRID] { - s.debit(debitUsage, tmtr.ttlLastUsed) - } - smg.sessionEnd(s.CGRID, s.TotalUsage) - cdr, err := s.EventStart.AsCDR(smg.cgrCfg, s.Tenant, smg.Timezone) + cdr, err := s.EventStart.AsCDR(sS.cgrCfg, s.Tenant, sS.tmz) if err != nil { utils.Logger.Warning( fmt.Sprintf("<%s> could not create CDR out of event %s, err: %s", @@ -286,10 +400,10 @@ func (smg *SMGeneric) ttlTerminate(s *SMGSession, tmtr *smgSessionTerminator) { ID: utils.UUIDSha1Prefix(), Event: cdr.AsMapStringIface(), } - if err = smg.cdrsrv.Call(utils.CdrsV2ProcessCDR, cgrEv, &reply); err != nil { + if err = sS.cdrS.Call(utils.CdrsV2ProcessCDR, cgrEv, &reply); err != nil { return } - if smg.resS != nil && s.ResourceID != "" { + if sS.resS != nil && s.ResourceID != "" { var reply string argsRU := utils.ArgRSv1ResourceUsage{ CGREvent: utils.CGREvent{ @@ -299,63 +413,352 @@ func (smg *SMGeneric) ttlTerminate(s *SMGSession, tmtr *smgSessionTerminator) { UsageID: s.ResourceID, Units: 1, } - if err := smg.resS.Call(utils.ResourceSv1ReleaseResources, + if err := sS.resS.Call(utils.ResourceSv1ReleaseResources, argsRU, &reply); err != nil { utils.Logger.Warning( fmt.Sprintf("<%s> error: %s could not release resource with resourceID: %s", utils.SessionS, err.Error(), s.ResourceID)) } } - smg.replicateSessionsWithID(s.CGRID, false, smg.smgReplConns) + if s.clntConn != nil { + var rply string + if err := s.clntConn.Call(utils.SessionSv1DisconnectSession, + utils.AttrDisconnectSession{ + EventStart: s.EventStart.AsMapInterface(), + Reason: ErrForcedDisconnect.Error()}, + &reply); err != nil { + if err != utils.ErrNotImplemented { + utils.Logger.Warning( + fmt.Sprintf("<%s> err: %s remotely disconnect session with id: %s", + utils.SessionS, err.Error(), s.CGRID)) + } + } + } + sS.replicateSessions(s.CGRID, false) + return } -func (smg *SMGeneric) recordASession(s *SMGSession) { - smg.aSessionsMux.Lock() - smg.activeSessions[s.CGRID] = append(smg.activeSessions[s.CGRID], s) - smg.setSessionTerminator(s) - smg.indexSession(s, false) - smg.aSessionsMux.Unlock() +// debitSession performs debit for a session run +func (sS *SessionS) debitSession(s *Session, sRunIdx int, dur time.Duration, + lastUsed *time.Duration) (maxDur time.Duration, err error) { + s.Lock() + if sRunIdx >= len(s.SRuns) { + err = errors.New("sRunIdx out of range") + s.Unlock() + return + } + sr := s.SRuns[sRunIdx] + rDur := sr.debitReserve(dur, lastUsage) // debit out of reserve, rDur is still to be debited + if rDur == time.Duration(0) { + s.Unlock() + return dur, nil // complete debit out of reserve + } + dbtRsrv := dur - rDur // the amount debited from reserve + if sr.CD.LoopIndex > 0 { + sr.CD.TimeStart = sr.CD.TimeEnd + } + sr.CD.TimeEnd = sr.CD.TimeStart.Add(rDur) + sr.CD.DurationIndex += rDur + cd := sr.CD.Clone() + s.Unlock() + cc := new(engine.CallCost) + if err := sS.ralS.Call("Responder.MaxDebit", cd, cc); err != nil { + s.Lock() + sr.ExtraDuration += dbtRsrv + s.Unlock() + return 0, err + } + s.Lock() + sr.CD.TimeEnd = cc.GetEndTime() // set debited timeEnd + ccDuration := cc.GetDuration() + if ccDuration > rDur { + sr.ExtraDuration = ccDuration - rDur + } + if ccDuration >= rDur { + sr.LastUsage = dur + } else { + sr.LastUsage = ccDuration + dbtRsrv + } + sr.CD.DurationIndex -= rDur + sr.CD.DurationIndex += ccDuration + sr.CD.MaxCostSoFar += cc.Cost + sr.CD.LoopIndex += 1 + sr.TotalUsage += sr.LastUsage + ec := engine.NewEventCostFromCallCost(cc, s.CGRID, sr.RunID) + if sr.EventCost == nil { + sr.EventCost = ec + } else { + sr.EventCost.Merge(ec) + } + maxDur = sr.LastUsage + s.Unlock() + return } -// Remove session from session list, removes all related in case of multiple runs, true if item was found -func (smg *SMGeneric) unrecordASession(cgrID string) bool { - smg.aSessionsMux.Lock() - defer smg.aSessionsMux.Unlock() - if _, found := smg.activeSessions[cgrID]; !found { +// debitLoopSession will periodically debit sessions, ie: automatic prepaid +func (sS *SessionS) debitLoopSession(s *Session, sRunIdx int, + dbtIvl time.Duration) (maxDur time.Duration, err error) { + + s.RLock() + lenSRuns := len(s.SRuns) + s.RUnlock() + if sRunIdx >= lenSRuns { + err = errors.New("sRunIdx out of range") + return + } + + var sleepDur time.Duration + for { + if maxDebit, err := sS.debitSession(dbtIvl, nil); err != nil { + utils.Logger.Warning( + fmt.Sprintf("<%s> could not complete debit operation on session: <%s>, error: <%s>", + utils.SessionS, self.CGRID, err.Error())) + dscReason := utils.ErrServerError.Error() + if err.Error() == utils.ErrUnauthorizedDestination.Error() { + dscReason = err.Error() + } + if err := sS.disconnectSession(dscReason); err != nil { + utils.Logger.Warning( + fmt.Sprintf("<%s> could not disconnect session: %s, error: %s", + utils.SessionS, self.CGRID, err.Error())) + } + return + } else if maxDebit < dbtIvl { + time.Sleep(maxDebit) + if err := self.disconnectSession(utils.ErrInsufficientCredit.Error()); err != nil { + utils.Logger.Warning( + fmt.Sprintf("<%s> could not disconnect session: %s, error: %s", + utils.SessionS, self.CGRID, err.Error())) + } + return + } + select { + case <-s.debitStop: + return + case <-time.After(dbtIvl): + continue + } + } + + return +} + +// refundSession will refund the extra usage debitted by the end of session +// not thread-safe so the locks need to be done in a layer above +// rUsage represents the amount of usage to be refunded +func (sS *SessionS) refundSession(s *Session, sRunIdx int, rUsage time.Duration) (err error) { + if sRunIdx >= len(s.SRuns) { + return errors.New("sRunIdx out of range") + } + sr := s.SRuns[sRunIdx] + if sr.EventCost == nil { + return errors.New("no event cost") + } + srplsEC, err := sr.EventCost.Trim(sr.EventCost.GetUsage() - rUsage) + if err != nil { + return err + } else if srplsEC == nil { + return + } + sCC := srplsEC.AsCallCost() + var incrmts engine.Increments + for _, tmspn := range sCC.Timespans { + for _, incr := range tmspn.Increments { + if incr.BalanceInfo == nil || + (incr.BalanceInfo.Unit == nil && + incr.BalanceInfo.Monetary == nil) { + continue // not enough information for refunds, most probably free units uncounted + } + incrmts = append(incrmts, incr) + } + } + cd := &engine.CallDescriptor{ + CgrID: s.CGRID, + RunID: sr.Event.GetStringIgnoreErrors(utils.RunID), + Direction: sr.CD.Direction, + Category: sr.CD.Category, + Tenant: sr.CD.Tenant, + Subject: sr.CD.Subject, + Account: sr.CD.Account, + Destination: sr.CD.Destination, + TOR: sr.CD.TOR, + Increments: incrmts, + } + var acnt engine.Account + if err = self.rals.Call(utils.ResponderRefundIncrements, cd, &acnt); err != nil { + return + } + if acnt.ID != "" { // Account info updated, update also cached AccountSummary + sr.EventCost.AccountSummary = acnt.AsAccountSummary() + } + return +} + +// storeSCost will post the session cost to CDRs +// not thread safe, need to be handled in a layer above +func (sS *SessionS) storeSCost(s *Session, sRunIdx int) (err error) { + if sRunIdx >= len(s.SRuns) { + return errors.New("sRunIdx out of range") + } + sr := s.SRuns[sRunIdx] + if sr.EventCost == nil { + return // no costs to save, ignore the operation + } + smCost := &engine.V2SMCost{ + CGRID: s.CGRID, + CostSource: utils.MetaSessionS, + RunID: sr.Event.GetStringIgnoreErrors(utils.RunID), + OriginHost: s.EventStart.GetStringIgnoreErrors(utils.OriginHost), + OriginID: s.EventStart.GetStringIgnoreErrors(utils.OriginID), + Usage: sr.TotalUsage, + CostDetails: sr.EventCost, + } + var reply string + if err := sS.cdrS.Call(utils.CdrsV2StoreSMCost, + engine.ArgsV2CDRSStoreSMCost{Cost: smCost, + CheckDuplicate: true}, &reply); err != nil { + if err == utils.ErrExists { + utils.Logger.Warning( + fmt.Sprintf("<%s> refunding session: <%s> error: <%s>", + utils.SessionS, cgrID, err.Error())) + if err = sS.refundSession(s, sRunIdx, sr.CD.GetDuration()); err != nil { // refund entire duration + utils.Logger.Warning( + fmt.Sprintf( + "<%s> failed refunding session: <%s>, srIdx: <%d>, error: <%s>", + utils.SessionS, cgrID, i, err.Error())) + } + } else { + return err + } + } + return nil +} + +// disconnectSession will send disconnect from SessionS to clients +func (sS *SessionS) disconnectSession(s *Session, rsn string) (err error) { + clnt := sS.biJClnt(s.ClientConnID) + if clnt == nil { + return fmt.Errorf("calling %s requires bidirectional JSON connection", utils.SessionSv1DisconnectSession) + } + s.RLock() // for reading the TotalUsage + s.EventStart.Set(utils.Usage, self.TotalUsage) // Set the usage to total one debitted + s.RUnlock() + servMethod := utils.SessionSv1DisconnectSession + if clnt.proto == 0 { // compatibility with OpenSIPS 2.3 + servMethod = "SMGClientV1.DisconnectSession" + } + var rply string + if err := clnt.conn.Call(servMethod, + utils.AttrDisconnectSession{EventStart: self.EventStart.AsMapInterface(), + Reason: rsn}, &rply); err != nil { + if err != utils.ErrNotImplemented { + return err + } + err = nil + } + return +} + +// replicateSessions will replicate sessions with or without cgrID specified +func (sS *SessionS) replicateSessions(cgrID string, psv bool, rplConns []*SReplConn) (err error) { + if len(rplConns) == 0 { + return + } + ss := sS.getSessions(cgrID, psv) + if len(ss) == 0 { + ss = []*Session{&Session{CGRID: cgrID}} // session scheduled to be removed from remote + } + var wg sync.WaitGroup + for _, rplConn := range sS.sReplConns { + if rplConn.Synchronous { + wg.Add(1) + } + go func(conn rpcclient.RpcClientConnection, sync bool, ss []*Session) { + for _, s := range ssMp { + var rply string + if err := conn.Call(utils.SessionSv1SetPassiveSession, + s.Clone(), &rply); err != nil { + utils.Logger.Warning( + fmt.Sprintf("<%s> cannot replicate session with id <%s>, err: %s", + utils.SessionS, s.CGRID, err.Error())) + } + } + if sync { + wg.Done() + } + }(rplConn.Connection, rplConn.Synchronous, ss) + } + wg.Wait() // wait for synchronous replication to finish + return +} + +// registerSession will register an active or passive Session +// called on init or relocate +func (sS *SessionS) registerSession(s *Session, passive bool) { + sMux := &sS.aSsMux + sMp := sS.aSessions + if passive { + sMux = &sS.pSsMux + sMp = sS.pSessions + } + sMux.Lock() + sMp[s.CGRID] = s + sS.indexSession(s, passive) + if !passive { + sS.setSessionTerminator(s) + } + sMux.Unlock() +} + +// uregisterSession will unregister an active or passive session based on it's CGRID +// called on session terminate or relocate +func (sS *SessionS) unregisterSession(cgrID string, passive bool) bool { + sMux := &sS.aSsMux + sMp := sS.aSessions + if passive { + sMux = &sS.pSsMux + sMp = sS.pSessions + } + sMux.Lock() + if _, found := sMp[cgrID]; !found { + sMux.Unlock() return false } - delete(smg.activeSessions, cgrID) - smg.sTsMux.RLock() - if st, found := smg.sessionTerminators[cgrID]; found { - st.endChan <- true + delete(sMp, cgrID) + sS.unindexSession(cgrID, passive) + if !passive { + sS.sTsMux.RLock() + if st, found := sS.sessionTerminators[cgrID]; found { + st.endChan <- true + } + sS.sTsMux.RUnlock() } - smg.sTsMux.RUnlock() - smg.unindexSession(cgrID, false) + sMux.Unlock() return true } -// indexSession explores settings and builds SessionsIndex -// uses different tables and mutex-es depending on active/passive session -func (smg *SMGeneric) indexSession(s *SMGSession, passiveSessions bool) { - idxMux := &smg.aSIMux // pointer to original mux since we cannot copy it - ssIndx := smg.aSessionsIndex - ssRIdx := smg.aSessionsRIndex +// indexSession will index an active or passive Session based on configuration +func (sS *SessionS) indexSession(s *Session, pSessions bool) { + idxMux := &sS.aSIMux // pointer to original mux since will have no effect if we copy it + ssIndx := sS.aSessionsIndex + ssRIdx := sS.aSessionsRIndex if passiveSessions { - idxMux = &smg.pSIMux - ssIndx = smg.pSessionsIndex - ssRIdx = smg.pSessionsRIndex + idxMux = &sS.pSIMux + ssIndx = sS.pSessionsIndex + ssRIdx = sS.pSessionsRIndex } idxMux.Lock() defer idxMux.Unlock() s.RLock() defer s.RUnlock() - for fieldName := range smg.ssIdxCfg { + idxCfg := sS.cgrCfg.SessionSCfg().SessionIndexes + for fieldName := range idxCfg { fieldVal, err := s.EventStart.GetString(fieldName) if err != nil { if err == utils.ErrNotFound { fieldVal = utils.NOT_AVAILABLE } else { - utils.Logger.Err(fmt.Sprintf("<%s> Error retrieving field: %s from event: %+v", utils.SessionS, fieldName, s.EventStart)) + utils.Logger.Err(fmt.Sprintf("<%s> retrieving field: %s from event: %+v, err: <%s>", utils.SessionS, fieldName, s.EventStart)) continue } } @@ -380,15 +783,16 @@ func (smg *SMGeneric) indexSession(s *SMGSession, passiveSessions bool) { return } -// unindexASession removes a session from indexes -func (smg *SMGeneric) unindexSession(cgrID string, passiveSessions bool) bool { - idxMux := &smg.aSIMux - ssIndx := smg.aSessionsIndex - ssRIdx := smg.aSessionsRIndex - if passiveSessions { - idxMux = &smg.pSIMux - ssIndx = smg.pSessionsIndex - ssRIdx = smg.pSessionsRIndex +// unindexASession removes an active or passive session from indexes +// called on terminate or relocate +func (sS *SessionS) unindexSession(cgrID string, pSessions bool) bool { + idxMux := &sS.aSIMux + ssIndx := sS.aSessionsIdx + ssRIdx := sS.aSessionsRIdx + if pSessions { + idxMux = &sS.pSIMux + ssIndx = sS.pSessionsIdx + ssRIdx = sS.pSessionsRIdx } idxMux.Lock() defer idxMux.Unlock() @@ -411,15 +815,37 @@ func (smg *SMGeneric) unindexSession(cgrID string, passiveSessions bool) bool { return true } +// getSessionIDsForPrefix works with session relocation returning list of sessions with ID matching prefix for OriginID field +func (sS *SessionS) getSessionIDsForPrefix(prefix string, pSessions bool) (cgrIDs []string) { + idxMux := &sS.aSIMux + ssIndx := sS.aSessionsIdx + if pSessions { + idxMux = &sS.pSIMux + ssIndx = sS.pSessionsIdx + } + idxMux.RLock() + // map[OriginID:map[12372-1:map[*default:511654dc4da7ce4706276cb458437cdd81d0e2b3]]] + for originID := range ssIndx[utils.OriginID] { + if strings.HasPrefix(originID, prefix) { + if _, hasDefaultRun := ssIndx[utils.OriginID][originID][utils.META_DEFAULT]; hasDefaultRun { + cgrIDs = append(cgrIDs, + ssIndx[utils.OriginID][originID][utils.META_DEFAULT].Slice()...) + } + } + } + idxMux.RUnlock() + return +} + // getSessionIDsMatchingIndexes will check inside indexes if it can find sessionIDs matching all filters // matchedIndexes returns map[matchedFieldName]possibleMatchedFieldVal so we optimize further to avoid checking them -func (smg *SMGeneric) getSessionIDsMatchingIndexes(fltrs map[string]string, - passiveSessions bool) (utils.StringMap, map[string]string) { - idxMux := &smg.aSIMux - ssIndx := smg.aSessionsIndex - if passiveSessions { - idxMux = &smg.pSIMux - ssIndx = smg.pSessionsIndex +func (sS *SessionS) getSessionIDsMatchingIndexes(fltrs map[string]string, + pSessions bool) (utils.StringMap, map[string]string) { + idxMux := &sS.aSIMux + ssIndx := sS.aSessionsIdx + if pSessions { + idxMux = &sS.pSIMux + ssIndx = sS.pSessionsIdx } idxMux.RLock() defer idxMux.RUnlock() @@ -482,464 +908,32 @@ func (smg *SMGeneric) getSessionIDsMatchingIndexes(fltrs map[string]string, return matchingSessions.Clone(), matchedIndexes } -// getSessionIDsForPrefix works with session relocation returning list of sessions with ID matching prefix for OriginID field -func (smg *SMGeneric) getSessionIDsForPrefix(prefix string, - passiveSessions bool) (cgrIDs []string) { - idxMux := &smg.aSIMux - ssIndx := smg.aSessionsIndex - if passiveSessions { - idxMux = &smg.pSIMux - ssIndx = smg.pSessionsIndex - } - idxMux.RLock() - // map[OriginID:map[12372-1:map[*default:511654dc4da7ce4706276cb458437cdd81d0e2b3]]] - for originID := range ssIndx[utils.OriginID] { - if strings.HasPrefix(originID, prefix) { - if _, hasDefaultRun := ssIndx[utils.OriginID][originID][utils.META_DEFAULT]; hasDefaultRun { - cgrIDs = append(cgrIDs, - ssIndx[utils.OriginID][originID][utils.META_DEFAULT].Slice()...) - } - } - } - idxMux.RUnlock() - return -} - -// v1ForkSessions is using DerivedChargers for session forking -func (smg *SMGeneric) v1ForkSessions(tnt string, evStart *engine.SafEvent, - clntConn rpcclient.RpcClientConnection, cgrID, resourceID string, - handlePseudo bool) (ss []*SMGSession, err error) { - cdr, err := evStart.AsCDR(smg.cgrCfg, tnt, smg.Timezone) - if err != nil { - utils.Logger.Warning(fmt.Sprintf("<%s> could not convert event: %s to CDR, err: %s", - utils.SessionS, evStart.String(), err.Error())) - return nil, err - } - var sessionRuns []*engine.SessionRun - if err := smg.rals.Call("Responder.GetSessionRuns", - cdr, &sessionRuns); err != nil { - return nil, err - } - noneSession := []*SMGSession{ - {Tenant: tnt, CGRID: cgrID, - ResourceID: resourceID, EventStart: evStart, - RunID: utils.META_NONE, Timezone: smg.Timezone, - rals: smg.rals, cdrsrv: smg.cdrsrv, - clntConn: clntConn}} - handledSessions := []string{utils.META_PREPAID} - if handlePseudo { - handledSessions = append(handledSessions, utils.META_PSEUDOPREPAID) - } - for _, sessionRun := range sessionRuns { - if !utils.IsSliceMember(handledSessions, sessionRun.RequestType) { - continue // not forking non-prepaid session - } - ss = append(ss, - &SMGSession{CGRID: cgrID, Tenant: tnt, - ResourceID: resourceID, EventStart: evStart, - RunID: sessionRun.DerivedCharger.RunID, Timezone: smg.Timezone, - rals: smg.rals, cdrsrv: smg.cdrsrv, - CD: sessionRun.CallDescriptor, clntConn: clntConn, - clientProto: smg.cgrCfg.SessionSCfg().ClientProtocol}) - } - if len(ss) == 0 { // we have no *prepaid session to work with - return noneSession, nil - } - return -} - -// v2ForkSessions is using ChargerS for session forking -func (smg *SMGeneric) v2ForkSessions(tnt string, evStart *engine.SafEvent, - clntConn rpcclient.RpcClientConnection, - cgrID, resourceID string, handlePseudo bool) (ss []*SMGSession, err error) { - cgrEv := &utils.CGREvent{ - Tenant: tnt, - ID: utils.UUIDSha1Prefix(), - Event: evStart.AsMapInterface(), - } - var chrgrs []*engine.ChrgSProcessEventReply - if err := smg.chargerS.Call(utils.ChargerSv1ProcessEvent, cgrEv, &chrgrs); err != nil { - if err.Error() == utils.ErrNotFound.Error() { - return nil, utils.ErrNoActiveSession - } - return nil, err - } - noneSession := []*SMGSession{ - {CGRID: cgrID, ResourceID: resourceID, EventStart: evStart, - RunID: utils.META_NONE, Timezone: smg.Timezone, - rals: smg.rals, cdrsrv: smg.cdrsrv, - clntConn: clntConn}} - handledSessions := []string{utils.META_PREPAID} - if handlePseudo { - handledSessions = append(handledSessions, utils.META_PSEUDOPREPAID) - } - for _, chrgr := range chrgrs { - evStart := engine.NewSafEvent(chrgr.CGREvent.Event) - if !utils.IsSliceMember(handledSessions, evStart.GetStringIgnoreErrors(utils.RequestType)) { - continue // not forking non-prepaid session - } - startTime := evStart.GetTimeIgnoreErrors(utils.AnswerTime, smg.Timezone) - if startTime.IsZero() { // AnswerTime not parsable, try SetupTime - startTime = evStart.GetTimeIgnoreErrors(utils.SetupTime, smg.Timezone) - } - cd := &engine.CallDescriptor{ - CgrID: cgrID, - RunID: evStart.GetStringIgnoreErrors(utils.RunID), - TOR: evStart.GetStringIgnoreErrors(utils.ToR), - Direction: utils.OUT, - Tenant: tnt, - Category: evStart.GetStringIgnoreErrors(utils.Category), - Subject: evStart.GetStringIgnoreErrors(utils.Subject), - Account: evStart.GetStringIgnoreErrors(utils.Account), - Destination: evStart.GetStringIgnoreErrors(utils.Destination), - TimeStart: startTime, - TimeEnd: startTime.Add(evStart.GetDurationIgnoreErrors(utils.Usage)), - ExtraFields: evStart.AsMapStringIgnoreErrors(utils.MainCDRFieldsMap), - } - ss = append(ss, - &SMGSession{CGRID: cgrID, Tenant: tnt, - ResourceID: resourceID, - EventStart: evStart, - RunID: evStart.GetStringIgnoreErrors(utils.RunID), - Timezone: smg.Timezone, - rals: smg.rals, cdrsrv: smg.cdrsrv, - CD: cd, clntConn: clntConn, - clientProto: smg.cgrCfg.SessionSCfg().ClientProtocol}) - } - if len(ss) == 0 { // we have no *prepaid session to work with - return noneSession, nil - } - return -} - -// sessionStart will handle a new session, pass the connectionId so we can communicate on disconnect request -func (smg *SMGeneric) sessionStart(tnt, cgrID string, evStart *engine.SafEvent, - clntConn rpcclient.RpcClientConnection, resourceID string, - dbtItval time.Duration) (err error) { - var ss []*SMGSession - if smg.chargerS == nil { // old way of session forking - ss, err = smg.v1ForkSessions(tnt, evStart, clntConn, cgrID, resourceID, false) - } else { - ss, err = smg.v2ForkSessions(tnt, evStart, clntConn, cgrID, resourceID, false) - } - if err != nil { - return - } - stopDebitChan := make(chan struct{}) - for _, s := range ss { - smg.recordASession(s) - if s.RunID != utils.META_NONE && - dbtItval != 0 { - s.stopDebit = stopDebitChan - go s.debitLoop(dbtItval) - } - } - return -} - -// sessionUpdate will reset terminator, perform debits and replicate sessions -func (smg *SMGeneric) sessionUpdate(tnt, cgrID string, ev *engine.SafEvent, - clnt rpcclient.RpcClientConnection, resourceID string, - dbtItval time.Duration) (maxUsage time.Duration, err error) { - // make sure the session exists, otherwise create - aSessions := smg.getSessions(cgrID, false) - if len(aSessions) == 0 { - if aSessions = smg.passiveToActive(cgrID); len(aSessions) == 0 { - if ev.HasField(utils.InitialOriginID) { - initialCGRID := utils.Sha1( - ev.GetStringIgnoreErrors(utils.InitialOriginID), - ev.GetStringIgnoreErrors(utils.OriginHost)) - err = smg.sessionRelocate(initialCGRID, - cgrID, ev.GetStringIgnoreErrors(utils.OriginID)) - smg.replicateSessionsWithID(initialCGRID, false, smg.smgReplConns) // report changes - } - if !ev.HasField(utils.InitialOriginID) || err == utils.ErrNotFound { // create a new session with this update - err = smg.sessionStart(tnt, cgrID, ev, clnt, resourceID, dbtItval) - } - if err != nil { - return - } - aSessions = smg.getSessions(cgrID, false) // try again to populate after starting above - if len(aSessions) == 0 { - utils.Logger.Err( - fmt.Sprintf("<%s> no active sessions for event: <%s>", - utils.SessionS, cgrID)) - err = rpcclient.ErrSessionNotFound - return - } - } - } - defer smg.replicateSessionsWithID(cgrID, false, smg.smgReplConns) - - var sesTTL, evLastUsed time.Duration - if sesTTL, err = getSessionTTL(ev, smg.cgrCfg.SessionSCfg().SessionTTL, - smg.cgrCfg.SessionSCfg().SessionTTLMaxDelay); err != nil { - return - } - var ttlLastUsed, ttlUsage, lastUsed *time.Duration - if ttlLastUsed, err = ev.GetDurationPtrOrDefault(utils.SessionTTLLastUsed, - smg.cgrCfg.SessionSCfg().SessionTTLLastUsed); err != nil { - return - } - if ttlUsage, err = ev.GetDurationPtrOrDefault(utils.SessionTTLUsage, - smg.cgrCfg.SessionSCfg().SessionTTLUsage); err != nil { - return - } - smg.resetTerminatorTimer(cgrID, sesTTL, ttlLastUsed, ttlUsage) - if evLastUsed, err = ev.GetDuration(utils.LastUsed); err == nil { - lastUsed = &evLastUsed - } else if err != utils.ErrNotFound { - return - } - if maxUsage, err = ev.GetDuration(utils.Usage); err != nil { - if err != utils.ErrNotFound { - return - } - maxUsage = smg.cgrCfg.SessionSCfg().MaxCallDuration - err = nil - } - for _, s := range aSessions[cgrID] { - var maxDur time.Duration - var maxUsageSet bool - if s.RunID == utils.META_NONE { - maxDur = time.Duration(-1) - } else if maxDur, err = s.debit(maxUsage, lastUsed); err != nil { - return - } - if maxDur == time.Duration(-1) && !maxUsageSet { - maxUsage = maxDur - } else if maxDur < maxUsage { - maxUsage = maxDur - } - } - return -} - -// sessionEnd will end a session from outside -func (smg *SMGeneric) sessionEnd(cgrID string, usage time.Duration) (err error) { - ss := smg.getSessions(cgrID, false) - if len(ss) == 0 { - if ss = smg.passiveToActive(cgrID); len(ss) == 0 { - return // ToDo: handle here also debits - } - } - if !smg.unrecordASession(cgrID) { // Unreference it early so we avoid concurrency - return // Did not find the session so no need to close it anymore - } - for idx, s := range ss[cgrID] { - if s.RunID == utils.META_NONE { - continue - } - s.TotalUsage = usage // save final usage as totalUsage - if idx == 0 && s.stopDebit != nil { - close(s.stopDebit) // Stop automatic debits - } - aTime, err := s.EventStart.GetTime(utils.AnswerTime, smg.Timezone) - if err != nil || aTime.IsZero() { - utils.Logger.Warning(fmt.Sprintf("<%s> could not retrieve answer time for session: %s, runId: %s, aTime: %+v, error: %v", - utils.SessionS, cgrID, s.RunID, aTime, err)) - continue // Unanswered session - } - if err := s.close(usage); err != nil { - utils.Logger.Err(fmt.Sprintf("<%s> Could not close session: %s, runId: %s, error: %s", utils.SessionS, cgrID, s.RunID, err.Error())) - } - if err := s.storeSMCost(); err != nil { - utils.Logger.Err(fmt.Sprintf("<%s> Could not save session: %s, runId: %s, error: %s", utils.SessionS, cgrID, s.RunID, err.Error())) - } - } - return -} - -// sessionRelocate is used when an update will relocate an initial session (eg multiple data streams) -func (smg *SMGeneric) sessionRelocate(initialID, cgrID, newOriginID string) (err error) { - if utils.IsSliceMember([]string{initialID, cgrID, newOriginID}, "") { // Not allowed empty params here - return utils.ErrMandatoryIeMissing - } - ssNew := smg.getSessions(cgrID, false) - if len(ssNew) != 0 { // Already relocated - return - } - if pSSNew := smg.getSessions(cgrID, true); len(pSSNew) != 0 { // passive sessions recorded, will be recovered so no need of relocation - return - } - ss := smg.getSessions(initialID, false) - if len(ss) == 0 { // No need of relocation - if ss = smg.passiveToActive(initialID); len(ss) == 0 { - return utils.ErrNotFound - } - } - for i, s := range ss[initialID] { - s.Lock() - s.CGRID = cgrID // Overwrite initial CGRID with new one - s.EventStart.Set(utils.CGRID, cgrID) // Overwrite CGRID for final CDR - s.EventStart.Set(utils.OriginID, newOriginID) // Overwrite OriginID for session indexing - s.Unlock() - smg.recordASession(s) - if i == 0 { - smg.unrecordASession(initialID) - } - } - return -} - -// replicateSessions will replicate session based on configuration -func (smg *SMGeneric) replicateSessionsWithID(cgrID string, passiveSessions bool, smgReplConns []*SMGReplicationConn) (err error) { - if len(smgReplConns) == 0 || - (smg.cgrCfg.SessionSCfg().DebitInterval != 0 && !passiveSessions) { // Replicating active not supported - return - } - ssMux := &smg.aSessionsMux - ssMp := smg.activeSessions // reference it so we don't overwrite the new map without protection - if passiveSessions { - ssMux = &smg.pSessionsMux - ssMp = smg.passiveSessions - } - ssMux.RLock() - ss := ssMp[cgrID] - if len(ss) != 0 { - ss[0].RLock() // lock session so we can clone it after releasing the map lock - } - ssMux.RUnlock() - ssCln := make([]*SMGSession, len(ss)) - for i, s := range ss { - ssCln[i] = s.Clone() - } - if len(ss) != 0 { - ss[0].RUnlock() - } - var wg sync.WaitGroup - for _, rplConn := range smgReplConns { - if rplConn.Synchronous { - wg.Add(1) - } - go func(conn rpcclient.RpcClientConnection, sync bool, ss []*SMGSession) { - var reply string - argSet := ArgsSetPassiveSessions{CGRID: cgrID, Sessions: ss} - conn.Call("SMGenericV1.SetPassiveSessions", argSet, &reply) - if sync { - wg.Done() - } - }(rplConn.Connection, rplConn.Synchronous, ssCln) - } - wg.Wait() // wait for synchronous replication to finish - return -} - -// getSessions is used to return in a thread-safe manner active or passive sessions -func (smg *SMGeneric) getSessions(cgrID string, passiveSessions bool) (aSS map[string][]*SMGSession) { - ssMux := &smg.aSessionsMux - ssMp := smg.activeSessions // reference it so we don't overwrite the new map without protection - if passiveSessions { - ssMux = &smg.pSessionsMux - ssMp = smg.passiveSessions - } - ssMux.RLock() - defer ssMux.RUnlock() - aSS = make(map[string][]*SMGSession) - if len(cgrID) == 0 { - for k, v := range ssMp { - aSS[k] = v // Copy to avoid concurrency on sessions map - } - return - } - if ss, hasCGRID := ssMp[cgrID]; hasCGRID { - aSS[cgrID] = ss - } - return -} - -// setPassiveSession is called when a session is set via RPC in passive sessions table -func (smg *SMGeneric) setPassiveSessions(cgrID string, ss []*SMGSession) (err error) { - if len(ss) == 0 { - return - } - for _, cacheKey := range []string{"InitiateSession" + cgrID, - "UpdateSession" + cgrID, "TerminateSession" + cgrID} { - if _, err := smg.responseCache.Get(cacheKey); err == nil { // Stop processing passive when there has been an update over active RPC - smg.deletePassiveSessions(cgrID) - return ErrActiveSession - } - } - smg.unrecordASession(cgrID) - smg.pSessionsMux.Lock() - smg.passiveSessions[cgrID] = ss - smg.pSessionsMux.Unlock() - for _, s := range ss { - smg.indexSession(s, true) - } - return -} - -// remPassiveSession is called when a session is removed via RPC from passive sessions table -// ToDo: test -func (smg *SMGeneric) removePassiveSessions(cgrID string) (err error) { - for _, cacheKey := range []string{"InitiateSession" + cgrID, "UpdateSession" + cgrID, "TerminateSession" + cgrID} { - if _, err := smg.responseCache.Get(cacheKey); err == nil { // Stop processing passive when there has been an update over active RPC - smg.deletePassiveSessions(cgrID) - return ErrActiveSession - } - } - smg.unrecordASession(cgrID) // just in case there is an active session - smg.deletePassiveSessions(cgrID) - return -} - -// deletePassiveSessions is used to remove a reference from the passiveSessions table -// ToDo: test it -func (smg *SMGeneric) deletePassiveSessions(cgrID string) { - smg.unindexSession(cgrID, true) - smg.pSessionsMux.Lock() - delete(smg.passiveSessions, cgrID) - smg.pSessionsMux.Unlock() -} - -// passiveToActive will transition the sessions from passive to active table -// ToDo: test -func (smg *SMGeneric) passiveToActive(cgrID string) (pSessions map[string][]*SMGSession) { - pSessions = smg.getSessions(cgrID, true) - if len(pSessions) == 0 { - return - } - for _, s := range pSessions[cgrID] { - smg.recordASession(s) - s.rals = smg.rals - s.cdrsrv = smg.cdrsrv - } - smg.deletePassiveSessions(cgrID) - return -} +// ToDo: break the method asActiveSessions to return []*Session +// func (sS *SessionS) filterSessions(fltrs map[string]string, psv bool) (ss []*Session) { // asActiveSessions returns sessions from either active or passive table as []*ActiveSession -func (smg *SMGeneric) asActiveSessions(fltrs map[string]string, count, passiveSessions bool) (aSessions []*ActiveSession, counter int, err error) { - aSessions = make([]*ActiveSession, 0) // Make sure we return at least empty list and not nil +func (sS *SessionS) asActiveSessions(fltrs map[string]string, + count, psv bool) (aSs []*ActiveSession, counter int, err error) { + aSs = make([]*ActiveSession, 0) // Make sure we return at least empty list and not nil // Check first based on indexes so we can downsize the list of matching sessions - matchingSessionIDs, checkedFilters := smg.getSessionIDsMatchingIndexes(fltrs, passiveSessions) + matchingSessionIDs, checkedFilters := sS.getSessionIDsMatchingIndexes(fltrs, pSessions) if len(matchingSessionIDs) == 0 && len(checkedFilters) != 0 { return } for fltrFldName := range fltrs { - if _, alreadyChecked := checkedFilters[fltrFldName]; alreadyChecked && fltrFldName != utils.RunID { // Optimize further checks, RunID should stay since it can create bugs + if _, alreadyChecked := checkedFilters[fltrFldName]; alreadyChecked && + fltrFldName != utils.RunID { // Optimize further checks, RunID should stay since it can create bugs delete(fltrs, fltrFldName) } } - var remainingSessions []*SMGSession // Survived index matching - var ss map[string][]*SMGSession - if passiveSessions { - ss = smg.getSessions(fltrs[utils.CGRID], true) - } else { - ss = smg.getSessions(fltrs[utils.CGRID], false) - } - for cgrID, sGrp := range ss { - if _, hasCGRID := matchingSessionIDs[cgrID]; !hasCGRID && len(checkedFilters) != 0 { - continue - } - for _, s := range sGrp { - remainingSessions = append(remainingSessions, s) - } + var remainingSessions []*Session // Survived index matching + ss := sS.getSessions(fltrs[utils.CGRID], psv) + for cgrID, s := range ss { + remainingSessions = append(remainingSessions, s) } if len(fltrs) != 0 { // Still have some filters to match for i := 0; i < len(remainingSessions); { - if !remainingSessions[i].EventStart.HasField(utils.RunID) { + if !remainingSessions[i].EventStart.HasField(utils.RunID) { // ToDo: try removing dependency on default run remainingSessions[i].EventStart.Set(utils.RunID, utils.META_DEFAULT) } matchingAll := true @@ -960,293 +954,388 @@ func (smg *SMGeneric) asActiveSessions(fltrs map[string]string, count, passiveSe return nil, len(remainingSessions), nil } for _, s := range remainingSessions { - aSessions = append(aSessions, s.AsActiveSession(smg.Timezone)) // Expensive for large number of sessions + aSs = append(aSs, + s.AsActiveSessions(sS.cgrCfg.GeneralCfg().DefaultTimezone, + sS.cgrCfg.GeneralCfg().NodeID)...) // Expensive for large number of sessions } return } -// Methods to apply on sessions, mostly exported through RPC/Bi-RPC +// forkSession will populate SRuns within a Session based on ChargerS output +// forSession can only be called once per Session +// not thread-safe since it should be called in init where there is no concurrency +func (sS *SessionS) forkSession(s *Session) (err error) { + if len(s.SRuns) != 0 { + return errors.New("already forked") + } + cgrEv := &utils.CGREvent{ + Tenant: s.Tenant, + ID: utils.UUIDSha1Prefix(), + Event: evStart.AsMapInterface(), + } + var chrgrs []*engine.ChrgSProcessEventReply + if err := sS.chargerS.Call(utils.ChargerSv1ProcessEvent, + cgrEv, &chrgrs); err != nil { + if err.Error() == utils.ErrNotFound.Error() { + return nil, utils.ErrNoActiveSession + } + return nil, err + } + s.SRuns = make([]*SRun, len(chrgrs)) + for i, chrgr := range chrgrs { + me := engine.NewMapEvent(chrgr.CGREvent.Event) + startTime := me.GetTimeIgnoreErrors(utils.AnswerTime, + sS.cgrCfg.GeneralCfg().DefaultTimezone) + if startTime.IsZero() { // AnswerTime not parsable, try SetupTime + startTime = evStart.GetTimeIgnoreErrors(utils.SetupTime, + sS.cgrCfg.GeneralCfg().DefaultTimezone) + } + s.SRuns[i] = &SRun{ + Event: me, + CD: &engine.CallDescriptor{ + CgrID: s.CGRID, + RunID: me.GetStringIgnoreErrors(utils.RunID), + TOR: me.GetStringIgnoreErrors(utils.ToR), + Direction: utils.OUT, + Tenant: s.Tenant, + Category: me.GetStringIgnoreErrors(utils.Category), + Subject: me.GetStringIgnoreErrors(utils.Subject), + Account: me.GetStringIgnoreErrors(utils.Account), + Destination: me.GetStringIgnoreErrors(utils.Destination), + TimeStart: startTime, + TimeEnd: startTime.Add(evStart.GetDurationIgnoreErrors(utils.Usage)), + ExtraFields: me.AsMapStringIgnoreErrors(utils.NewStringMap(utils.PrimaryCdrFields...)), + }, + } + } + return +} -// MaxUsage calculates maximum usage allowed for given event -func (smg *SMGeneric) GetMaxUsage(tnt string, ev *engine.SafEvent) (maxUsage time.Duration, err error) { - cgrID := GetSetCGRID(ev) - cacheKey := "MaxUsage" + cgrID - if item, err := smg.responseCache.Get(cacheKey); err == nil && item != nil { - return (item.Value.(time.Duration)), item.Err +// getSessions is used to return in a thread-safe manner active or passive sessions +func (sS *SessionS) getSessions(cgrID string, pSessions bool) (ss []*Session) { + ssMux := &sS.aSsMux // get the pointer so we don't copy, otherwise locks will not work + ssMp := sS.aSessions // reference it so we don't overwrite the new map without protection + if pSessions { + ssMux = &sS.pSsMux + ssMp = sS.pSessions } - defer smg.responseCache.Cache(cacheKey, &utils.ResponseCacheItem{Value: maxUsage, Err: err}) - if has := ev.HasField(utils.Usage); !has { // make sure we have a minimum duration configured - ev.Set(utils.Usage, smg.cgrCfg.SessionSCfg().MaxCallDuration) - } - // fork sessions - var ss []*SMGSession - if smg.chargerS == nil { // old way of session forking - ss, err = smg.v1ForkSessions(tnt, ev, nil, cgrID, "", true) - } else { - ss, err = smg.v2ForkSessions(tnt, ev, nil, cgrID, "", true) - } - if err != nil { + ssMux.RLock() + defer ssMux.RUnlock() + if len(cgrID) == 0 { + ss = make([]*Session, len(ssMp)) + var i int + for _, s := range ssMp { + ss[i] = s + } return } - var minUsage *time.Duration // find out the minimum usage - for _, s := range ss { - if s.RunID == utils.META_NONE { - minUsage = utils.DurationPointer(-1) - break - } - var maxDur time.Duration - if err = smg.rals.Call("Responder.GetMaxSessionTime", s.CD, &maxDur); err != nil { - return - } - if minUsage == nil || maxDur < *minUsage { - minUsage = &maxDur - } - } - maxUsage = *minUsage - if maxUsage != time.Duration(-1) && - maxUsage < smg.cgrCfg.SessionSCfg().MinCallDuration { - return 0, errors.New("UNAUTHORIZED_MIN_DURATION") + if s, hasCGRID := ssMp[cgrID]; hasCGRID { + ss = []*Session{s} } return } -// Called on session start -func (smg *SMGeneric) InitiateSession(tnt string, ev *engine.SafEvent, - clnt rpcclient.RpcClientConnection, resourceID string, - dbtItval time.Duration) (maxUsage time.Duration, err error) { - cgrID := GetSetCGRID(ev) - _, err = guardian.Guardian.Guard(func() (iface interface{}, err error) { // Lock it on CGRID level - cacheKey := "InitiateSession" + cgrID - if item, err := smg.responseCache.Get(cacheKey); err == nil && item != nil { - return item.Value.(time.Duration), item.Err - } - defer smg.responseCache.Cache(cacheKey, - &utils.ResponseCacheItem{Value: maxUsage, Err: err}) // schedule response caching - smg.deletePassiveSessions(cgrID) - if err = smg.sessionStart(tnt, cgrID, ev, clnt, resourceID, dbtItval); err != nil { - smg.sessionEnd(cgrID, 0) - return - } - if dbtItval != 0 { // Session handled by debit loop - maxUsage = time.Duration(-1) - return - } - maxUsage, err = smg.sessionUpdate(tnt, cgrID, ev, clnt, resourceID, dbtItval) - if err != nil || maxUsage == 0 { - smg.sessionEnd(cgrID, 0) - } - return - }, smg.cgrCfg.GeneralCfg().LockingTimeout, cgrID) +// transitSState will transit the sessions from one state (active/passive) to another (passive/active) +func (sS *SessionS) transitSState(cgrID string, psv bool) (ss []*Session) { + ss = sS.getSessions(cgrID, !psv) + for _, s := range pSessions { + sS.unregisterSession(s, !psv) + sS.registerSession(s, psv) + // ToDo: activate prepaid debits + } return } -// Execute debits for usage/maxUsage -func (smg *SMGeneric) UpdateSession(tnt string, ev *engine.SafEvent, - clnt rpcclient.RpcClientConnection, resourceID string, - dbtItval time.Duration) (maxUsage time.Duration, err error) { - cgrID := GetSetCGRID(ev) - _, err = guardian.Guardian.Guard(func() (iface interface{}, err error) { // Lock it on CGRID level - cacheKey := "UpdateSession" + cgrID - if item, err := smg.responseCache.Get(cacheKey); err == nil && item != nil { - return item.Value.(time.Duration), item.Err - } - defer smg.responseCache.Cache(cacheKey, - &utils.ResponseCacheItem{Value: maxUsage, Err: err}) - maxUsage, err = smg.sessionUpdate(tnt, cgrID, ev, clnt, resourceID, dbtItval) - if err != nil { - smg.sessionEnd(cgrID, 0) - } - return - }, smg.cgrCfg.GeneralCfg().LockingTimeout, cgrID) +// getActivateSessions returns the sessions from active list or moves from passive +func (sS *SessionS) getActivateSessions(cgrID string) (ss []*Session) { + ss = sS.getSessions(cgrID, false) + if len(aSessions) == 0 { + ss = sS.transitSState(cgrID, false) + } return } -// Called on session end, should stop debit loop -func (smg *SMGeneric) TerminateSession(tnt string, ev *engine.SafEvent, - clnt rpcclient.RpcClientConnection, resourceID string, - dbtItvl time.Duration) (err error) { - cgrID := GetSetCGRID(ev) - _, err = guardian.Guardian.Guard(func() (iface interface{}, err error) { // Lock it on CGRID level - cacheKey := "TerminateSession" + cgrID - if item, err := smg.responseCache.Get(cacheKey); err == nil && item != nil { - return nil, item.Err - } - defer smg.responseCache.Cache(cacheKey, &utils.ResponseCacheItem{Err: err}) - if ev.HasField(utils.InitialOriginID) { - initialCGRID := utils.Sha1( - ev.GetStringIgnoreErrors(utils.InitialOriginID), - ev.GetStringIgnoreErrors(utils.OriginHost)) - err = smg.sessionRelocate(initialCGRID, cgrID, - ev.GetStringIgnoreErrors(utils.OriginID)) - if err == utils.ErrNotFound { // Session was already relocated, create a new session with this update - err = smg.sessionStart(tnt, cgrID, ev, clnt, resourceID, dbtItvl) +// relocateSession will change the CGRID of a session (ie: prefix based session group) +func (sS *SessionS) relocateSessions(initOriginID, originID, originHost string) (ss []*Session) { + if initOriginID == "" { + return + } + initCGRID := utils.Sha1(initOriginID, originHost) + ss = sS.getActivateSessions(initCGRID, false) + for i, s := range ss { + sS.unregisterSession(s.CGRID, false) + s.Lock() + s.CGRID = cgrID + // Overwrite initial CGRID with new one + s.EventStart.Set(utils.CGRID, utils.Sha1(originID, originHost)) // Overwrite CGRID for final CDR + s.EventStart.Set(utils.OriginID, originID) // Overwrite OriginID for session indexing + s.Unlock() + sS.registerSession(s.CGRID, false) + sS.replicateSessions(initCGRID, false, sS.smgReplConns) + } + return +} + +// getRelocateSessions will relocate a session if it cannot find cgrID and initialOriginID is present +func (sS *SessionS) getRelocateSessions(cgrID string, initOriginID, + originID, originHost string) (ss []*Session) { + if ss = sS.getActivateSessions(cgrID); len(ss) != 0 || + initOriginID == "" { + return + } + return sS.relocateSessions(initOriginID, originID, originHost) +} + +// syncSessions synchronizes the active sessions with the one in the clients +// it will force-disconnect the one found in SessionS but not in clients +func (sS *SessionS) syncSessions() { + queriedCGRIDs := engine.NewSafEvent(nil) // need this to be + var err error + for _, clnt := range sS.biJClnts() { + errChan := make(chan error) + go func() { + var queriedSessionIDs []*SessionID + if err := clnt.conn.Call(utils.SessionSv1GetActiveSessionIDs, + utils.EmptyString, &queriedSessionIDs); err != nil { + errChan <- err } - if err != nil && err != utils.ErrMandatoryIeMissing { - return + for _, sessionID := range queriedSessionIDs { + queriedCGRIDs.Set(sessionID.CGRID()) = struct{}{} } - smg.replicateSessionsWithID(initialCGRID, false, smg.smgReplConns) - } - sessionIDs := []string{cgrID} - if ev.HasField(utils.OriginIDPrefix) { // OriginIDPrefix is present, OriginID will not be anymore considered - sessionIDPrefix := ev.GetStringIgnoreErrors(utils.OriginIDPrefix) - if sessionIDs = smg.getSessionIDsForPrefix(sessionIDPrefix, false); len(sessionIDs) == 0 { - sessionIDs = smg.getSessionIDsForPrefix(sessionIDPrefix, true) - for _, sessionID := range sessionIDs { // activate sessions for prefix - smg.passiveToActive(sessionID) - } - } - } - usage, errUsage := ev.GetDuration(utils.Usage) - var lastUsed time.Duration - if errUsage != nil { - if errUsage != utils.ErrNotFound { - err = errUsage - return - } - lastUsed, err = ev.GetDuration(utils.LastUsed) + errChan <- nil + }() + select { + case err = <-errChan: if err != nil { - if err == utils.ErrNotFound { - err = utils.ErrMandatoryIeMissing - } - return + utils.Logger.Warning( + fmt.Sprintf("<%s> error quering session ids : %+v", utils.SessionS, err)) } + case <-time.After(smg.cgrCfg.GeneralCfg().ReplyTimeout): + utils.Logger.Warning( + fmt.Sprintf("<%s> timeout quering session ids ", utils.SessionS)) } - var hasActiveSession bool - for _, sessionID := range sessionIDs { - aSessions := smg.getSessions(sessionID, false) - if len(aSessions) == 0 { - if aSessions = smg.passiveToActive(cgrID); len(aSessions) == 0 { - utils.Logger.Err(fmt.Sprintf("<%s> terminate with no active sessions for cgrID: <%s>", utils.SessionS, cgrID)) - continue - } - } - hasActiveSession = true - defer smg.replicateSessionsWithID(sessionID, false, smg.smgReplConns) - s := aSessions[sessionID][0] - if errUsage != nil { - usage = s.TotalUsage - s.LastUsage + lastUsed - } - if errSEnd := smg.sessionEnd(sessionID, usage); errSEnd != nil { - err = errSEnd // Last error will be the one returned as API result - } + + } + var toBeRemoved []string + sS.aSsMux.RLock() + for cgrid := range sS.aSessions { + if queriedCGRIDs.HasField(cgrid) { + toBeRemoved = append(toBeRemoved, cgrid) } - if !hasActiveSession { - err = rpcclient.ErrSessionNotFound + } + smg.aSsMux.RUnlock() + for _, cgrID := range toBeRemoved { + ss := sS.getSessions(cgrID, false) + if len(aSessions) == 0 { + continue + } + if err := sS.forceSTerminate(ss[0], 0, nil); err != nil { + utils.Logger.Warning(fmt.Sprintf("<%s> failed force-terminating session: <%s>, err: <%s>", utils.SessionS, cgrID)) + } + } +} + +// authSession calculates maximum usage allowed for given session +func (sS *SessionS) authSession(tnt string, evStart *engine.SafEvent) (maxUsage time.Duration, err error) { + s = &Session{ + CGRID: cgrID, + Tenant: tnt, + EventStart: evStart, + } + if err = sS.forkSession(s); err != nil { + return + } + var reqMaxUsage time.Duration + if reqMaxUsage, err = s.EventStart.GetStringIgnoreErrors(utils.Usage); err != nil { + if err != utils.ErrNotFound { return } - return - }, smg.cgrCfg.GeneralCfg().LockingTimeout, cgrID) + reqMaxUsage = sS.cgrCfg.SessionSCfg().MaxCallDuration + err = nil + } + var maxUsageSet bool // so we know if we have set the 0 on purpose + prepaidReqs := []string{utils.META_PREPAID, utils.META_PSEUDOPREPAID} + for _, sr := range s.SRuns { + var rplyMaxUsage time.Duration + if !utils.IsSliceMember(prepaidReqs, + sr.Event.GetStringIgnoreErrors(utils.RequestType)) { + rplyMaxUsage = time.Duration(-1) + } else if err = sS.ralS.Call(utils.ResponderGetMaxSessionTime, + sr.CD, &rplyMaxUsage); err != nil { + return + } + if !maxUsageSet || + maxUsage == time.Duration(-1) || + (rplyMaxUsage < maxUsage && rplyMaxUsage != time.Duration(-1)) { + maxUsage = rplyMaxUsage + maxUsageSet = true + } + } return } -// Processes one time events (eg: SMS) -func (smg *SMGeneric) ChargeEvent(tnt string, ev *engine.SafEvent) (maxUsage time.Duration, err error) { - cgrID := GetSetCGRID(ev) - cacheKey := "ChargeEvent" + cgrID - if item, err := smg.responseCache.Get(cacheKey); err == nil && item != nil { - return item.Value.(time.Duration), item.Err +// initSession handles a new session +func (sS *SessionS) initSession(tnt string, evStart *engine.SafEvent, clntConnID string, + resID string, dbtItval time.Duration) (s *Session, err error) { + cgrID := GetSetCGRID(evStart) + s = &Session{ + CGRID: cgrID, + Tenant: tnt, + ResourceID: resID, + EventStart: evStart, + ClientConnID: clntConnID, + debitStop: make(chan struct{}), } - defer smg.responseCache.Cache(cacheKey, &utils.ResponseCacheItem{Value: maxUsage, Err: err}) - // fork sessions - var ss []*SMGSession - if smg.chargerS == nil { // old way of session forking - ss, err = smg.v1ForkSessions(tnt, ev, nil, cgrID, "", false) - } else { - ss, err = smg.v2ForkSessions(tnt, ev, nil, cgrID, "", false) + if err = sS.forkSession(s); err != nil { + return nil, err } - if err != nil { + for i, sr := range s.SRuns { + if dbtItval != 0 && + sr.Event.GetStringIgnoreErrors(utils.RequestType) == utils.META_PREPAID { + go sS.debitLoopSession(s, i, dbtItval) + } + } + sS.registerASession(s) // make the session available to the rest of the system + return +} + +// updateSession will reset terminator, perform debits and replicate sessions +func (sS *SessionS) updateSession(s *Session) (maxUsage time.Duration, err error) { + defer sS.replicateSessions(s.CGRID) + // update fields from new event + protectedFlds := []string{utils.CGRID, utils.OriginHost, utils.OriginID, utils.Usage} // declare here so we don't sort more than once + for k, v := range ev.AsMapInterface() { + if utils.IsSliceMember(protectedFlds, k) { // reserved field, don't overwrite + continue + } + s.EventStart.Set(k, v) // update previoius field with new one + } + sS.setSTerminator(s) + if evLastUsed, err = ev.GetDuration(utils.LastUsed); err == nil { + lastUsed = &evLastUsed + } else if err != utils.ErrNotFound { return } - // debit each forked session - var maxDur *time.Duration // Avoid differences between default 0 and received 0 - for _, s := range ss { - var durDebit time.Duration - if durDebit, err = s.debit(s.CD.GetDuration(), nil); err != nil { - utils.Logger.Err(fmt.Sprintf("<%s> Could not Debit CD: %+v, RunID: %s, error: %s", - utils.SessionS, s.CD, s.RunID, err.Error())) - break + var reqMaxUsage time.Duration + if reqMaxUsage, err = ev.GetDuration(utils.Usage); err != nil { + if err != utils.ErrNotFound { + return } - if durDebit == 0 { - err = utils.ErrInsufficientCredit - break + reqMaxUsage = sS.cgrCfg.SessionSCfg().MaxCallDuration + err = nil + } + s.RLock() + var maxUsageSet bool // so we know if we have set the 0 on purpose + prepaidReqs := []string{utils.META_PREPAID, utils.META_PSEUDOPREPAID} + for _, sr := range s.SRuns { + var rplyMaxUsage time.Duration + if !utils.IsSliceMember(prepaidReqs, + sr.Event.GetStringIgnoreErrors(utils.RequestType)) { + rplyMaxUsage = time.Duration(-1) + } else if rplyMaxUsage, err = s.debit(reqMaxUsage, + sr.Event.GetDurationPtrIgnoreErrors(utils.LastUsed)); err != nil { + return } - if maxDur == nil || *maxDur > durDebit { - maxDur = utils.DurationPointer(durDebit) + if !maxUsageSet || + maxUsage == time.Duration(-1) || + (rplyMaxUsage < maxUsage && rplyMaxUsage != time.Duration(-1)) { + maxUsage = rplyMaxUsage + maxUsageSet = true } } - if err != nil { // Refund the ones already taken since we have error on one of the debits - for _, s := range ss { - if err := s.close(time.Duration(0)); err != nil { - utils.Logger.Err(fmt.Sprintf("<%s> error: %s closing session with runID: %s", - utils.SessionS, err.Error(), s.RunID)) + s.RUnlock() + return +} + +// endSession will end a session from outside +func (sS *SessionS) endSession(s *Session, tUsage *time.Duration) (err error) { + s.Lock() // no need to release it untill end since the session should be anyway closed + sS.unregisterSession(cgrID, false) + for i, sr := range s.SRuns { + sUsage := s.TotalUsage + if tUsage != nil { + sUsage = *tUsage + s.TotalUsage = *tUsage + } + if s.stopDebit != nil { + close(s.stopDebit) // Stop automatic debits + s.stopDebit = nil + } + if sr.EventCost != nil { + if notCharged := sUsage - sr.EventCost.GetUsage(); notCharged > 0 { // we did not charge enough, make a manual debit here + if sr.CD.LoopIndex > 0 { + sr.CD.TimeStart = sr.CD.TimeEnd + } + sr.CD.TimeEnd = self.CD.TimeStart.Add(notCharged) + sr.CD.DurationIndex += notCharged + cc := new(engine.CallCost) + if err = sS.ralS.Call(utils.ResponderDebit, self.CD, cc); err == nil { + sr.EventCost.Merge( + engine.NewEventCostFromCallCost(cc, sr.CGRID, + sr.Event.GetStringIgnoreErrors(utils.RunID))) + } + } else if notCharged < 0 { // charged too much, try refund + if err = sS.refundSession(s, sRunIdx, -notCharged); err != nil { + utils.Logger.Warning( + fmt.Sprintf( + "<%s> failed refunding session: <%s>, srIdx: <%d>, error: <%s>", + utils.SessionS, cgrID, i, err.Error())) + } } } - return - } - // store session log - for _, s := range ss { - if errStore := s.storeSMCost(); err != nil { - utils.Logger.Err(fmt.Sprintf("<%s> error: %s storing session with runID: %s", - utils.SessionS, errStore.Error(), s.RunID)) - err = ErrPartiallyExecuted + if err := sS.storeSCost(s, i); err != nil { + utils.Logger.Warning( + fmt.Sprintf("<%s> failed storing session cost for <%s>, error: <%s>", + utils.SessionS, cgrID, s.RunID, err.Error())) } } - if err != nil { - return - } - if maxDur != nil { - maxUsage = *maxDur - } + s.Unlock() return } -func (smg *SMGeneric) ProcessCDR(tnt string, ev *engine.SafEvent) (err error) { +// chargeEvent will charge a single event (ie: SMS) +func (sS *SessionS) chargeEvent(tnt string, ev *engine.SafEvent) (maxUsage time.Duration, err error) { cgrID := GetSetCGRID(ev) - cacheKey := "ProcessCDR" + cgrID - if item, err := smg.responseCache.Get(cacheKey); err == nil && item != nil { - return item.Err + var s *Session + if s, err = sS.initSession(tnt, cgrID, ev, "", "", 0); err != nil { + return } - defer smg.responseCache.Cache(cacheKey, &utils.ResponseCacheItem{Err: err}) + if maxUsage, err = sS.updateSession(s); err != nil { + if errEnd := sS.endSession(cgrID, 0); errEnd != nil { + utils.Logger.Warning( + fmt.Sprintf("<%s> error when force-ending charged event: <%s>, err: <%s>", + utils.SessionS, cgrID, err.Error())) + } + return + } + if errEnd := sS.endSession(cgrID, maxUsage); errEnd != nil { + utils.Logger.Warning( + fmt.Sprintf("<%s> error when ending charged event: <%s>, err: <%s>", + utils.SessionS, cgrID, err.Error())) + } + return // returns here the maxUsage from update +} + +func (sS *SessionS) processCDR(tnt string, ev *engine.SafEvent) (err error) { + cgrID := GetSetCGRID(ev) cgrEv := &utils.CGREvent{ Tenant: tnt, ID: utils.UUIDSha1Prefix(), Event: ev.AsMapInterface(), } var reply string - if err = smg.cdrsrv.Call(utils.CdrsV2ProcessCDR, cgrEv, &reply); err != nil { - return - } - return + return sS.cdrS.Call(utils.CdrsV2ProcessCDR, cgrEv, &reply) } -func (smg *SMGeneric) Connect() error { - if smg.cgrCfg.SessionSCfg().ChannelSyncInterval != 0 { - go func() { - for { // Schedule sync channels to run repetately - time.Sleep(smg.cgrCfg.SessionSCfg().ChannelSyncInterval) - smg.syncSessions() - } +// APIs start here - }() - } - return nil +// Call is part of RpcClientConnection interface +func (sS *SessionS) Call(serviceMethod string, args interface{}, reply interface{}) error { + return sS.CallBiRPC(nil, serviceMethod, args, reply) } -// System shutdown -func (smg *SMGeneric) Shutdown() error { - for ssId := range smg.getSessions("", false) { // Force sessions shutdown - smg.sessionEnd(ssId, time.Duration(smg.cgrCfg.MaxCallDuration)) - } - return nil -} - -// RpcClientConnection interface -func (smg *SMGeneric) Call(serviceMethod string, args interface{}, reply interface{}) error { - return smg.CallBiRPC(nil, serviceMethod, args, reply) // Capture the version part out of original call -} - -// Part of utils.BiRPCServer to help internal connections do calls over rpcclient.RpcClientConnection interface -func (smg *SMGeneric) CallBiRPC(clnt rpcclient.RpcClientConnection, +// CallBiRPC is part of utils.BiRPCServer interface to help internal connections do calls over rpcclient.RpcClientConnection interface +func (sS *SessionS) CallBiRPC(clnt rpcclient.RpcClientConnection, serviceMethod string, args interface{}, reply interface{}) error { parts := strings.Split(serviceMethod, ".") if len(parts) != 2 { @@ -1282,308 +1371,118 @@ func (smg *SMGeneric) CallBiRPC(clnt rpcclient.RpcClientConnection, return err } -func (smg *SMGeneric) BiRPCV1GetMaxUsage(clnt rpcclient.RpcClientConnection, - ev engine.MapEvent, maxUsage *float64) error { - maxUsageDur, err := smg.GetMaxUsage( - utils.FirstNonEmpty(ev.GetStringIgnoreErrors(utils.Tenant), - smg.cgrCfg.GeneralCfg().DefaultTenant), - engine.NewSafEvent(ev)) +// BiRPCv1GetActiveSessions returns the list of active sessions based on filter +func (sS *SessionS) BiRPCv1GetActiveSessions(clnt rpcclient.RpcClientConnection, + fltr map[string]string, reply *[]*ActiveSession) error { + for fldName, fldVal := range fltr { + if fldVal == "" { + fltr[fldName] = utils.META_NONE + } + } + aSs, _, err := sS.asActiveSessions(fltr, false, false) if err != nil { return utils.NewErrServerError(err) + } else if len(aSs) == 0 { + return utils.ErrNotFound } - if maxUsageDur == time.Duration(-1) { - *maxUsage = -1.0 + *reply = aSs + return nil +} + +// BiRPCv1GetActiveSessionsCount counts the active sessions +func (sS *SessionS) BiRPCv1GetActiveSessionsCount(clnt rpcclient.RpcClientConnection, + fltr map[string]string, reply *int) error { + for fldName, fldVal := range fltr { + if fldVal == "" { + fltr[fldName] = utils.META_NONE + } + } + if _, count, err := sS.asActiveSessions(fltr, true, false); err != nil { + return err } else { - *maxUsage = maxUsageDur.Seconds() + *reply = count } return nil } -// BiRPCV2GetMaxUsage returns the maximum usage as duration/int64 -func (smg *SMGeneric) BiRPCV2GetMaxUsage(clnt rpcclient.RpcClientConnection, - ev engine.MapEvent, maxUsage *time.Duration) error { - maxUsageDur, err := smg.GetMaxUsage( - utils.FirstNonEmpty(ev.GetStringIgnoreErrors(utils.Tenant), - smg.cgrCfg.GeneralCfg().DefaultTenant), - engine.NewSafEvent(ev)) +// BiRPCv1GetPassiveSessions returns the passive sessions handled by SessionS +func (sS *SessionS) BiRPCv1GetPassiveSessions(clnt rpcclient.RpcClientConnection, + fltr map[string]string, reply *[]*ActiveSession) error { + for fldName, fldVal := range fltr { + if fldVal == "" { + fltr[fldName] = utils.META_NONE + } + } + pSs, _, err := sS.asActiveSessions(fltr, false, true) if err != nil { return utils.NewErrServerError(err) + } else if len(pSs) == 0 { + return utils.ErrNotFound } - *maxUsage = maxUsageDur + *reply = pSs return nil } -// Called on session start, returns the maximum number of seconds the session can last -func (smg *SMGeneric) BiRPCV1InitiateSession(clnt rpcclient.RpcClientConnection, - ev engine.MapEvent, maxUsage *float64) (err error) { - var minMaxUsage time.Duration - tnt := utils.FirstNonEmpty(ev.GetStringIgnoreErrors(utils.Tenant), - smg.cgrCfg.GeneralCfg().DefaultTenant) - if minMaxUsage, err = smg.InitiateSession(tnt, - engine.NewSafEvent(ev), clnt, "", - smg.cgrCfg.SessionSCfg().DebitInterval); err != nil { - if err != rpcclient.ErrSessionNotFound { - err = utils.NewErrServerError(err) +// BiRPCv1GetPassiveSessionsCount counts the passive sessions handled by the system +func (sS *SessionS) BiRPCv1GetPassiveSessionsCount(clnt rpcclient.RpcClientConnection, + fltr map[string]string, reply *int) error { + for fldName, fldVal := range fltr { + if fldVal == "" { + fltr[fldName] = utils.META_NONE } - return } - if minMaxUsage == time.Duration(-1) { - // handle auth for OpenSIPS 2.1 - var authUsage time.Duration - if authUsage, err = smg.GetMaxUsage(tnt, engine.NewSafEvent(ev)); err != nil { - return - } - if authUsage != time.Duration(-1) { - *maxUsage = authUsage.Seconds() - } else { - *maxUsage = smg.cgrCfg.SessionSCfg().MaxCallDuration.Seconds() // Force max since -1 is not supported by OpenSIPS 2.1 - } + if _, count, err := sS.asActiveSessions(fltr, true, true); err != nil { + return err } else { - *maxUsage = minMaxUsage.Seconds() - } - return -} - -// BiRPCV2InitiateSession initiates a new session, returns the maximum duration the session can last -func (smg *SMGeneric) BiRPCV2InitiateSession(clnt rpcclient.RpcClientConnection, - ev engine.MapEvent, maxUsage *time.Duration) (err error) { - var minMaxUsage time.Duration - if minMaxUsage, err = smg.InitiateSession( - utils.FirstNonEmpty(ev.GetStringIgnoreErrors(utils.Tenant), - smg.cgrCfg.GeneralCfg().DefaultTenant), - engine.NewSafEvent(ev), clnt, "", - smg.cgrCfg.SessionSCfg().DebitInterval); err != nil { - if err != rpcclient.ErrSessionNotFound { - err = utils.NewErrServerError(err) - } - return - } else { - *maxUsage = minMaxUsage - } - return -} - -// Interim updates, returns remaining duration from the RALs -func (smg *SMGeneric) BiRPCV1UpdateSession(clnt rpcclient.RpcClientConnection, - ev engine.MapEvent, maxUsage *float64) (err error) { - var minMaxUsage time.Duration - if minMaxUsage, err = smg.UpdateSession( - utils.FirstNonEmpty(ev.GetStringIgnoreErrors(utils.Tenant), - smg.cgrCfg.GeneralCfg().DefaultTenant), - engine.NewSafEvent(ev), clnt, "", - smg.cgrCfg.SessionSCfg().DebitInterval); err != nil { - if err != rpcclient.ErrSessionNotFound { - err = utils.NewErrServerError(err) - } - return - } - if minMaxUsage == time.Duration(-1) { - *maxUsage = -1.0 - } else { - *maxUsage = minMaxUsage.Seconds() - } - return -} - -// BiRPCV1UpdateSession updates an existing session, returning the duration which the session can still last -func (smg *SMGeneric) BiRPCV2UpdateSession(clnt rpcclient.RpcClientConnection, - ev engine.MapEvent, maxUsage *time.Duration) (err error) { - var minMaxUsage time.Duration - if minMaxUsage, err = smg.UpdateSession( - utils.FirstNonEmpty(ev.GetStringIgnoreErrors(utils.Tenant), - smg.cgrCfg.GeneralCfg().DefaultTenant), - engine.NewSafEvent(ev), clnt, "", - smg.cgrCfg.SessionSCfg().DebitInterval); err != nil { - if err != rpcclient.ErrSessionNotFound { - err = utils.NewErrServerError(err) - } - } else { - *maxUsage = minMaxUsage - } - return -} - -// Called on session end, should stop debit loop -func (smg *SMGeneric) BiRPCV1TerminateSession(clnt rpcclient.RpcClientConnection, - ev engine.MapEvent, reply *string) (err error) { - if err = smg.TerminateSession( - utils.FirstNonEmpty(ev.GetStringIgnoreErrors(utils.Tenant), - smg.cgrCfg.GeneralCfg().DefaultTenant), - engine.NewSafEvent(ev), clnt, "", - smg.cgrCfg.SessionSCfg().DebitInterval); err != nil { - if err != rpcclient.ErrSessionNotFound { - err = utils.NewErrServerError(err) - } - } else { - *reply = utils.OK - } - return -} - -// Called on individual Events (eg SMS) -func (smg *SMGeneric) BiRPCV1ChargeEvent(clnt rpcclient.RpcClientConnection, - ev engine.MapEvent, maxUsage *float64) error { - if minMaxUsage, err := smg.ChargeEvent( - utils.FirstNonEmpty(ev.GetStringIgnoreErrors(utils.Tenant), - smg.cgrCfg.GeneralCfg().DefaultTenant), - engine.NewSafEvent(ev)); err != nil { - return utils.NewErrServerError(err) - } else { - *maxUsage = minMaxUsage.Seconds() + *reply = count } return nil } -// Called on individual Events (eg SMS) -func (smg *SMGeneric) BiRPCV2ChargeEvent(clnt rpcclient.RpcClientConnection, - ev engine.MapEvent, maxUsage *time.Duration) error { - if minMaxUsage, err := smg.ChargeEvent( - utils.FirstNonEmpty(ev.GetStringIgnoreErrors(utils.Tenant), - smg.cgrCfg.GeneralCfg().DefaultTenant), - engine.NewSafEvent(ev)); err != nil { - return utils.NewErrServerError(err) - } else { - *maxUsage = minMaxUsage +// BiRPCv1SetPassiveSessions used for replicating Sessions +func (sS *SessionS) BiRPCv1SetPassiveSessions(clnt rpcclient.RpcClientConnection, + s *Session, reply *string) (err error) { + if s.CGRID == "" { + return utils.NewErrMandatoryIeMissing([]string{utils.CGRID}) } - return nil -} - -// Called on session end, should send the CDR to CDRS -func (smg *SMGeneric) BiRPCV1ProcessCDR(clnt rpcclient.RpcClientConnection, - ev engine.MapEvent, reply *string) error { - if err := smg.ProcessCDR( - utils.FirstNonEmpty(ev.GetStringIgnoreErrors(utils.Tenant), - smg.cgrCfg.GeneralCfg().DefaultTenant), - engine.NewSafEvent(ev)); err != nil { - return utils.NewErrServerError(err) + if s.EventStart == nil { // remove instead of + if removed := sS.unregisterSession(s.CGRID, true); !removed { + return utils.ErrServerError + } + } else { + sS.registerSession(s, true) } *reply = utils.OK - return nil -} - -func (smg *SMGeneric) BiRPCV1GetActiveSessions(clnt rpcclient.RpcClientConnection, - fltr map[string]string, reply *[]*ActiveSession) error { - for fldName, fldVal := range fltr { - if fldVal == "" { - fltr[fldName] = utils.META_NONE - } - } - aSessions, _, err := smg.asActiveSessions(fltr, false, false) - if err != nil { - return utils.NewErrServerError(err) - } else if len(aSessions) == 0 { - return utils.ErrNotFound - } - *reply = aSessions - return nil -} - -func (smg *SMGeneric) BiRPCV1GetActiveSessionsCount(clnt rpcclient.RpcClientConnection, - fltr map[string]string, reply *int) error { - for fldName, fldVal := range fltr { - if fldVal == "" { - fltr[fldName] = utils.META_NONE - } - } - if _, count, err := smg.asActiveSessions(fltr, true, false); err != nil { - return err - } else { - *reply = count - } - return nil -} - -func (smg *SMGeneric) BiRPCV1GetPassiveSessions(clnt rpcclient.RpcClientConnection, - fltr map[string]string, reply *[]*ActiveSession) error { - for fldName, fldVal := range fltr { - if fldVal == "" { - fltr[fldName] = utils.META_NONE - } - } - aSessions, _, err := smg.asActiveSessions(fltr, false, true) - if err != nil { - return utils.NewErrServerError(err) - } else if len(aSessions) == 0 { - return utils.ErrNotFound - } - *reply = aSessions - return nil -} - -func (smg *SMGeneric) BiRPCV1GetPassiveSessionsCount(clnt rpcclient.RpcClientConnection, - fltr map[string]string, reply *int) error { - for fldName, fldVal := range fltr { - if fldVal == "" { - fltr[fldName] = utils.META_NONE - } - } - if _, count, err := smg.asActiveSessions(fltr, true, true); err != nil { - return err - } else { - *reply = count - } - return nil -} - -type ArgsSetPassiveSessions struct { - CGRID string - Sessions []*SMGSession -} - -// BiRPCV1SetPassiveSession used for replicating SMGSessions -func (smg *SMGeneric) BiRPCV1SetPassiveSessions(clnt rpcclient.RpcClientConnection, - args ArgsSetPassiveSessions, reply *string) (err error) { - if len(args.Sessions) == 0 { - err = smg.removePassiveSessions(args.CGRID) - } else { - err = smg.setPassiveSessions(args.CGRID, args.Sessions) - } - if err == nil { - *reply = utils.OK - } return } type ArgsReplicateSessions struct { - Filter map[string]string + CGRID string + Passive bool Connections []*config.HaPoolConfig } -// BiRPCV1ReplicateActiveSessions will replicate active sessions to either args.Connections or the internal configured ones +// BiRPCv1ReplicateSessions will replicate active sessions to either args.Connections or the internal configured ones // args.Filter is used to filter the sessions which are replicated, CGRID is the only one possible for now -func (smg *SMGeneric) BiRPCV1ReplicateActiveSessions(clnt rpcclient.RpcClientConnection, +func (sS *SessionS) BiRPCv1ReplicateSessions(clnt rpcclient.RpcClientConnection, args ArgsReplicateSessions, reply *string) (err error) { - smgConns := smg.smgReplConns + cacheKey := "BiRPCv1ReplicateSessions" + args.CGRID + if item, err := sS.responseCache.Get(cacheKey); err == nil && item != nil { + return (item.Value.(*string)), item.Err + } + defer sS.responseCache.Cache(cacheKey, + &utils.ResponseCacheItem{Value: reply, Err: err}) + sSConns := sS.sReplConns if len(args.Connections) != 0 { - if smgConns, err = NewSessionReplicationConns(args.Connections, - smg.cgrCfg.GeneralCfg().Reconnects, smg.cgrCfg.GeneralCfg().ConnectTimeout, + if sSConns, err = NewSReplConns(args.Connections, + smg.cgrCfg.GeneralCfg().Reconnects, + smg.cgrCfg.GeneralCfg().ConnectTimeout, smg.cgrCfg.GeneralCfg().ReplyTimeout); err != nil { - return + return utils.NewErrServerError(err) } } - aSs := smg.getSessions(args.Filter[utils.CGRID], false) - for cgrID := range aSs { - smg.replicateSessionsWithID(cgrID, false, smgConns) - } - *reply = utils.OK - return -} - -// BiRPCV1ReplicatePassiveSessions will replicate passive sessions to either args.Connections or the internal configured ones -// args.Filter is used to filter the sessions which are replicated, CGRID is the only one possible for now -func (smg *SMGeneric) BiRPCV1ReplicatePassiveSessions(clnt rpcclient.RpcClientConnection, - args ArgsReplicateSessions, reply *string) (err error) { - smgConns := smg.smgReplConns - if len(args.Connections) != 0 { - if smgConns, err = NewSessionReplicationConns(args.Connections, - smg.cgrCfg.GeneralCfg().Reconnects, smg.cgrCfg.GeneralCfg().ConnectTimeout, - smg.cgrCfg.GeneralCfg().ReplyTimeout); err != nil { - return - } - } - aSs := smg.getSessions(args.Filter[utils.CGRID], true) - for cgrID := range aSs { - smg.replicateSessionsWithID(cgrID, true, smgConns) + if err = sS.replicateSessions(cgrID, args.Passive, sSConns); err != nil { + return utils.NewErrServerError(err) } *reply = utils.OK return @@ -1609,6 +1508,7 @@ func NewV1AuthorizeArgs(attrs, res, maxUsage, thrslds, return } +// V1AuthorizeArgs are options available in auth request type V1AuthorizeArgs struct { GetAttributes bool AuthorizeResources bool @@ -1622,6 +1522,7 @@ type V1AuthorizeArgs struct { utils.Paginator } +// V1AuthorizeReply are options available in auth reply type V1AuthorizeReply struct { Attributes *engine.AttrSProcessEventReply ResourceAllocation *string @@ -1664,9 +1565,19 @@ func (v1AuthReply *V1AuthorizeReply) AsNavigableMap( return config.NewNavigableMap(cgrReply), nil } -// BiRPCV1Authorize performs authorization for CGREvent based on specific components -func (smg *SMGeneric) BiRPCv1AuthorizeEvent(clnt rpcclient.RpcClientConnection, +// BiRPCv1AuthorizeEvent performs authorization for CGREvent based on specific components +func (sS *SessionS) BiRPCv1AuthorizeEvent(clnt rpcclient.RpcClientConnection, args *V1AuthorizeArgs, authReply *V1AuthorizeReply) (err error) { + if args.CGREvent.ID == "" { + args.CGREvent.ID = utils.GenUUID() + } + cacheKey := "BiRPCv1AuthorizeEventWithDigest" + args.CGREvent.ID + if item, err := sS.respCache.Get(cacheKey); err == nil && item != nil { + return (item.Value.(*V1AuthorizeReplyWithDigest)), item.Err + } + defer sS.respCache.Cache(cacheKey, + &utils.ResponseCacheItem{Value: authReply, Err: err}) + if !args.GetAttributes && !args.AuthorizeResources && !args.GetMaxUsage && !args.GetSuppliers { return utils.NewErrMandatoryIeMissing("subsystems") @@ -1674,21 +1585,18 @@ func (smg *SMGeneric) BiRPCv1AuthorizeEvent(clnt rpcclient.RpcClientConnection, if args.CGREvent.Tenant == "" { args.CGREvent.Tenant = smg.cgrCfg.GeneralCfg().DefaultTenant } - if args.CGREvent.ID == "" { - args.CGREvent.ID = utils.GenUUID() - } if args.GetAttributes { - if smg.attrS == nil { + if sS.attrS == nil { return utils.NewErrNotConnected(utils.AttributeS) } if args.CGREvent.Context == nil { // populate if not already in - args.CGREvent.Context = utils.StringPointer(utils.MetaSessionS) + args.CGREvent.Context = utils.StringPointer(utils.MetaSessions) } attrArgs := &engine.AttrArgsProcessEvent{ CGREvent: args.CGREvent, } var rplyEv engine.AttrSProcessEventReply - if err := smg.attrS.Call(utils.AttributeSv1ProcessEvent, + if err := sS.attrS.Call(utils.AttributeSv1ProcessEvent, attrArgs, &rplyEv); err == nil { args.CGREvent = *rplyEv.CGREvent authReply.Attributes = &rplyEv @@ -1697,10 +1605,7 @@ func (smg *SMGeneric) BiRPCv1AuthorizeEvent(clnt rpcclient.RpcClientConnection, } } if args.GetMaxUsage { - if smg.rals == nil { - return utils.NewErrNotConnected(utils.RALService) - } - maxUsage, err := smg.GetMaxUsage(args.CGREvent.Tenant, + maxUsage, err := sS.authSession(args.CGREvent.Tenant, engine.NewSafEvent(args.CGREvent.Event)) if err != nil { return utils.NewErrRALs(err) @@ -1708,7 +1613,7 @@ func (smg *SMGeneric) BiRPCv1AuthorizeEvent(clnt rpcclient.RpcClientConnection, authReply.MaxUsage = &maxUsage } if args.AuthorizeResources { - if smg.resS == nil { + if sS.resS == nil { return utils.NewErrNotConnected(utils.ResourceS) } originID, _ := args.CGREvent.FieldAsString(utils.OriginID) @@ -1721,14 +1626,14 @@ func (smg *SMGeneric) BiRPCv1AuthorizeEvent(clnt rpcclient.RpcClientConnection, UsageID: originID, Units: 1, } - if err = smg.resS.Call(utils.ResourceSv1AuthorizeResources, + if err = sS.resS.Call(utils.ResourceSv1AuthorizeResources, attrRU, &allocMsg); err != nil { return utils.NewErrResourceS(err) } authReply.ResourceAllocation = &allocMsg } if args.GetSuppliers { - if smg.splS == nil { + if sS.splS == nil { return utils.NewErrNotConnected(utils.SupplierS) } cgrEv := args.CGREvent.Clone() @@ -1742,7 +1647,7 @@ func (smg *SMGeneric) BiRPCv1AuthorizeEvent(clnt rpcclient.RpcClientConnection, CGREvent: *cgrEv, Paginator: args.Paginator, } - if err = smg.splS.Call(utils.SupplierSv1GetSuppliers, + if err = sS.splS.Call(utils.SupplierSv1GetSuppliers, sArgs, &splsReply); err != nil { return utils.NewErrSupplierS(err) } @@ -1750,36 +1655,40 @@ func (smg *SMGeneric) BiRPCv1AuthorizeEvent(clnt rpcclient.RpcClientConnection, authReply.Suppliers = &splsReply } } - if smg.thdS != nil && args.ProcessThresholds { - if smg.thdS == nil { + if args.ProcessThresholds { + if sS.thdS == nil { return utils.NewErrNotConnected(utils.ThresholdS) } var tIDs []string thEv := &engine.ArgsProcessEvent{ CGREvent: args.CGREvent, } - if err := smg.thdS.Call(utils.ThresholdSv1ProcessEvent, thEv, &tIDs); err != nil && + if err := sS.thdS.Call(utils.ThresholdSv1ProcessEvent, thEv, &tIDs); err != nil && err.Error() != utils.ErrNotFound.Error() { utils.Logger.Warning( - fmt.Sprintf(" error: %s processing event %+v with ThresholdS.", err.Error(), thEv)) + fmt.Sprintf("<%s> error: %s processing event %+v with ThresholdS.", + utils.SessionS, err.Error(), thEv)) } authReply.ThresholdIDs = &tIDs } - if smg.statS != nil && args.ProcessStats { - if smg.statS == nil { + if args.ProcessStats { + if sS.statS == nil { return utils.NewErrNotConnected(utils.StatService) } var statReply []string - if err := smg.statS.Call(utils.StatSv1ProcessEvent, &engine.StatsArgsProcessEvent{CGREvent: args.CGREvent}, &statReply); err != nil && + if err := smg.statS.Call(utils.StatSv1ProcessEvent, + &engine.StatsArgsProcessEvent{CGREvent: args.CGREvent}, &statReply); err != nil && err.Error() != utils.ErrNotFound.Error() { utils.Logger.Warning( - fmt.Sprintf(" error: %s processing event %+v with StatS.", err.Error(), args.CGREvent)) + fmt.Sprintf("<%s> error: %s processing event %+v with StatS.", + utils.SessionS, err.Error(), args.CGREvent)) } authReply.StatQueueIDs = &statReply } return nil } +// V1AuthorizeReplyWithDigest contains return options for auth with digest type V1AuthorizeReplyWithDigest struct { AttributesDigest *string ResourceAllocation *string @@ -1791,14 +1700,10 @@ type V1AuthorizeReplyWithDigest struct { // BiRPCv1AuthorizeEventWithDigest performs authorization for CGREvent based on specific components // returning one level fields instead of multiple ones returned by BiRPCv1AuthorizeEvent -func (smg *SMGeneric) BiRPCv1AuthorizeEventWithDigest(clnt rpcclient.RpcClientConnection, +func (sS *SessionS) BiRPCv1AuthorizeEventWithDigest(clnt rpcclient.RpcClientConnection, args *V1AuthorizeArgs, authReply *V1AuthorizeReplyWithDigest) (err error) { - if !args.GetAttributes && !args.AuthorizeResources && - !args.GetMaxUsage && !args.GetSuppliers { - return utils.NewErrMandatoryIeMissing("subsystems") - } var initAuthRply V1AuthorizeReply - if err = smg.BiRPCv1AuthorizeEvent(clnt, args, &initAuthRply); err != nil { + if err = sS.BiRPCv1AuthorizeEvent(clnt, args, &initAuthRply); err != nil { return } if args.GetAttributes && initAuthRply.Attributes != nil { @@ -1827,6 +1732,7 @@ func (smg *SMGeneric) BiRPCv1AuthorizeEventWithDigest(clnt rpcclient.RpcClientCo return nil } +// NewV1InitSessionArgs is a constructor for V1InitSessionArgs func NewV1InitSessionArgs(attrs, resrc, acnt, thrslds, stats bool, cgrEv utils.CGREvent) *V1InitSessionArgs { return &V1InitSessionArgs{ @@ -1839,6 +1745,7 @@ func NewV1InitSessionArgs(attrs, resrc, acnt, thrslds, stats bool, } } +// V1InitSessionArgs are options for session initialization request type V1InitSessionArgs struct { GetAttributes bool AllocateResources bool @@ -1848,6 +1755,7 @@ type V1InitSessionArgs struct { utils.CGREvent } +// V1InitSessionReply are options for initialization reply type V1InitSessionReply struct { Attributes *engine.AttrSProcessEventReply ResourceAllocation *string @@ -1886,30 +1794,38 @@ func (v1Rply *V1InitSessionReply) AsNavigableMap( return config.NewNavigableMap(cgrReply), nil } -// BiRPCV2InitiateSession initiates a new session, returns the maximum duration the session can last -func (smg *SMGeneric) BiRPCv1InitiateSession(clnt rpcclient.RpcClientConnection, +// BiRPCv1InitiateSession initiates a new session +func (sS *SessionS) BiRPCv1InitiateSession(clnt rpcclient.RpcClientConnection, args *V1InitSessionArgs, rply *V1InitSessionReply) (err error) { + if args.CGREvent.ID == "" { + args.CGREvent.ID = utils.GenUUID() + } + cacheKey := "BiRPCv1InitiateSession" + args.CGREvent.ID + if item, err := sS.respCache.Get(cacheKey); err == nil && item != nil { + return (item.Value.(*V1InitSessionReply)), item.Err + } + defer sS.respCache.Cache(cacheKey, + &utils.ResponseCacheItem{Value: rply, Err: err}) + if !args.GetAttributes && !args.AllocateResources && !args.InitSession { return utils.NewErrMandatoryIeMissing("subsystems") } if args.CGREvent.Tenant == "" { args.CGREvent.Tenant = smg.cgrCfg.GeneralCfg().DefaultTenant } - if args.CGREvent.ID == "" { - args.CGREvent.ID = utils.GenUUID() - } + originID, _ := args.CGREvent.FieldAsString(utils.OriginID) if args.GetAttributes { - if smg.attrS == nil { + if sS.attrS == nil { return utils.NewErrNotConnected(utils.AttributeS) } if args.CGREvent.Context == nil { // populate if not already in - args.CGREvent.Context = utils.StringPointer(utils.MetaSessionS) + args.CGREvent.Context = utils.StringPointer(utils.MetaSessions) } attrArgs := &engine.AttrArgsProcessEvent{ CGREvent: args.CGREvent, } var rplyEv engine.AttrSProcessEventReply - if err := smg.attrS.Call(utils.AttributeSv1ProcessEvent, + if err := sS.attrS.Call(utils.AttributeSv1ProcessEvent, attrArgs, &rplyEv); err == nil { args.CGREvent = *rplyEv.CGREvent rply.Attributes = &rplyEv @@ -1918,11 +1834,10 @@ func (smg *SMGeneric) BiRPCv1InitiateSession(clnt rpcclient.RpcClientConnection, } } if args.AllocateResources { - if smg.resS == nil { + if sS.resS == nil { return utils.NewErrNotConnected(utils.ResourceS) } - originID, err := args.CGREvent.FieldAsString(utils.OriginID) - if err != nil { + if originID == "" { return utils.NewErrMandatoryIeMissing(utils.OriginID) } attrRU := utils.ArgRSv1ResourceUsage{ @@ -1931,51 +1846,46 @@ func (smg *SMGeneric) BiRPCv1InitiateSession(clnt rpcclient.RpcClientConnection, Units: 1, } var allocMessage string - if err = smg.resS.Call(utils.ResourceSv1AllocateResources, + if err = sS.resS.Call(utils.ResourceSv1AllocateResources, attrRU, &allocMessage); err != nil { return utils.NewErrResourceS(err) } rply.ResourceAllocation = &allocMessage } if args.InitSession { - if smg.rals == nil { - return utils.NewErrNotConnected(utils.RALService) - } var err error - originID := "" - if args.AllocateResources { - originID, err = args.CGREvent.FieldAsString(utils.OriginID) - if err != nil { - return utils.NewErrMandatoryIeMissing(utils.OriginID) - } - } ev := engine.NewSafEvent(args.CGREvent.Event) - dbtItvl := smg.cgrCfg.SessionSCfg().DebitInterval + dbtItvl := sS.cgrCfg.SessionSCfg().DebitInterval if ev.HasField(utils.CGRDebitInterval) { // dynamic DebitInterval via CGRDebitInterval if dbtItvl, err = ev.GetDuration(utils.CGRDebitInterval); err != nil { return utils.NewErrRALs(err) } } - if maxUsage, err := smg.InitiateSession( - args.CGREvent.Tenant, - ev, clnt, originID, dbtItvl); err != nil { + s, err := sS.initSession(args.CGREvent.Tenant, ev, + sS.biJClntID(clnt), originID, dbtItvl) + if err != nil { + return utils.NewErrRALs(err) + } + if maxUsage, err := sS.updateSession(s); err != nil { return utils.NewErrRALs(err) } else { rply.MaxUsage = &maxUsage } } if args.ProcessThresholds { - if smg.thdS == nil { + if sS.thdS == nil { return utils.NewErrNotConnected(utils.ThresholdS) } var tIDs []string thEv := &engine.ArgsProcessEvent{ CGREvent: args.CGREvent, } - if err := smg.thdS.Call(utils.ThresholdSv1ProcessEvent, thEv, &tIDs); err != nil && + if err := sS.thdS.Call(utils.ThresholdSv1ProcessEvent, + thEv, &tIDs); err != nil && err.Error() != utils.ErrNotFound.Error() { utils.Logger.Warning( - fmt.Sprintf(" error: %s processing event %+v with ThresholdS.", err.Error(), thEv)) + fmt.Sprintf("<%s> error: %s processing event %+v with ThresholdS.", + utils.SessionS, err.Error(), thEv)) } rply.ThresholdIDs = &tIDs } @@ -1984,10 +1894,13 @@ func (smg *SMGeneric) BiRPCv1InitiateSession(clnt rpcclient.RpcClientConnection, return utils.NewErrNotConnected(utils.StatService) } var statReply []string - if err := smg.statS.Call(utils.StatSv1ProcessEvent, &engine.StatsArgsProcessEvent{CGREvent: args.CGREvent}, &statReply); err != nil && + if err := sS.statS.Call(utils.StatSv1ProcessEvent, + &engine.StatsArgsProcessEvent{CGREvent: args.CGREvent}, + &statReply); err != nil && err.Error() != utils.ErrNotFound.Error() { utils.Logger.Warning( - fmt.Sprintf(" error: %s processing event %+v with StatS.", err.Error(), args.CGREvent)) + fmt.Sprintf("<%s> error: %s processing event %+v with StatS.", + utils.SessionS, err.Error(), args.CGREvent)) } rply.StatQueueIDs = &statReply } @@ -2002,18 +1915,15 @@ type V1InitReplyWithDigest struct { StatQueues *string } -func (smg *SMGeneric) BiRPCv1InitiateSessionWithDigest(clnt rpcclient.RpcClientConnection, +func (sS *SessionS) BiRPCv1InitiateSessionWithDigest(clnt rpcclient.RpcClientConnection, args *V1InitSessionArgs, initReply *V1InitReplyWithDigest) (err error) { - if !args.GetAttributes && !args.AllocateResources && - !args.InitSession { - return utils.NewErrMandatoryIeMissing("subsystems") - } var initSessionRply V1InitSessionReply - if err = smg.BiRPCv1InitiateSession(clnt, args, &initSessionRply); err != nil { + if err = sS.BiRPCv1InitiateSession(clnt, args, &initSessionRply); err != nil { return } - if args.GetAttributes && initSessionRply.Attributes != nil { + if args.GetAttributes && + initSessionRply.Attributes != nil { initReply.AttributesDigest = utils.StringPointer(initSessionRply.Attributes.Digest()) } @@ -2039,18 +1949,21 @@ func (smg *SMGeneric) BiRPCv1InitiateSessionWithDigest(clnt rpcclient.RpcClientC return nil } +// NewV1UpdateSessionArgs is a constructor for update session arguments func NewV1UpdateSessionArgs(attrs, acnts bool, cgrEv utils.CGREvent) *V1UpdateSessionArgs { return &V1UpdateSessionArgs{GetAttributes: attrs, UpdateSession: acnts, CGREvent: cgrEv} } +// V1UpdateSessionArgs contains options for session update type V1UpdateSessionArgs struct { GetAttributes bool UpdateSession bool utils.CGREvent } +// V1UpdateSessionReply contains options for session update reply type V1UpdateSessionReply struct { Attributes *engine.AttrSProcessEventReply MaxUsage *time.Duration @@ -2077,30 +1990,37 @@ func (v1Rply *V1UpdateSessionReply) AsNavigableMap( return config.NewNavigableMap(cgrReply), nil } -// BiRPCV1UpdateSession updates an existing session, returning the duration which the session can still last -func (smg *SMGeneric) BiRPCv1UpdateSession(clnt rpcclient.RpcClientConnection, +// BiRPCv1UpdateSession updates an existing session, returning the duration which the session can still last +func (sS *SessionS) BiRPCv1UpdateSession(clnt rpcclient.RpcClientConnection, args *V1UpdateSessionArgs, rply *V1UpdateSessionReply) (err error) { + if args.CGREvent.ID == "" { + args.CGREvent.ID = utils.GenUUID() + } + cacheKey := "BiRPCv1UpdateSession" + args.CGREvent.ID + if item, err := sS.respCache.Get(cacheKey); err == nil && item != nil { + return (item.Value.(*V1UpdateSessionReply)), item.Err + } + defer sS.respCache.Cache(cacheKey, + &utils.ResponseCacheItem{Value: rply, Err: err}) + if !args.GetAttributes && !args.UpdateSession { return utils.NewErrMandatoryIeMissing("subsystems") } if args.CGREvent.Tenant == "" { args.CGREvent.Tenant = smg.cgrCfg.GeneralCfg().DefaultTenant } - if args.CGREvent.ID == "" { - args.CGREvent.ID = utils.GenUUID() - } if args.GetAttributes { - if smg.attrS == nil { + if sS.attrS == nil { return utils.NewErrNotConnected(utils.AttributeS) } if args.CGREvent.Context == nil { // populate if not already in - args.CGREvent.Context = utils.StringPointer(utils.MetaSessionS) + args.CGREvent.Context = utils.StringPointer(utils.MetaSessions) } attrArgs := &engine.AttrArgsProcessEvent{ CGREvent: args.CGREvent, } var rplyEv engine.AttrSProcessEventReply - if err := smg.attrS.Call(utils.AttributeSv1ProcessEvent, + if err := sS.attrS.Call(utils.AttributeSv1ProcessEvent, attrArgs, &rplyEv); err == nil { args.CGREvent = *rplyEv.CGREvent rply.Attributes = &rplyEv @@ -2109,22 +2029,30 @@ func (smg *SMGeneric) BiRPCv1UpdateSession(clnt rpcclient.RpcClientConnection, } } if args.UpdateSession { - if smg.rals == nil { - return utils.NewErrNotConnected(utils.RALService) - } - originID, err := args.CGREvent.FieldAsString(utils.OriginID) - if err != nil { - return utils.NewErrMandatoryIeMissing(utils.OriginID) - } - ev := engine.NewSafEvent(args.CGREvent.Event) - dbtItvl := smg.cgrCfg.SessionSCfg().DebitInterval - if ev.HasField(utils.CGRDebitInterval) { // dynamic DebitInterval via CGRDebitInterval - if dbtItvl, err = ev.GetDuration(utils.CGRDebitInterval); err != nil { + me := engine.NewMapEvent(args.CGREvent.Event) + dbtItvl := sS.cgrCfg.SessionSCfg().DebitInterval + if me.HasField(utils.CGRDebitInterval) { // dynamic DebitInterval via CGRDebitInterval + if dbtItvl, err = me.GetDuration(utils.CGRDebitInterval); err != nil { return utils.NewErrRALs(err) } } - if maxUsage, err := smg.UpdateSession(args.CGREvent.Tenant, - ev, clnt, originID, dbtItvl); err != nil { + ev := engine.NewSafEvent(args.CGREvent.Event) + cgrID := GetSetCGRID(ev) + ss = sS.getRelocateSessions(cgrID, + me.GetStringIgnoreErrors(utils.InitialOriginID), + me.GetStringIgnoreErrors(utils.OriginID), + me.GetStringIgnoreErrors(utils.OriginHost)) + var s *Session + if len(ss) == 0 { + if s, err = sS.initSession(args.CGREvent.Tenant, + ev, sS.biJClntID(clnt), + me.GetStringIgnoreErrors(utils.OriginID), dbtItvl); err != nil { + return utils.NewErrRALs(err) + } + } else { + s = ss[0] + } + if maxUsage, err := sS.updateSession(s); err != nil { return utils.NewErrRALs(err) } else { rply.MaxUsage = &maxUsage @@ -2152,23 +2080,28 @@ type V1TerminateSessionArgs struct { } // BiRPCV1TerminateSession will stop debit loops as well as release any used resources -func (smg *SMGeneric) BiRPCv1TerminateSession(clnt rpcclient.RpcClientConnection, +func (sS *SessionS) BiRPCv1TerminateSession(clnt rpcclient.RpcClientConnection, args *V1TerminateSessionArgs, rply *string) (err error) { + if args.CGREvent.ID == "" { + args.CGREvent.ID = utils.GenUUID() + } + cacheKey := "BiRPCv1TerminateSession" + args.CGREvent.ID + if item, err := sS.respCache.Get(cacheKey); err == nil && item != nil { + return (item.Value.(*V1TerminateSessionArgs)), item.Err + } + defer sS.respCache.Cache(cacheKey, + &utils.ResponseCacheItem{Value: rply, Err: err}) + if !args.TerminateSession && !args.ReleaseResources { return utils.NewErrMandatoryIeMissing("subsystems") } if args.CGREvent.Tenant == "" { args.CGREvent.Tenant = smg.cgrCfg.GeneralCfg().DefaultTenant } - if args.CGREvent.ID == "" { - args.CGREvent.ID = utils.GenUUID() - } + me := engine.NewMapEvent(args.CGREvent.Event) // used for easy access to fields within the event + originID := me.GetStringIgnoreErrors(utils.OriginID) if args.TerminateSession { - if smg.rals == nil { - return utils.NewErrNotConnected(utils.RALService) - } - originID, err := args.CGREvent.FieldAsString(utils.OriginID) - if err != nil { + if originID == "" { return utils.NewErrMandatoryIeMissing(utils.OriginID) } ev := engine.NewSafEvent(args.CGREvent.Event) @@ -2178,17 +2111,30 @@ func (smg *SMGeneric) BiRPCv1TerminateSession(clnt rpcclient.RpcClientConnection return utils.NewErrRALs(err) } } - if err = smg.TerminateSession(args.CGREvent.Tenant, - ev, clnt, originID, dbtItvl); err != nil { + ss = sS.getRelocateSessions(cgrID, + me.GetStringIgnoreErrors(utils.InitialOriginID), + me.GetStringIgnoreErrors(utils.OriginID), + me.GetStringIgnoreErrors(utils.OriginHost)) + var s *Session + if len(ss) == 0 { + if s, err = sS.initSession(args.CGREvent.Tenant, + ev, sS.biJClntID(clnt), + me.GetStringIgnoreErrors(utils.OriginID), dbtItvl); err != nil { + return utils.NewErrRALs(err) + } + } else { + s = ss[0] + } + if err = sS.endSession(s, + me.GetDurationPtrIgnoreErrors(utils.Usage)); err != nil { return utils.NewErrRALs(err) } } if args.ReleaseResources { - if smg.resS == nil { + if sS.resS == nil { return utils.NewErrNotConnected(utils.ResourceS) } - originID, err := args.CGREvent.FieldAsString(utils.OriginID) - if err != nil { + if originID == "" { return utils.NewErrMandatoryIeMissing(utils.OriginID) } var reply string @@ -2197,47 +2143,61 @@ func (smg *SMGeneric) BiRPCv1TerminateSession(clnt rpcclient.RpcClientConnection UsageID: originID, // same ID should be accepted by first group since the previous resource should be expired Units: 1, } - if err = smg.resS.Call(utils.ResourceSv1ReleaseResources, + if err = sS.resS.Call(utils.ResourceSv1ReleaseResources, argsRU, &reply); err != nil { return utils.NewErrResourceS(err) } } if args.ProcessThresholds { - if smg.thdS == nil { + if sS.thdS == nil { return utils.NewErrNotConnected(utils.ThresholdS) } var tIDs []string thEv := &engine.ArgsProcessEvent{ CGREvent: args.CGREvent, } - if err := smg.thdS.Call(utils.ThresholdSv1ProcessEvent, thEv, &tIDs); err != nil && + if err := sS.thdS.Call(utils.ThresholdSv1ProcessEvent, thEv, &tIDs); err != nil && err.Error() != utils.ErrNotFound.Error() { utils.Logger.Warning( - fmt.Sprintf(" error: %s processing event %+v with ThresholdS.", err.Error(), thEv)) + fmt.Sprintf("<%s> error: %s processing event %+v with ThresholdS.", + utils.SessionS, err.Error(), thEv)) } } if args.ProcessStats { - if smg.statS == nil { + if sS.statS == nil { return utils.NewErrNotConnected(utils.StatS) } var statReply []string - if err := smg.statS.Call(utils.StatSv1ProcessEvent, &engine.StatsArgsProcessEvent{CGREvent: args.CGREvent}, &statReply); err != nil && + if err := sS.statS.Call(utils.StatSv1ProcessEvent, + &engine.StatsArgsProcessEvent{CGREvent: args.CGREvent}, &statReply); err != nil && err.Error() != utils.ErrNotFound.Error() { utils.Logger.Warning( - fmt.Sprintf(" error: %s processing event %+v with StatS.", err.Error(), args.CGREvent)) + fmt.Sprintf("<%s> error: %s processing event %+v with StatS.", + utils.SessionS, err.Error(), args.CGREvent)) } } *rply = utils.OK return } -// Called on session end, should send the CDR to CDRS -func (smg *SMGeneric) BiRPCv1ProcessCDR(clnt rpcclient.RpcClientConnection, - cgrEv *utils.CGREvent, reply *string) error { - cgrEv.Context = utils.StringPointer(utils.MetaSessionS) - return smg.cdrsrv.Call(utils.CdrsV2ProcessCDR, cgrEv, reply) +// BiRPCv1ProcessCDR sends the CDR to CDRs +func (sS *SessionS) BiRPCv1ProcessCDR(clnt rpcclient.RpcClientConnection, + cgrEv *utils.CGREvent, rply *string) error { + if args.CGREvent.ID == "" { + args.CGREvent.ID = utils.GenUUID() + } + cacheKey := "BiRPCv1ProcessCDR" + args.CGREvent.ID + if item, err := sS.respCache.Get(cacheKey); err == nil && item != nil { + return (item.Value.(*string)), item.Err + } + defer sS.respCache.Cache(cacheKey, + &utils.ResponseCacheItem{Value: rply, Err: err}) + + GetSetCGRID(ev) + return sS.cdrS.Call(utils.CdrsV2ProcessCDR, cgrEv, rply) } +// NewV1ProcessEventArgs is a constructor for EventArgs used by ProcessEvent func NewV1ProcessEventArgs(resrc, acnts, attrs, thds, stats bool, cgrEv utils.CGREvent) *V1ProcessEventArgs { return &V1ProcessEventArgs{ @@ -2250,6 +2210,7 @@ func NewV1ProcessEventArgs(resrc, acnts, attrs, thds, stats bool, } } +// V1ProcessEventArgs are the options passed to ProcessEvent API type V1ProcessEventArgs struct { GetAttributes bool AllocateResources bool @@ -2260,6 +2221,7 @@ type V1ProcessEventArgs struct { utils.CGREvent } +// V1ProcessEventReply is the reply for the ProcessEvent API type V1ProcessEventReply struct { MaxUsage *time.Duration ResourceAllocation *string @@ -2290,27 +2252,37 @@ func (v1Rply *V1ProcessEventReply) AsNavigableMap( return config.NewNavigableMap(cgrReply), nil } -// Called on session end, should send the CDR to CDRS -func (smg *SMGeneric) BiRPCv1ProcessEvent(clnt rpcclient.RpcClientConnection, +// BiRPCv1ProcessEvent processes one event with the right subsystems based on arguments received +func (sS *SessionS) BiRPCv1ProcessEvent(clnt rpcclient.RpcClientConnection, args *V1ProcessEventArgs, rply *V1ProcessEventReply) (err error) { - if args.CGREvent.Tenant == "" { - args.CGREvent.Tenant = smg.cgrCfg.GeneralCfg().DefaultTenant - } if args.CGREvent.ID == "" { args.CGREvent.ID = utils.GenUUID() } + cacheKey := "BiRPCv1ProcessEvent" + args.CGREvent.ID + if item, err := sS.respCache.Get(cacheKey); err == nil && item != nil { + return (item.Value.(*V1ProcessEventReply)), item.Err + } + defer sS.respCache.Cache(cacheKey, + &utils.ResponseCacheItem{Value: rply, Err: err}) + + if args.CGREvent.Tenant == "" { + args.CGREvent.Tenant = smg.cgrCfg.GeneralCfg().DefaultTenant + } + me := engine.NewMapEvent(args.CGREvent.Event) + originID := me.GetStringIgnoreErrors(utils.OriginID) + if args.GetAttributes { - if smg.attrS == nil { + if sS.attrS == nil { return utils.NewErrNotConnected(utils.AttributeS) } if args.CGREvent.Context == nil { // populate if not already in - args.CGREvent.Context = utils.StringPointer(utils.MetaSessionS) + args.CGREvent.Context = utils.StringPointer(utils.MetaSessions) } attrArgs := &engine.AttrArgsProcessEvent{ CGREvent: args.CGREvent, } var rplyEv engine.AttrSProcessEventReply - if err := smg.attrS.Call(utils.AttributeSv1ProcessEvent, + if err := sS.attrS.Call(utils.AttributeSv1ProcessEvent, attrArgs, &rplyEv); err == nil { args.CGREvent = *rplyEv.CGREvent rply.Attributes = &rplyEv @@ -2319,11 +2291,10 @@ func (smg *SMGeneric) BiRPCv1ProcessEvent(clnt rpcclient.RpcClientConnection, } } if args.AllocateResources { - if smg.resS == nil { + if sS.resS == nil { return utils.NewErrNotConnected(utils.ResourceS) } - originID, err := args.CGREvent.FieldAsString(utils.OriginID) - if err != nil { + if originID == "" { return utils.NewErrMandatoryIeMissing(utils.OriginID) } attrRU := utils.ArgRSv1ResourceUsage{ @@ -2332,17 +2303,14 @@ func (smg *SMGeneric) BiRPCv1ProcessEvent(clnt rpcclient.RpcClientConnection, Units: 1, } var allocMessage string - if err = smg.resS.Call(utils.ResourceSv1AllocateResources, + if err = sS.resS.Call(utils.ResourceSv1AllocateResources, attrRU, &allocMessage); err != nil { return utils.NewErrResourceS(err) } rply.ResourceAllocation = &allocMessage } if args.Debit { - if smg.rals == nil { - return utils.NewErrNotConnected(utils.RALService) - } - if maxUsage, err := smg.ChargeEvent(args.CGREvent.Tenant, + if maxUsage, err := sS.chargeEvent(args.CGREvent.Tenant, engine.NewSafEvent(args.CGREvent.Event)); err != nil { return utils.NewErrRALs(err) } else { @@ -2350,134 +2318,169 @@ func (smg *SMGeneric) BiRPCv1ProcessEvent(clnt rpcclient.RpcClientConnection, } } if args.ProcessThresholds { - if smg.thdS == nil { + if sS.thdS == nil { return utils.NewErrNotConnected(utils.ThresholdS) } var tIDs []string thEv := &engine.ArgsProcessEvent{ CGREvent: args.CGREvent, } - if err := smg.thdS.Call(utils.ThresholdSv1ProcessEvent, thEv, &tIDs); err != nil && + if err := sS.thdS.Call(utils.ThresholdSv1ProcessEvent, + thEv, &tIDs); err != nil && err.Error() != utils.ErrNotFound.Error() { utils.Logger.Warning( - fmt.Sprintf(" error: %s processing event %+v with ThresholdS.", err.Error(), thEv)) + fmt.Sprintf("<%s> error: %s processing event %+v with ThresholdS.", + utils.SessionS, err.Error(), thEv)) } } if args.ProcessStats { - if smg.statS == nil { + if sS.statS == nil { return utils.NewErrNotConnected(utils.StatS) } var statReply []string - if err := smg.statS.Call(utils.StatSv1ProcessEvent, &engine.StatsArgsProcessEvent{CGREvent: args.CGREvent}, &statReply); err != nil && + if err := sS.statS.Call(utils.StatSv1ProcessEvent, + &engine.StatsArgsProcessEvent{CGREvent: args.CGREvent}, &statReply); err != nil && err.Error() != utils.ErrNotFound.Error() { utils.Logger.Warning( - fmt.Sprintf(" error: %s processing event %+v with StatS.", err.Error(), args.CGREvent)) + fmt.Sprintf("<%s> error: %s processing event %+v with StatS.", + utils.SessionS, err.Error(), args.CGREvent)) } } return nil } -func (smg *SMGeneric) OnBiJSONConnect(c *rpc2.Client) { - var s struct{} - smg.biJsonConns[c] = s -} - -func (smg *SMGeneric) OnBiJSONDisconnect(c *rpc2.Client) { - delete(smg.biJsonConns, c) -} - -func (smg *SMGeneric) syncSessions() { - var rpcClnts []rpcclient.RpcClientConnection - for _, conn := range smg.intBiJSONConns { - rpcClnts = append(rpcClnts, conn) - } - for conn := range smg.biJsonConns { - rpcClnts = append(rpcClnts, conn) - } - queriedCGRIDs := make(utils.StringMap) - var err error - for _, conn := range rpcClnts { - var queriedSessionIDs []*SessionID - if conn != nil { - errChan := make(chan error) - go func() { - errChan <- conn.Call(utils.SessionSv1GetActiveSessionIDs, - "", &queriedSessionIDs) - }() - select { - case err = <-errChan: - if err != nil { - utils.Logger.Warning( - fmt.Sprintf("<%s> error quering session ids : %+v", utils.SessionS, err)) - continue - } - case <-time.After(smg.cgrCfg.GeneralCfg().ReplyTimeout): - utils.Logger.Warning( - fmt.Sprintf("<%s> timeout quering session ids ", utils.SessionS)) - continue - } - for _, sessionID := range queriedSessionIDs { - queriedCGRIDs[sessionID.CGRID()] = true - } - } - } - var toBeRemoved []string - smg.aSessionsMux.RLock() - for cgrid := range smg.activeSessions { - if _, has := queriedCGRIDs[cgrid]; !has { - toBeRemoved = append(toBeRemoved, cgrid) - } - } - smg.aSessionsMux.RUnlock() - for _, cgrID := range toBeRemoved { - aSessions := smg.getSessions(cgrID, false) - if len(aSessions[cgrID]) == 0 { - continue - } - terminator := &smgSessionTerminator{ - ttl: time.Duration(0), - } - smg.ttlTerminate(aSessions[cgrID][0], terminator) - } -} - -func (smg *SMGeneric) BiRPCv1SyncSessions(clnt rpcclient.RpcClientConnection, +// BiRPCv1SyncSessions will sync sessions on demand +func (sS *SessionS) BiRPCv1SyncSessions(clnt rpcclient.RpcClientConnection, ignParam string, reply *string) error { - smg.syncSessions() + sS.syncSessions() *reply = utils.OK return nil } -func (smg *SMGeneric) BiRPCV1ForceDisconnect(clnt rpcclient.RpcClientConnection, +// BiRPCV1ForceDisconnect will force disconnecting sessions matching sessions +func (sS *SessionS) BiRPCV1ForceDisconnect(clnt rpcclient.RpcClientConnection, fltr map[string]string, reply *string) error { for fldName, fldVal := range fltr { if fldVal == "" { fltr[fldName] = utils.META_NONE } } - aSessions, _, err := smg.asActiveSessions(fltr, false, false) + aSs, _, err := sS.asActiveSessions(fltr, false, false) if err != nil { return utils.NewErrServerError(err) - } else if len(aSessions) == 0 { + } else if len(aSs) == 0 { return utils.ErrNotFound } - for _, aSession := range aSessions { - sessions := smg.getSessions(aSession.CGRID, false) - if len(sessions[aSession.CGRID]) == 0 { - continue - } - terminator := &smgSessionTerminator{ - ttl: time.Duration(0), - } - smg.ttlTerminate(sessions[aSession.CGRID][0], terminator) + for _, as := range aSs { + sS.forceSTerminate(aSs.CGRID, 0, nil) } *reply = utils.OK return nil } -func (smg *SMGeneric) BiRPCv1RegisterInternalBiJSONConn(clnt rpcclient.RpcClientConnection, - ignParam string, reply *string) error { - smg.intBiJSONConns = append(smg.intBiJSONConns, clnt) - *reply = utils.OK +// BiRPCV1GetMaxUsage returns the maximum usage as seconds, compatible with OpenSIPS 2.3 +// DEPRECATED, it will be removed in future versions +func (sS *SessionS) BiRPCV1GetMaxUsage(clnt rpcclient.RpcClientConnection, + ev engine.MapEvent, maxUsage *float64) error { + maxUsageDur, err := smg.GetMaxUsage( + utils.FirstNonEmpty(ev.GetStringIgnoreErrors(utils.Tenant), + smg.cgrCfg.GeneralCfg().DefaultTenant), + engine.NewSafEvent(ev)) + if err != nil { + return utils.NewErrServerError(err) + } + if maxUsageDur == time.Duration(-1) { + *maxUsage = -1.0 + } else { + *maxUsage = maxUsageDur.Seconds() + } return nil } + +// BiRPCV1InitiateSession is called on session start, returns the maximum number of seconds the session can last +// DEPRECATED, it will be removed in future versions +// Kept for compatibility with OpenSIPS 2.3 +func (sS *SessionS) BiRPCV1InitiateSession(clnt rpcclient.RpcClientConnection, + ev engine.MapEvent, maxUsage *float64) (err error) { + var rply *V1InitSessionReply + if err = sS.BiRPCv1InitiateSession( + clnt, + &V1InitSessionArgs{ + InitSession: true, + CGREvent: &utils.CGREvent{ + Tenant: utils.FirstNonEmpty( + ev.GetStringIgnoreErrors(utils.Tenant), + smg.cgrCfg.GeneralCfg().DefaultTenant), + ID: utils.UUIDSha1Prefix(), + Event: ev}}, + rply); err != nil { + return + } + if *rply.MaxUsage == time.Duration(-1) { + *maxUsage = -1.0 + } else { + *maxUsage = *rply.MaxUsage.Seconds() + } + return +} + +// BiRPCV1UpdateSession processes interim updates, returns remaining duration from the RALs +// DEPRECATED, it will be removed in future versions +// Kept for compatibility with OpenSIPS 2.3 +func (sS *SessionS) BiRPCV1UpdateSession(clnt rpcclient.RpcClientConnection, + ev engine.MapEvent, maxUsage *float64) (err error) { + var rply *V1UpdateSessionReply + if err = sS.BiRPCv1UpdateSession( + clnt, + &V1InitSessionArgs{ + UpdateSession: true, + CGREvent: &utils.CGREvent{ + Tenant: utils.FirstNonEmpty( + ev.GetStringIgnoreErrors(utils.Tenant), + smg.cgrCfg.GeneralCfg().DefaultTenant), + ID: utils.UUIDSha1Prefix(), + Event: ev}}, + rply); err != nil { + return + } + if *rply.MaxUsage == time.Duration(-1) { + *maxUsage = -1.0 + } else { + *maxUsage = *rply.MaxUsage.Seconds() + } + return +} + +// BiRPCV1TerminateSession is called on session end, should stop debit loop +// DEPRECATED, it will be removed in future versions +// Kept for compatibility with OpenSIPS 2.3 +func (sS *SessionS) BiRPCV1TerminateSession(clnt rpcclient.RpcClientConnection, + ev engine.MapEvent, rply *string) (err error) { + return sS.BiRPCv1TerminateSession( + clnt, + &V1TerminateSessionArgs{ + TerminateSession: true, + CGREvent: &utils.CGREvent{ + Tenant: utils.FirstNonEmpty( + ev.GetStringIgnoreErrors(utils.Tenant), + smg.cgrCfg.GeneralCfg().DefaultTenant), + ID: utils.UUIDSha1Prefix(), + Event: ev}}, + rply) +} + +// BiRPCV1ProcessCDR should send the CDR to CDRS +// DEPRECATED, it will be removed in future versions +// Kept for compatibility with OpenSIPS 2.3 +func (sS *SessionS) BiRPCV1ProcessCDR(clnt rpcclient.RpcClientConnection, + ev engine.MapEvent, rply *string) (err error) { + return sS.ProcessCDR( + clnt, + &utils.CGREvent{ + Tenant: utils.FirstNonEmpty( + ev.GetStringIgnoreErrors(utils.Tenant), + smg.cgrCfg.GeneralCfg().DefaultTenant), + ID: utils.UUIDSha1Prefix(), + Event: ev}, + rply) +} diff --git a/sessions/sessions_test.go b/sessions/sessions_test.go index 89e91849b..36c938052 100755 --- a/sessions/sessions_test.go +++ b/sessions/sessions_test.go @@ -18,6 +18,7 @@ along with this program. If not, see package sessions +/* import ( "reflect" "testing" @@ -359,3 +360,4 @@ func TestV1ProcessEventReplyAsNavigableMap(t *testing.T) { t.Errorf("Expecting \n%+v\n, received: \n%+v", expected, rply) } } +*/ diff --git a/utils/consts.go b/utils/consts.go index 534c6a00f..03fc1b572 100755 --- a/utils/consts.go +++ b/utils/consts.go @@ -18,6 +18,8 @@ along with this program. If not, see package utils +import "sort" + var ( CDRExportFormats = []string{DRYRUN, MetaFileCSV, MetaFileFWV, MetaHTTPjsonCDR, MetaHTTPjsonMap, MetaHTTPjson, META_HTTP_POST, MetaAMQPjsonCDR, MetaAMQPjsonMap, MetaAWSjsonMap} @@ -774,6 +776,7 @@ const ( SessionSv1GetActiveSessions = "SessionSv1.GetActiveSessions" SessionSv1ForceDisconnect = "SessionSv1.ForceDisconnect" SessionSv1GetPassiveSessions = "SessionSv1.GetPassiveSessions" + SessionSv1SetPassiveSessions = "SessionSV1.SetPassiveSessions" SMGenericV1InitiateSession = "SMGenericV1.InitiateSession" SMGenericV2InitiateSession = "SMGenericV2.InitiateSession" SMGenericV2UpdateSession = "SMGenericV2.UpdateSession" @@ -782,6 +785,13 @@ const ( SessionSv1RegisterInternalBiJSONConn = "SessionSv1.RegisterInternalBiJSONConn" ) +// Responder APIs +const ( + ResponderDebit = "Responder.Debit" + ResponderRefundIncrements = "Responder.RefundIncrements" + ResponderGetMaxSessionTime = "Responder.GetMaxSessionTime" +) + // DispatcherS APIs const ( DispatcherSv1Ping = "DispatcherSv1.Ping" @@ -814,10 +824,11 @@ const ( // Cdrs APIs const ( - CdrsV1CountCDRs = "CdrsV1.CountCDRs" - CdrsV1GetCDRs = "CdrsV1.GetCDRs" - CdrsV2ProcessCDR = "CdrsV2.ProcessCDR" - CdrsV2RateCDRs = "CdrsV2.RateCDRs" + CdrsV1CountCDRs = "CdrsV1.CountCDRs" + CdrsV1GetCDRs = "CdrsV1.GetCDRs" + CdrsV2ProcessCDR = "CdrsV2.ProcessCDR" + CdrsV2RateCDRs = "CdrsV2.RateCDRs" + CdrsV2StoreSMCost = "CdrsV2.StoreSMCost" ) // Scheduler @@ -979,7 +990,16 @@ func buildCacheIndexesToPrefix() { } } +// sortStringSlices makes sure the slices are string sorted +// so we can search inside using SliceHasMember +func sortStringSlices() { + sort.Strings(CDRExportFormats) + sort.Strings(PrimaryCdrFields) + sort.Strings(NotExtraCDRFields) +} + func init() { + sortStringSlices() buildCacheInstRevPrefixes() buildCacheIndexesToPrefix() MainCDRFieldsMap = NewStringMap(MainCDRFields...) diff --git a/utils/slice.go b/utils/slice.go index c15f75630..eac95488e 100644 --- a/utils/slice.go +++ b/utils/slice.go @@ -32,16 +32,13 @@ func IsSliceMember(ss []string, s string) bool { return false } -// SliceHasMember is a simpler mode to match inside a slice +// SliceHasMember searches within a *sorted* slice // useful to search in shared vars (no slice sort) -func SliceHasMember(ss []string, s string) (has bool) { - for _, mbr := range ss { - if mbr == s { - has = true - break - } +func SliceHasMember(ss []string, s string) bool { + if i := sort.SearchStrings(ss, s); i < len(ss) && ss[i] == s { + return true } - return + return false } func SliceWithoutMember(ss []string, s string) []string {