worker: reduce subflow polling load and app retry pressure

This commit is contained in:
yashsinghcodes
2026-02-23 23:25:17 +05:30
parent 6bf70ca89d
commit bd8eb55e9f
+123 -12
View File
@@ -19,6 +19,7 @@ import (
"os" "os"
"strconv" "strconv"
"strings" "strings"
"sync"
"time" "time"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
@@ -110,6 +111,8 @@ var finishedExecutions []string
var imagesDistributed []string var imagesDistributed []string
var imagedownloadTimeout = time.Second * 300 var imagedownloadTimeout = time.Second * 300
var subflowPollBackoff sync.Map
var window = shuffle.NewTimeWindow(10 * time.Second) var window = shuffle.NewTimeWindow(10 * time.Second)
// Images to be autodeployed in the latest version of Shuffle. // Images to be autodeployed in the latest version of Shuffle.
@@ -275,8 +278,6 @@ func setWorkflowExecution(ctx context.Context, workflowExecution shuffle.Workflo
break break
} }
// Sleep for 1 second
time.Sleep(1 * time.Second)
} }
} else { } else {
log.Printf("[DEBUG][%s] No need to poll for results. Not polling", workflowExecution.ExecutionId) log.Printf("[DEBUG][%s] No need to poll for results. Not polling", workflowExecution.ExecutionId)
@@ -2176,6 +2177,58 @@ func handleSubflowPoller(ctx context.Context, workflowExecution shuffle.Workflow
data = fmt.Sprintf(`{"execution_id": "%s", "authorization": "%s"}`, workflowExecution.ExecutionId, workflowExecution.Authorization) data = fmt.Sprintf(`{"execution_id": "%s", "authorization": "%s"}`, workflowExecution.ExecutionId, workflowExecution.Authorization)
} }
key := fmt.Sprintf("%s:%s", workflowExecution.ExecutionId, subflowId)
cacheKey := fmt.Sprintf("workflowexecution_%s", workflowExecution.ExecutionId)
usedCache := false
if cacheData, err := shuffle.GetCache(ctx, cacheKey); err == nil {
cachedBytes, ok := cacheData.([]uint8)
if ok {
cacheWorkflow := shuffle.WorkflowExecution{}
if jsonErr := json.Unmarshal([]byte(cachedBytes), &cacheWorkflow); jsonErr == nil {
workflowExecution = cacheWorkflow
usedCache = true
log.Printf("[DEBUG][%s] Using cached workflow execution for subflow poll", workflowExecution.ExecutionId)
if workflowExecution.Status == "FINISHED" || workflowExecution.Status == "SUCCESS" {
log.Printf("[INFO][%s] Workflow execution is finished (cache). Exiting worker.", workflowExecution.ExecutionId)
log.Printf("[DEBUG] Shutting down (20)")
resetSubflowPollDelay(key)
if isKubernetes == "true" {
clientset, _, err := shuffle.GetKubernetesClient()
if err != nil {
log.Println("[ERROR] Error getting kubernetes client (2):", err)
os.Exit(1)
}
cleanupKubernetesExecution(clientset, workflowExecution, kubernetesNamespace)
} else {
shutdown(workflowExecution, "", "", true)
}
}
for _, result := range workflowExecution.Results {
if result.Action.ID != subflowId {
continue
}
if result.Status == "SUCCESS" || result.Status == "FINISHED" || result.Status == "FAILURE" || result.Status == "ABORTED" {
resetSubflowPollDelay(key)
setWorkflowExecution(ctx, workflowExecution, false)
return nil
}
}
}
}
}
if usedCache {
delay := nextSubflowPollDelay(key)
attempt := getSubflowPollAttempt(key)
log.Printf("[DEBUG][%s] Subflow poll backoff attempt %d for %s (cache hit), sleeping %s", workflowExecution.ExecutionId, attempt, subflowId, delay)
time.Sleep(delay)
return errors.New("Subflow status not found yet (cache)")
}
req, err := http.NewRequest( req, err := http.NewRequest(
"POST", "POST",
streamResultUrl, streamResultUrl,
@@ -2186,7 +2239,7 @@ func handleSubflowPoller(ctx context.Context, workflowExecution shuffle.Workflow
newresp, err := client.Do(req) newresp, err := client.Do(req)
if err != nil { if err != nil {
log.Printf("[ERROR] Failed making request (1): %s", err) log.Printf("[ERROR] Failed making request (1): %s", err)
time.Sleep(time.Duration(sleepTime) * time.Second) time.Sleep(nextSubflowPollDelay(key))
return err return err
} }
@@ -2194,7 +2247,7 @@ func handleSubflowPoller(ctx context.Context, workflowExecution shuffle.Workflow
body, err := ioutil.ReadAll(newresp.Body) body, err := ioutil.ReadAll(newresp.Body)
if err != nil { if err != nil {
log.Printf("[ERROR] Failed reading body (1): %s", err) log.Printf("[ERROR] Failed reading body (1): %s", err)
time.Sleep(time.Duration(sleepTime) * time.Second) time.Sleep(nextSubflowPollDelay(key))
return err return err
} }
@@ -2206,20 +2259,21 @@ func handleSubflowPoller(ctx context.Context, workflowExecution shuffle.Workflow
shutdown(workflowExecution, "", "", true) shutdown(workflowExecution, "", "", true)
} }
time.Sleep(time.Duration(sleepTime) * time.Second) time.Sleep(nextSubflowPollDelay(key))
return errors.New(fmt.Sprintf("Bad statuscode: %d", newresp.StatusCode)) return errors.New(fmt.Sprintf("Bad statuscode: %d", newresp.StatusCode))
} }
err = json.Unmarshal(body, &workflowExecution) err = json.Unmarshal(body, &workflowExecution)
if err != nil { if err != nil {
log.Printf("[ERROR] Failed workflowExecution unmarshal: %s", err) log.Printf("[ERROR] Failed workflowExecution unmarshal: %s", err)
time.Sleep(time.Duration(sleepTime) * time.Second) time.Sleep(nextSubflowPollDelay(key))
return err return err
} }
if workflowExecution.Status == "FINISHED" || workflowExecution.Status == "SUCCESS" { if workflowExecution.Status == "FINISHED" || workflowExecution.Status == "SUCCESS" {
log.Printf("[INFO][%s] Workflow execution is finished. Exiting worker.", workflowExecution.ExecutionId) log.Printf("[INFO][%s] Workflow execution is finished. Exiting worker.", workflowExecution.ExecutionId)
log.Printf("[DEBUG] Shutting down (20)") log.Printf("[DEBUG] Shutting down (20)")
resetSubflowPollDelay(key)
if isKubernetes == "true" { if isKubernetes == "true" {
// log.Printf("workflow execution: %#v", workflowExecution) // log.Printf("workflow execution: %#v", workflowExecution)
clientset, _, err := shuffle.GetKubernetesClient() clientset, _, err := shuffle.GetKubernetesClient()
@@ -2235,11 +2289,14 @@ func handleSubflowPoller(ctx context.Context, workflowExecution shuffle.Workflow
} }
hasUserinput := false hasUserinput := false
foundSubflow := false
for _, result := range workflowExecution.Results { for _, result := range workflowExecution.Results {
if result.Action.ID != subflowId { if result.Action.ID != subflowId {
continue continue
} }
foundSubflow = true
if result.Action.AppName == "User Input" { if result.Action.AppName == "User Input" {
hasUserinput = true hasUserinput = true
} }
@@ -2247,21 +2304,66 @@ func handleSubflowPoller(ctx context.Context, workflowExecution shuffle.Workflow
log.Printf("[DEBUG][%s] Found subflow to handle: %s (%s)", workflowExecution.ExecutionId, result.Action.AppName, result.Status) log.Printf("[DEBUG][%s] Found subflow to handle: %s (%s)", workflowExecution.ExecutionId, result.Action.AppName, result.Status)
if result.Status == "SUCCESS" || result.Status == "FINISHED" || result.Status == "FAILURE" || result.Status == "ABORTED" { if result.Status == "SUCCESS" || result.Status == "FINISHED" || result.Status == "FAILURE" || result.Status == "ABORTED" {
// Check for results // Check for results
resetSubflowPollDelay(key)
setWorkflowExecution(ctx, workflowExecution, false) setWorkflowExecution(ctx, workflowExecution, false)
return nil return nil
} }
} }
if workflowExecution.Status == "WAITING" && workflowExecution.ExecutionSource != "default" && os.Getenv("SHUFFLE_SWARM_CONFIG") != "run" && os.Getenv("SHUFFLE_SWARM_CONFIG") != "swarm" { if workflowExecution.Status == "WAITING" && !foundSubflow && workflowExecution.ExecutionSource != "default" && os.Getenv("SHUFFLE_SWARM_CONFIG") != "run" && os.Getenv("SHUFFLE_SWARM_CONFIG") != "swarm" {
log.Printf("[INFO][%s] Workflow execution is waiting. Exiting worker, as backend will restart it.", workflowExecution.ExecutionId) log.Printf("[INFO][%s] Workflow execution is waiting without subflow progress. Exiting worker, as backend will restart it.", workflowExecution.ExecutionId)
resetSubflowPollDelay(key)
shutdown(workflowExecution, "", "", true) shutdown(workflowExecution, "", "", true)
return nil
} }
log.Printf("[INFO][%s] (2) Status: %s, Results: %d, actions: %d. Userinput: %#v", workflowExecution.ExecutionId, workflowExecution.Status, len(workflowExecution.Results), len(workflowExecution.Workflow.Actions)+extra, hasUserinput) log.Printf("[INFO][%s] (2) Status: %s, Results: %d, actions: %d. Userinput: %#v", workflowExecution.ExecutionId, workflowExecution.Status, len(workflowExecution.Results), len(workflowExecution.Workflow.Actions)+extra, hasUserinput)
delay := nextSubflowPollDelay(key)
attempt := getSubflowPollAttempt(key)
log.Printf("[DEBUG][%s] Subflow poll backoff attempt %d for %s, sleeping %s", workflowExecution.ExecutionId, attempt, subflowId, delay)
time.Sleep(delay)
return errors.New("Subflow status not found yet") return errors.New("Subflow status not found yet")
} }
func nextSubflowPollDelay(key string) time.Duration {
baseDelay := 250 * time.Millisecond
maxDelay := 5 * time.Second
current := 0
if raw, ok := subflowPollBackoff.Load(key); ok {
if value, ok := raw.(int); ok {
current = value
}
}
if current < 8 {
current += 1
}
subflowPollBackoff.Store(key, current)
delay := baseDelay * time.Duration(1<<uint(current-1))
if delay > maxDelay {
delay = maxDelay
}
return delay
}
func resetSubflowPollDelay(key string) {
subflowPollBackoff.Delete(key)
}
func getSubflowPollAttempt(key string) int {
if raw, ok := subflowPollBackoff.Load(key); ok {
if value, ok := raw.(int); ok {
return value
}
}
return 0
}
func handleDefaultExecutionWrapper(ctx context.Context, workflowExecution shuffle.WorkflowExecution, streamResultUrl string, extra int) error { func handleDefaultExecutionWrapper(ctx context.Context, workflowExecution shuffle.WorkflowExecution, streamResultUrl string, extra int) error {
if extra == -1 { if extra == -1 {
extra = 0 extra = 0
@@ -3479,6 +3581,8 @@ func deploySwarmService(dockercli *dockerclient.Client, name, image string, depl
fmt.Sprintf("SHUFFLE_APP_EXPOSED_PORT=%d", deployport), fmt.Sprintf("SHUFFLE_APP_EXPOSED_PORT=%d", deployport),
fmt.Sprintf("SHUFFLE_SWARM_CONFIG=%s", os.Getenv("SHUFFLE_SWARM_CONFIG")), fmt.Sprintf("SHUFFLE_SWARM_CONFIG=%s", os.Getenv("SHUFFLE_SWARM_CONFIG")),
fmt.Sprintf("SHUFFLE_LOGS_DISABLED=%s", logsDisabled), fmt.Sprintf("SHUFFLE_LOGS_DISABLED=%s", logsDisabled),
fmt.Sprintf("CALLBACK_URL=%s", baseUrl),
fmt.Sprintf("BASE_URL=%s", appCallbackUrl),
}, },
Hosts: []string{ Hosts: []string{
containerName, containerName,
@@ -3736,9 +3840,10 @@ func findAppInfo(image, name string, redeploy bool) (int, error) {
// Remove the service and redeploy it. // Remove the service and redeploy it.
// There are cases where the service doesn't update properly // There are cases where the service doesn't update properly
// Check when the last update happened. If it was within the last few minutes, skip // Check when the last update happened. If it was within the last few minutes, skip
if int(time.Since(service.UpdatedAt).Seconds()) > 60 { updateAgeSeconds := int(time.Since(service.UpdatedAt).Seconds())
if updateAgeSeconds > 60 {
log.Printf("[INFO] Attempting redeploy of app %s with image %s since it is more than 10 minutes since last attempt with failure.", name, image) log.Printf("[INFO] Attempting redeploy of app %s with image %s since last update was %d seconds ago.", name, image, updateAgeSeconds)
err = dockercli.ServiceRemove( err = dockercli.ServiceRemove(
context.Background(), context.Background(),
@@ -3767,7 +3872,7 @@ func findAppInfo(image, name string, redeploy bool) (int, error) {
} }
} }
} else { } else {
//log.Printf("[INFO] NOT redeploying service %s since it was updated less than 3 minutes ago.", name) log.Printf("[INFO] Skipping redeploy of app %s since last update was %d seconds ago.", name, updateAgeSeconds)
} }
} }
@@ -4095,6 +4200,12 @@ func sendAppRequest(ctx context.Context, incomingUrl, appName string, port int,
log.Printf("[DEBUG] Setting client timeout to %d seconds for app request", timeoutInt) log.Printf("[DEBUG] Setting client timeout to %d seconds for app request", timeoutInt)
client.Timeout = time.Duration(timeoutInt) * time.Second client.Timeout = time.Duration(timeoutInt) * time.Second
} }
} else if len(os.Getenv("SHUFFLE_APP_SDK_TIMEOUT")) > 0 {
timeoutInt, err := strconv.Atoi(os.Getenv("SHUFFLE_APP_SDK_TIMEOUT"))
if err == nil && timeoutInt > 0 {
log.Printf("[DEBUG] Using SHUFFLE_APP_SDK_TIMEOUT=%d seconds for app request", timeoutInt)
client.Timeout = time.Duration(timeoutInt) * time.Second
}
} }
// Content type required // Content type required