Files
ttstd-infinite-canvas/backend/internal/app/provider_video_polling.go

142 lines
3.5 KiB
Go

package app
import (
"context"
"encoding/json"
"errors"
"net"
"net/http"
"strings"
"time"
)
const defaultVideoPollInterval = 30 * time.Second
type videoPollPolicy struct {
Interval time.Duration
MaxNotFoundMisses int
Sleep func(context.Context, time.Duration) error
}
type videoPollOutcome struct {
Done bool
Result map[string]interface{}
}
func defaultVideoPollPolicy() videoPollPolicy {
return videoPollPolicy{
Interval: defaultVideoPollInterval,
MaxNotFoundMisses: 3,
Sleep: sleepContext,
}
}
func runVideoPollLoop(ctx context.Context, taskID string, policy videoPollPolicy, query func(context.Context) (videoPollOutcome, error)) (map[string]interface{}, error) {
policy = normalizeVideoPollPolicy(policy)
deadline := providerPollingDeadline(ctx)
nextDelay := policy.Interval
notFoundMisses := 0
for time.Now().Before(deadline) {
if err := policy.Sleep(ctx, nextDelay); err != nil {
return nil, err
}
outcome, err := query(ctx)
if err != nil {
retry, notFound := retryableVideoPollError(ctx, err)
if !retry {
return nil, err
}
if notFound {
notFoundMisses++
if notFoundMisses >= policy.MaxNotFoundMisses {
return nil, err
}
} else {
notFoundMisses = 0
}
nextDelay = max(policy.Interval, providerRetryAfter(err))
continue
}
notFoundMisses = 0
nextDelay = policy.Interval
if outcome.Done {
return outcome.Result, nil
}
}
if err := ctx.Err(); err != nil {
return nil, err
}
return nil, context.DeadlineExceeded
}
func normalizeVideoPollPolicy(policy videoPollPolicy) videoPollPolicy {
if policy.Interval <= 0 {
policy.Interval = defaultVideoPollInterval
}
if policy.MaxNotFoundMisses <= 0 {
policy.MaxNotFoundMisses = 3
}
if policy.Sleep == nil {
policy.Sleep = sleepContext
}
return policy
}
func retryableVideoPollError(ctx context.Context, err error) (retry bool, notFound bool) {
if err == nil || ctx.Err() != nil || errors.Is(err, context.Canceled) {
return false, false
}
if code, _ := ChannelSlotFailureDetails(err); code != "" {
return true, false
}
if errors.Is(err, context.DeadlineExceeded) {
return true, false
}
var httpErr providerHTTPError
if errors.As(err, &httpErr) {
if isProviderTaskNotReadyError(httpErr) {
return true, true
}
if httpErr.StatusCode == http.StatusNotFound {
return true, true
}
switch httpErr.StatusCode {
case http.StatusRequestTimeout, http.StatusConflict, http.StatusTooEarly, http.StatusTooManyRequests:
return true, false
default:
return httpErr.StatusCode >= http.StatusInternalServerError, false
}
}
var networkError net.Error
if errors.As(err, &networkError) {
return networkError.Timeout() || networkError.Temporary(), false
}
return false, false
}
func isProviderTaskNotReadyError(httpErr providerHTTPError) bool {
if httpErr.StatusCode != http.StatusBadRequest && httpErr.StatusCode != http.StatusNotFound {
return false
}
var payload map[string]any
if json.Unmarshal([]byte(httpErr.Body), &payload) != nil {
return false
}
code, message := providerFailureDetails(payload)
for _, value := range []string{code, message} {
switch strings.ToLower(strings.TrimSpace(value)) {
case "task_not_exist", "task_not_found", "task not exist", "task not found":
return true
}
}
return false
}
func providerRetryAfter(err error) time.Duration {
var httpErr providerHTTPError
if errors.As(err, &httpErr) && httpErr.RetryAfter > 0 {
return httpErr.RetryAfter
}
return 0
}