Sign in to run and submit your work
Reading is open to everyone. Running code and saving drafts need an account so your work is yours and comes back on your next visit.
or
CODE WORKSPACE
A loader talks to a flaky sink. Retrying is the right answer for a timeout and the wrong answer for a rejection, and the wait between attempts has to grow so a struggling service is not hammered.
Write run_with_retry(tasks, policy). Implement the retry as a decorator on an inner function called attempt_task, and report the log plus the decorated function's name.
Function to write
run_with_retry(tasks: list[dict], policy: dict) -> dictA dict with the per-task retry `log` and the decorated function's `wrapped_name`.
How to approach it
Get the delays right before the decorator. Two attempts have one gap.
Sample cases
+ 2 held back until you submit
two timeouts then success
Three attempts, two delays that double, and the identity of the wrapped function preserved.
Input
Argument 1
[
{
'name': 'load-eu',
'outcomes': [
'timeout',
'timeout',
'ok'
]
}
]Argument 2
{
'max_attempts': 3,
'base_delay': 1,
'max_delay': 4,
'retry_on': [
'timeout'
]
}Returns
{
'log': [
{
'name': 'load-eu',
'outcome': 'ok',
'attempts': 3,
'delays': [
1,
2
]
}
],
'wrapped_name': 'attempt_task'
}an error that cannot succeed
A non-retryable outcome stops after one attempt — retrying it would be three times the wait for the same answer.
Input
Argument 1
[
{
'name': 'load-us',
'outcomes': [
'forbidden'
]
}
]Argument 2
{
'max_attempts': 3,
'base_delay': 1,
'max_delay': 4,
'retry_on': [
'timeout'
]
}Returns
{
'log': [
{
'name': 'load-us',
'outcome': 'forbidden',
'attempts': 1,
'delays': []
}
],
'wrapped_name': 'attempt_task'
}nothing to run
No tasks means an empty log, and the wrapped name is still reported.
Input
Argument 1
[] (empty list)
Argument 2
{
'max_attempts': 3,
'base_delay': 1,
'max_delay': 4,
'retry_on': [
'timeout'
]
}Returns
{
'log': [],
'wrapped_name': 'attempt_task'
}Constraints
{'log': [...], 'wrapped_name': ...}. wrapped_name is attempt_task.__name__ after decoration — if it comes back as wrapper, the decorator lost the identity.{'name': ..., 'outcome': ..., 'attempts': ..., 'delays': [...]}.attempt_task(task, attempt) returns task['outcomes'][attempt], or ok once the list runs out.ok. Stop immediately on an outcome that is not in retry_on — a rejection does not become an acceptance by being asked again.base_delay * 2 ** (N - 1), capped at max_delay when one is set. There is no delay after the final attempt.attempts counts attempts actually made, so it is never more than max_attempts.Worked example
Task load-eu returns timeout, timeout, then ok under a policy of three attempts, base delay 1 and cap 4.
The result is three attempts and two delays — [1, 2]. Two, not three: a delay exists to separate two attempts, and after the last one there is nothing to separate. Getting that off-by-one wrong is what makes a job that fails fast take an extra minute per task to admit it.
What this tests
That a decorator returns a replacement function and has to preserve the identity of the one it wraps, and that a retry policy is three separate decisions — what to retry, how many times, and how long to wait — rather than one number.
run_with_retry(tasks: list[dict], policy: dict) -> dictSubmit for review to find out what your query gets right, what it gets wrong, and how it compares with the best working query for this exercise.
This scenario runs a full workspace — editor, canvas and results side by side. It needs a laptop or desktop to be usable. Open this page on a bigger screen to start building.