Skip to content

Commit

Permalink
Fix retrying of calls where answering machine was detected
Browse files Browse the repository at this point in the history
  • Loading branch information
rowanseymour committed Aug 13, 2021
1 parent 923df39 commit daaa2a1
Show file tree
Hide file tree
Showing 6 changed files with 69 additions and 32 deletions.
36 changes: 29 additions & 7 deletions core/ivr/ivr.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,10 @@ type Client interface {

ResumeForRequest(r *http.Request) (Resume, error)

StatusForRequest(r *http.Request) (models.ConnectionStatus, int)
// StatusForRequest returns the current call status for the passed in request and also:
// - duration of call if known
// - whether we should hangup - i.e. the call is still in progress on the provider side, but we shouldn't continue with it
StatusForRequest(r *http.Request) (models.ConnectionStatus, int, bool)

PreprocessResume(ctx context.Context, db *sqlx.DB, rp *redis.Pool, conn *models.ChannelConnection, r *http.Request) ([]byte, error)

Expand Down Expand Up @@ -445,7 +448,7 @@ func ResumeIVRFlow(
}

// make sure our call is still happening
status, _ := client.StatusForRequest(r)
status, _, _ := client.StatusForRequest(r)
if status != models.ConnectionStatusInProgress {
err := conn.UpdateStatus(ctx, rt.DB, status, 0, time.Now())
if err != nil {
Expand Down Expand Up @@ -588,10 +591,30 @@ func buildMsgResume(
// ended for some reason and update the state of the call and session if so
func HandleIVRStatus(ctx context.Context, rt *runtime.Runtime, oa *models.OrgAssets, client Client, conn *models.ChannelConnection, r *http.Request, w http.ResponseWriter) error {
// read our status and duration from our client
status, duration := client.StatusForRequest(r)
status, duration, hangup := client.StatusForRequest(r)

if hangup {
err := HangupCall(ctx, rt.Config, rt.DB, conn)
if err != nil {
return errors.Wrapf(err, "error hanging up call")
}
}

// if we errored schedule a retry if appropriate
if status == models.ConnectionStatusErrored {
// get session associated with this connection
sessionID, sessionStatus, err := models.SessionForConnection(ctx, rt.DB, conn)
if err != nil {
return errors.Wrapf(err, "error fetching session for connection")
}

// if session is still active we interrupt it
if sessionStatus == models.SessionStatusWaiting {
err = models.ExitSessions(ctx, rt.DB, []models.SessionID{sessionID}, models.ExitInterrupted, time.Now())
if err != nil {
logrus.WithError(err).Error("error interrupting session for connection")
}
}

// no associated start? this is a permanent failure
if conn.StartID() == models.NilStartID {
conn.MarkFailed(ctx, rt.DB, time.Now())
Expand All @@ -611,9 +634,8 @@ func HandleIVRStatus(ctx context.Context, rt *runtime.Runtime, oa *models.OrgAss

conn.MarkErrored(ctx, rt.DB, time.Now(), flow.IVRRetryWait())

if conn.Status() == models.ConnectionStatusErrored {
return client.WriteEmptyResponse(w, fmt.Sprintf("status updated: %s next_attempt: %s", conn.Status(), conn.NextAttempt()))
}
return client.WriteEmptyResponse(w, fmt.Sprintf("status updated: %s next_attempt: %s", conn.Status(), conn.NextAttempt()))

} else if status == models.ConnectionStatusFailed {
conn.MarkFailed(ctx, rt.DB, time.Now())
} else {
Expand Down
20 changes: 11 additions & 9 deletions core/ivr/twiml/twiml.go
Original file line number Diff line number Diff line change
Expand Up @@ -287,37 +287,39 @@ func (c *client) ResumeForRequest(r *http.Request) (ivr.Resume, error) {
}
}

// StatusForRequest returns the current call status for the passed in status (and optional duration if known)
func (c *client) StatusForRequest(r *http.Request) (models.ConnectionStatus, int) {
// StatusForRequest returns the current call status for the passed in request and also:
// - duration of call if known
// - whether we should hangup - i.e. the call is still in progress on the provider side, but we shouldn't continue with it
func (c *client) StatusForRequest(r *http.Request) (models.ConnectionStatus, int, bool) {
// we re-use our status callback for AMD results which will have an AnsweredBy field but no CallStatus field
answeredBy := r.Form.Get("AnsweredBy")
if answeredBy != "" {
switch answeredBy {
case "machine_start", "fax":
return models.ConnectionStatusErrored, 0
return models.ConnectionStatusErrored, 0, true
}
return models.ConnectionStatusInProgress, 0
return models.ConnectionStatusInProgress, 0, false
}

status := r.Form.Get("CallStatus")
switch status {

case "queued", "ringing":
return models.ConnectionStatusWired, 0
return models.ConnectionStatusWired, 0, false

case "in-progress", "initiated":
return models.ConnectionStatusInProgress, 0
return models.ConnectionStatusInProgress, 0, false

case "completed":
duration, _ := strconv.Atoi(r.Form.Get("CallDuration"))
return models.ConnectionStatusCompleted, duration
return models.ConnectionStatusCompleted, duration, false

case "busy", "no-answer", "canceled", "failed":
return models.ConnectionStatusErrored, 0
return models.ConnectionStatusErrored, 0, false

default:
logrus.WithField("call_status", status).Error("unknown call status in status callback")
return models.ConnectionStatusFailed, 0
return models.ConnectionStatusFailed, 0, false
}
}

Expand Down
24 changes: 13 additions & 11 deletions core/ivr/vonage/vonage.go
Original file line number Diff line number Diff line change
Expand Up @@ -542,48 +542,50 @@ type StatusRequest struct {
Duration string `json:"duration"`
}

// StatusForRequest returns the current call status for the passed in status (and optional duration if known)
func (c *client) StatusForRequest(r *http.Request) (models.ConnectionStatus, int) {
// StatusForRequest returns the current call status for the passed in request and also:
// - duration of call if known
// - whether we should hangup - i.e. the call is still in progress on the provider side, but we shouldn't continue with it
func (c *client) StatusForRequest(r *http.Request) (models.ConnectionStatus, int, bool) {
// this is a resume, call is in progress, no need to look at the body
if r.Form.Get("action") == "resume" {
return models.ConnectionStatusInProgress, 0
return models.ConnectionStatusInProgress, 0, false
}

status := &StatusRequest{}
bb, err := readBody(r)
if err != nil {
logrus.WithError(err).Error("error reading status request body")
return models.ConnectionStatusErrored, 0
return models.ConnectionStatusErrored, 0, false
}
err = json.Unmarshal(bb, status)
if err != nil {
logrus.WithError(err).WithField("body", string(bb)).Error("error unmarshalling ncco status")
return models.ConnectionStatusErrored, 0
return models.ConnectionStatusErrored, 0, false
}

// transfer status callbacks have no status, safe to ignore them
if status.Status == "" {
return models.ConnectionStatusInProgress, 0
return models.ConnectionStatusInProgress, 0, false
}

switch status.Status {

case "started", "ringing":
return models.ConnectionStatusWired, 0
return models.ConnectionStatusWired, 0, false

case "answered":
return models.ConnectionStatusInProgress, 0
return models.ConnectionStatusInProgress, 0, false

case "completed":
duration, _ := strconv.Atoi(status.Duration)
return models.ConnectionStatusCompleted, duration
return models.ConnectionStatusCompleted, duration, false

case "rejected", "busy", "unanswered", "timeout", "failed", "machine":
return models.ConnectionStatusErrored, 0
return models.ConnectionStatusErrored, 0, false

default:
logrus.WithField("status", status.Status).Error("unknown call status in ncco callback")
return models.ConnectionStatusFailed, 0
return models.ConnectionStatusFailed, 0, false
}
}

Expand Down
3 changes: 0 additions & 3 deletions core/models/channel_connection.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,8 @@ const (
ConnectionStatusQueued = ConnectionStatus("Q")
ConnectionStatusWired = ConnectionStatus("W")
ConnectionStatusInProgress = ConnectionStatus("I")
ConnectionStatusBusy = ConnectionStatus("B")
ConnectionStatusFailed = ConnectionStatus("F")
ConnectionStatusErrored = ConnectionStatus("E")
ConnectionStatusNoAnswer = ConnectionStatus("N")
ConnectionStatusCancelled = ConnectionStatus("C")
ConnectionStatusCompleted = ConnectionStatus("D")

ConnectionMaxRetries = 3
Expand Down
14 changes: 14 additions & 0 deletions core/models/runs.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ type SessionCommitHook func(context.Context, *sqlx.Tx, *redis.Pool, *OrgAssets,
type SessionID int64
type SessionStatus string

const NilSessionID = SessionID(0)

type FlowRunID int64

const NilFlowRunID = FlowRunID(0)
Expand Down Expand Up @@ -987,6 +989,18 @@ WHERE
fs.contact_id = ANY($2)
`

func SessionForConnection(ctx context.Context, db *sqlx.DB, conn *ChannelConnection) (SessionID, SessionStatus, error) {
res := struct {
ID SessionID `db:"id"`
Status SessionStatus `db:"status"`
}{}
err := db.GetContext(ctx, &res, `SELECT id, status FROM flows_flowsession WHERE connection_id = $1`, conn.ID())
if err == sql.ErrNoRows {
return NilSessionID, SessionStatusFailed, nil
}
return res.ID, res.Status, err
}

// RunExpiration looks up the run expiration for the passed in run, can return nil if the run is no longer active
func RunExpiration(ctx context.Context, db *sqlx.DB, runID FlowRunID) (*time.Time, error) {
var expiration time.Time
Expand Down
4 changes: 2 additions & 2 deletions core/tasks/ivr/worker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,8 @@ func (c *MockClient) ResumeForRequest(r *http.Request) (ivr.Resume, error) {
return nil, nil
}

func (c *MockClient) StatusForRequest(r *http.Request) (models.ConnectionStatus, int) {
return models.ConnectionStatusFailed, 10
func (c *MockClient) StatusForRequest(r *http.Request) (models.ConnectionStatus, int, bool) {
return models.ConnectionStatusFailed, 10, false
}

func (c *MockClient) PreprocessResume(ctx context.Context, db *sqlx.DB, rp *redis.Pool, conn *models.ChannelConnection, r *http.Request) ([]byte, error) {
Expand Down

0 comments on commit daaa2a1

Please sign in to comment.