Files
go-sip/internal/rpc/approved_worker.go
T

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
}