100 lines
3.5 KiB
Go
100 lines
3.5 KiB
Go
package rpc
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"log"
|
|
"time"
|
|
|
|
"git.ipao.vip/rogee/go-sip/internal/agent"
|
|
)
|
|
|
|
var (
|
|
ErrApprovedDialExpired = errors.New("Dispatcher-approved dial deadline expired")
|
|
ErrApprovedCallPreparation = errors.New("approved call preparation rejected before dispatch")
|
|
)
|
|
|
|
// ApprovedCallWorker keeps a call registered until its prepared synchronous
|
|
// runner has finished physical hangup. Prepare must validate the per-call
|
|
// snapshot, media and delivery without dialing or persisting business data;
|
|
// the returned runner must not return before hangup. The process lifecycle,
|
|
// not the initiating Unary request, owns the call context.
|
|
type ApprovedCallWorker struct {
|
|
Lifecycle context.Context
|
|
Calls *agent.TaskCalls
|
|
Prepare func(ApprovedExecution) (func(context.Context) error, error)
|
|
OnFailure func(ApprovedExecution, error) error
|
|
Now func() time.Time
|
|
}
|
|
|
|
// Originate returns after registration and launch, allowing Dispatcher to
|
|
// acknowledge issuance while the call remains cancellable by task control.
|
|
// If scheduling delays the worker past the signed deadline it reports failure
|
|
// without dialing; an unknown durable execution is never automatically retried.
|
|
func (w *ApprovedCallWorker) Originate(request context.Context, approved ApprovedExecution) error {
|
|
if w == nil || request == nil || w.Lifecycle == nil || w.Calls == nil || w.Prepare == nil || w.OnFailure == nil {
|
|
return errors.New("approved call lifecycle, runner and failure reporting are required")
|
|
}
|
|
if err := request.Err(); err != nil {
|
|
return err
|
|
}
|
|
if err := w.Lifecycle.Err(); err != nil {
|
|
return err
|
|
}
|
|
if approved.DispatcherID == "" || approved.TenantID <= 0 || approved.TaskID == "" || approved.CallID == "" || approved.MaxCallDuration <= 0 || approved.DialBefore.IsZero() {
|
|
return errors.New("approved call identity, duration or dial deadline is incomplete")
|
|
}
|
|
now := w.Now
|
|
if now == nil {
|
|
now = time.Now
|
|
}
|
|
if !now().Before(approved.DialBefore) {
|
|
return ErrApprovedDialExpired
|
|
}
|
|
prepared, err := w.Prepare(approved)
|
|
if err != nil {
|
|
return errors.Join(ErrApprovedCallPreparation, err)
|
|
}
|
|
if prepared == nil {
|
|
return ErrApprovedCallPreparation
|
|
}
|
|
callContext, cancel := context.WithTimeout(w.Lifecycle, approved.MaxCallDuration)
|
|
release, err := w.Calls.Register(agent.TaskIdentity{DispatcherID: approved.DispatcherID, TenantID: approved.TenantID, TaskID: approved.TaskID}, approved.CallID, cancel)
|
|
if err != nil {
|
|
cancel()
|
|
return err
|
|
}
|
|
issued := make(chan error, 1)
|
|
go func() {
|
|
// Deferred cleanup also runs if a broken runner panics; panics are not
|
|
// swallowed, and the process does not claim a successful outcome.
|
|
defer cancel()
|
|
defer release()
|
|
if err := callContext.Err(); err != nil {
|
|
issued <- err
|
|
return
|
|
}
|
|
if err := request.Err(); err != nil {
|
|
issued <- err
|
|
return
|
|
}
|
|
if !now().Before(approved.DialBefore) {
|
|
issued <- ErrApprovedDialExpired
|
|
return
|
|
}
|
|
issued <- nil // the Unary response may now acknowledge scheduled work
|
|
runErr := prepared(callContext)
|
|
// Physical execution resources are released independently of any later
|
|
// failure reporting, OSS work or MQ publisher confirmation.
|
|
release()
|
|
if runErr == nil {
|
|
return
|
|
}
|
|
log.Printf("Agent approved call ended with error: event_id=%q task_id=%q cause_type=%T", approved.SourceEventID, approved.TaskID, runErr)
|
|
if err := w.OnFailure(approved, runErr); err != nil {
|
|
log.Printf("Agent approved call failure reporting failed: event_id=%q task_id=%q cause_type=%T", approved.SourceEventID, approved.TaskID, err)
|
|
}
|
|
}()
|
|
return <-issued
|
|
}
|