mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-10 08:07:03 +00:00
feat: retry a workflow-as-code task from its task options (#11013)
* feat: retry a workflow-as-code task from its task options Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WvjgKMRNtRNPnAkg6MkiTA * fix: claim every retry attempt key up front, so a step cannot alias one Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WvjgKMRNtRNPnAkg6MkiTA * fix: bound retry attempts, which now claim their keys up front Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WvjgKMRNtRNPnAkg6MkiTA * fix: honour an explicit zero retry multiplier in the python client Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WvjgKMRNtRNPnAkg6MkiTA * docs: state the retry validation rules once in the task docstring Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WvjgKMRNtRNPnAkg6MkiTA --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
33f9828c3e
commit
d3f305db98
Generated
+84
-7
@@ -4580,6 +4580,22 @@ def parse_sql_client_name(name: str) -> tuple[str, Optional[str]]
|
||||
# decoded back before the caller sees it: a \`\`datetime\`\` comes back as a
|
||||
# string, a tuple as a list.
|
||||
#
|
||||
# \`\`retry\`\` re-dispatches the task after a failure, inside \`\`@workflow\`\` only.
|
||||
# Every attempt is a step of its own (\`\`call_api\`\`, \`\`call_api#2\`\`, ...) and
|
||||
# the wait between two of them is a durable sleep, so a retrying task holds no
|
||||
# worker while it backs off. Keys: \`\`attempts\`\` (retries after the first
|
||||
# failure, a whole number from 0 to 100), \`\`delay\`\` (seconds before the first
|
||||
# retry, sub-second delays dropped), \`\`multiplier\`\` (applied to the delay
|
||||
# after each attempt, 1 keeps it constant), \`\`max_delay\`\` (ceiling in
|
||||
# seconds). \`\`attempts\`\` is required, and an out-of-range or unknown key is
|
||||
# rejected where the policy is written.
|
||||
#
|
||||
# A workflow sleeps once per round, so tasks backing off in the same fan-out
|
||||
# wait one after another rather than together: the delay before a fan-out
|
||||
# retries is the sum of every backoff pending in it, not the longest one, and
|
||||
# it grows with both the width of the fan-out and \`\`attempts\`\`. Retries with
|
||||
# no \`\`delay\`\` all go out in a single round.
|
||||
#
|
||||
# Usage::
|
||||
#
|
||||
# @task
|
||||
@@ -4587,10 +4603,15 @@ def parse_sql_client_name(name: str) -> tuple[str, Optional[str]]
|
||||
#
|
||||
# @task(path="f/external_script", timeout=600, tag="gpu")
|
||||
# async def run_external(x: int): ...
|
||||
def task(_func = None, path: Optional[str] = None, tag: Optional[str] = None, timeout: Optional[int] = None, cache_ttl: Optional[int] = None, priority: Optional[int] = None, concurrency_limit: Optional[int] = None, concurrency_key: Optional[str] = None, concurrency_time_window_s: Optional[int] = None)
|
||||
#
|
||||
# @task(retry={"attempts": 3, "delay": 30, "multiplier": 2})
|
||||
# async def call_api(payload: dict): ...
|
||||
def task(_func = None, path: Optional[str] = None, tag: Optional[str] = None, timeout: Optional[int] = None, cache_ttl: Optional[int] = None, priority: Optional[int] = None, concurrency_limit: Optional[int] = None, concurrency_key: Optional[str] = None, concurrency_time_window_s: Optional[int] = None, retry: Optional[dict] = None)
|
||||
|
||||
# Create a task that dispatches to a separate Windmill script.
|
||||
#
|
||||
# \`\`retry\`\` takes the same policy as :func:\`task\`.
|
||||
#
|
||||
# Usage::
|
||||
#
|
||||
# extract = task_script("f/data/extract", timeout=600)
|
||||
@@ -4598,10 +4619,12 @@ def task(_func = None, path: Optional[str] = None, tag: Optional[str] = None, ti
|
||||
# @workflow
|
||||
# async def main():
|
||||
# data = await extract(url="https://...")
|
||||
def task_script(path: str, timeout: Optional[int] = None, tag: Optional[str] = None, cache_ttl: Optional[int] = None, priority: Optional[int] = None, concurrency_limit: Optional[int] = None, concurrency_key: Optional[str] = None, concurrency_time_window_s: Optional[int] = None)
|
||||
def task_script(path: str, timeout: Optional[int] = None, tag: Optional[str] = None, cache_ttl: Optional[int] = None, priority: Optional[int] = None, concurrency_limit: Optional[int] = None, concurrency_key: Optional[str] = None, concurrency_time_window_s: Optional[int] = None, retry: Optional[dict] = None)
|
||||
|
||||
# Create a task that dispatches to a separate Windmill flow.
|
||||
#
|
||||
# \`\`retry\`\` takes the same policy as :func:\`task\`.
|
||||
#
|
||||
# Usage::
|
||||
#
|
||||
# pipeline = task_flow("f/etl/pipeline", priority=10)
|
||||
@@ -4609,7 +4632,7 @@ def task_script(path: str, timeout: Optional[int] = None, tag: Optional[str] = N
|
||||
# @workflow
|
||||
# async def main():
|
||||
# result = await pipeline(input=data)
|
||||
def task_flow(path: str, timeout: Optional[int] = None, tag: Optional[str] = None, cache_ttl: Optional[int] = None, priority: Optional[int] = None, concurrency_limit: Optional[int] = None, concurrency_key: Optional[str] = None, concurrency_time_window_s: Optional[int] = None)
|
||||
def task_flow(path: str, timeout: Optional[int] = None, tag: Optional[str] = None, cache_ttl: Optional[int] = None, priority: Optional[int] = None, concurrency_limit: Optional[int] = None, concurrency_key: Optional[str] = None, concurrency_time_window_s: Optional[int] = None, retry: Optional[dict] = None)
|
||||
|
||||
# Decorator marking an async function as a workflow-as-code entry point.
|
||||
#
|
||||
@@ -6657,6 +6680,34 @@ A caught failure reads the same whether it came from a task or from a \`step()\`
|
||||
Import: \`import { workflow, task, taskScript, taskFlow, step, sleep, waitForApproval, getApprovalUrls, getResumeUrls, parallel } from "windmill-client"\`
|
||||
|
||||
\`\`\`typescript
|
||||
/**
|
||||
* Re-dispatch policy for a failed task.
|
||||
*
|
||||
* Every attempt is a step of its own (\`fetch\`, \`fetch#2\`, \`fetch#3\`), and the
|
||||
* wait between two of them is a durable sleep, so a retrying task holds no
|
||||
* worker while it backs off.
|
||||
*
|
||||
* A workflow sleeps once per round, so tasks backing off in the same fan-out
|
||||
* wait one after another rather than together: the delay before a fan-out
|
||||
* retries is the sum of every backoff pending in it, not the longest one, and
|
||||
* it grows with both the width of the fan-out and \`attempts\`. Retries with no
|
||||
* \`delay\` all go out in a single round.
|
||||
*/
|
||||
export interface TaskRetry {
|
||||
/** Attempts after the first failure: \`2\` runs the task at most 3 times.
|
||||
* A whole number from 0 to 100; anything else is rejected where the policy
|
||||
* is written. */
|
||||
attempts: number;
|
||||
/** Seconds to wait before the first retry. Default 0, retry immediately.
|
||||
* Sub-second delays are dropped — a durable sleep resolves to the second. */
|
||||
delay?: number;
|
||||
/** Applied to the delay after each attempt: 1 (the default) keeps it
|
||||
* constant, 2 doubles it. */
|
||||
multiplier?: number;
|
||||
/** Ceiling for the delay in seconds, for a \`multiplier\` above 1. */
|
||||
max_delay?: number;
|
||||
}
|
||||
|
||||
export interface TaskOptions {
|
||||
timeout?: number;
|
||||
tag?: string;
|
||||
@@ -6665,6 +6716,7 @@ export interface TaskOptions {
|
||||
concurrency_limit?: number;
|
||||
concurrency_key?: string;
|
||||
concurrency_time_window_s?: number;
|
||||
retry?: TaskRetry;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -6682,9 +6734,11 @@ export async function getResumeUrls(approver?: string, flowLevel?: boolean): Pro
|
||||
* @example
|
||||
* const extract_data = task(async (url: string) => { ... });
|
||||
* const run_external = task("f/external_script", async (x: number) => { ... });
|
||||
* const call_api = task(fetchOrders, { retry: { attempts: 3, delay: 30, multiplier: 2 } });
|
||||
*
|
||||
* Inside a \`workflow()\`, calling a task dispatches it as a step.
|
||||
* Outside a workflow, the function body executes directly.
|
||||
* Outside a workflow, the function body executes directly and
|
||||
* {@link TaskOptions} — retry included — does not apply.
|
||||
*
|
||||
* A task runs as its own job, so its result is always encoded as JSON and
|
||||
* decoded back before the caller sees it: a \`Date\` comes back as a string, a
|
||||
@@ -6828,6 +6882,22 @@ def get_resume_urls(approver: str = None, flow_level: bool = None) -> dict
|
||||
# decoded back before the caller sees it: a \`\`datetime\`\` comes back as a
|
||||
# string, a tuple as a list.
|
||||
#
|
||||
# \`\`retry\`\` re-dispatches the task after a failure, inside \`\`@workflow\`\` only.
|
||||
# Every attempt is a step of its own (\`\`call_api\`\`, \`\`call_api#2\`\`, ...) and
|
||||
# the wait between two of them is a durable sleep, so a retrying task holds no
|
||||
# worker while it backs off. Keys: \`\`attempts\`\` (retries after the first
|
||||
# failure, a whole number from 0 to 100), \`\`delay\`\` (seconds before the first
|
||||
# retry, sub-second delays dropped), \`\`multiplier\`\` (applied to the delay
|
||||
# after each attempt, 1 keeps it constant), \`\`max_delay\`\` (ceiling in
|
||||
# seconds). \`\`attempts\`\` is required, and an out-of-range or unknown key is
|
||||
# rejected where the policy is written.
|
||||
#
|
||||
# A workflow sleeps once per round, so tasks backing off in the same fan-out
|
||||
# wait one after another rather than together: the delay before a fan-out
|
||||
# retries is the sum of every backoff pending in it, not the longest one, and
|
||||
# it grows with both the width of the fan-out and \`\`attempts\`\`. Retries with
|
||||
# no \`\`delay\`\` all go out in a single round.
|
||||
#
|
||||
# Usage::
|
||||
#
|
||||
# @task
|
||||
@@ -6835,10 +6905,15 @@ def get_resume_urls(approver: str = None, flow_level: bool = None) -> dict
|
||||
#
|
||||
# @task(path="f/external_script", timeout=600, tag="gpu")
|
||||
# async def run_external(x: int): ...
|
||||
def task(_func = None, *, path: Optional[str] = None, tag: Optional[str] = None, timeout: Optional[int] = None, cache_ttl: Optional[int] = None, priority: Optional[int] = None, concurrency_limit: Optional[int] = None, concurrency_key: Optional[str] = None, concurrency_time_window_s: Optional[int] = None)
|
||||
#
|
||||
# @task(retry={"attempts": 3, "delay": 30, "multiplier": 2})
|
||||
# async def call_api(payload: dict): ...
|
||||
def task(_func = None, *, path: Optional[str] = None, tag: Optional[str] = None, timeout: Optional[int] = None, cache_ttl: Optional[int] = None, priority: Optional[int] = None, concurrency_limit: Optional[int] = None, concurrency_key: Optional[str] = None, concurrency_time_window_s: Optional[int] = None, retry: Optional[dict] = None)
|
||||
|
||||
# Create a task that dispatches to a separate Windmill script.
|
||||
#
|
||||
# \`\`retry\`\` takes the same policy as :func:\`task\`.
|
||||
#
|
||||
# Usage::
|
||||
#
|
||||
# extract = task_script("f/data/extract", timeout=600)
|
||||
@@ -6846,10 +6921,12 @@ def task(_func = None, *, path: Optional[str] = None, tag: Optional[str] = None,
|
||||
# @workflow
|
||||
# async def main():
|
||||
# data = await extract(url="https://...")
|
||||
def task_script(path: str, *, timeout: Optional[int] = None, tag: Optional[str] = None, cache_ttl: Optional[int] = None, priority: Optional[int] = None, concurrency_limit: Optional[int] = None, concurrency_key: Optional[str] = None, concurrency_time_window_s: Optional[int] = None)
|
||||
def task_script(path: str, *, timeout: Optional[int] = None, tag: Optional[str] = None, cache_ttl: Optional[int] = None, priority: Optional[int] = None, concurrency_limit: Optional[int] = None, concurrency_key: Optional[str] = None, concurrency_time_window_s: Optional[int] = None, retry: Optional[dict] = None)
|
||||
|
||||
# Create a task that dispatches to a separate Windmill flow.
|
||||
#
|
||||
# \`\`retry\`\` takes the same policy as :func:\`task\`.
|
||||
#
|
||||
# Usage::
|
||||
#
|
||||
# pipeline = task_flow("f/etl/pipeline", priority=10)
|
||||
@@ -6857,7 +6934,7 @@ def task_script(path: str, *, timeout: Optional[int] = None, tag: Optional[str]
|
||||
# @workflow
|
||||
# async def main():
|
||||
# result = await pipeline(input=data)
|
||||
def task_flow(path: str, *, timeout: Optional[int] = None, tag: Optional[str] = None, cache_ttl: Optional[int] = None, priority: Optional[int] = None, concurrency_limit: Optional[int] = None, concurrency_key: Optional[str] = None, concurrency_time_window_s: Optional[int] = None)
|
||||
def task_flow(path: str, *, timeout: Optional[int] = None, tag: Optional[str] = None, cache_ttl: Optional[int] = None, priority: Optional[int] = None, concurrency_limit: Optional[int] = None, concurrency_key: Optional[str] = None, concurrency_time_window_s: Optional[int] = None, retry: Optional[dict] = None)
|
||||
|
||||
# Decorator marking an async function as a workflow-as-code entry point.
|
||||
#
|
||||
|
||||
Reference in New Issue
Block a user