Poll until ready
Many workflows wait for something outside AutoFlow to finish: a pipeline, a rollout, an external job. poll is the built-in for that. It re-invokes an action until a check over its result passes, and it does so without recording an attempt in the workflow’s history.
Signature
poll(action, check, interval, timeout,
args=None, kwargs=None, timeout_value=None)actionis a value of theactiontype: an action loaded from a module, or one produced byderived_action. A plain Starlark function is rejected.checkis a CEL expression, not Starlark, evaluated against the action’s returned value bound asret. It must yield a bool and is compiled whenpollis called, so a typo fails immediately.intervalandtimeoutare durations and must both be positive. The deadline is anchored to the workflow’s time, so it is stable across replays.argsandkwargsare passed to the action on every attempt.timeout_valueis what the future yields when the deadline passes without the check passing. The default isNone.
poll returns a future; gather it like any other, see Futures and gather.
Why not a sleep loop
A loop of call_api and sleep works, but every iteration records an action result and a timer in the workflow’s history. Two hours of polling every 30 seconds is 240 iterations and a history that every replay re-executes.
poll rides autocore’s retry path instead. A not-ready result re-schedules the same activity task after interval and writes nothing to history. Whether the check passes on the first attempt or the thousandth, the history holds one event. Only when the check passes, the deadline passes, or the action fails for good does an event land.
What counts as not ready
- The check evaluates to
false. - The action returns an error the module marked as retryable.
Everything else ends the poll: a check that evaluates to true resolves the future with the value; an error the module did not mark retryable fails the workflow; a transform that fails is terminal too. A failure of the invocation itself, such as the module being unreachable, is retried with a backoff from one second to thirty, up to five consecutive failures, still bounded by timeout; a not-ready result resets that count.
ret navigates scalars, lists, tuples, sets and dicts with int, string or bool keys. Proto messages, sensitive values and channels are not navigable.
Classifying with a derived action
call_api returns a 4-tuple and never marks anything retryable, so its raw result is awkward to poll: a 502 from GitLab would satisfy ret[0] != 200 and end the poll. A derived_action wraps the action in a pure Starlark transform that runs where the action runs and returns the value the check sees and the history records.
load("module:gitlab", "call_api")
def _pipeline_state(invoke, project_id, pipeline_id, headers):
status, _, body, err = invoke(
"GET",
"/api/v4/projects/%d/pipelines/%d" % (project_id, pipeline_id),
headers = headers,
)
if err != None or status == 429 or status >= 500:
return "transient", None
if status != 200:
return "error", "pipeline API returned %d" % status
pipeline = json.decode(str(body), default = None)
if pipeline == None:
return "error", "response is not JSON"
return pipeline["status"], pipeline
pipeline_probe = derived_action(call_api, transform = _pipeline_state)
def main(w, project_id, pipeline_id, token):
state, pipeline = gather(poll(
action = pipeline_probe,
check = "ret[0] in ['success', 'failed', 'canceled', " +
"'skipped', 'manual', 'error']",
kwargs = {
"project_id": project_id,
"pipeline_id": pipeline_id,
"headers": {"Authorization": "Bearer " + token},
},
interval = 30 * time.second,
timeout = 2 * time.hour,
timeout_value = ("timed_out", None),
))
if state != "success":
fail("pipeline ended in state %s: %s" % (state, pipeline))
return pipeline["web_url"]How it fits together:
- The transform’s first parameter,
invoke, performs the real call and returns the action’s value directly, not a future. It must be called exactly once. - The remaining parameters are whatever the caller of the derived action passes;
pollpasses itskwargs. - The transform returns a small tuple, so
ret[0]is the classification andret[1]the detail.timeout_valuemirrors that shape, somaindestructures either outcome the same way. - The check lists the states that end the wait. Anything else, including a pipeline still
runningand the"transient"a 429 or 5xx becomes, is not ready, so an unexpected intermediate state keeps polling rather than failing. A body that is not JSON yet becomes"error", which ends the poll and letsmainfail with a message. A transform must be total:json.decodewithdefaultkeeps it from failing on a half-written response. - The tuple is what history records. The full pipeline JSON is kept only in the final result, not per attempt.
Transform rules
A transform is a top-level def in the same file, with at least one ordinary positional parameter for invoke. It sees json, math, time, struct, module and the Starlark universe, and nothing else from its file: no constants, no helpers, no other actions, no gather. A transform’s source is at most 8 KiB, at most 8 transforms may stack on one action, and its output must fit in 256 KiB.
Sensitive values, such as the Authorization header above, pass through a transform opaquely. It can forward them to invoke but not read them.
Related
- Call the GitLab API for
call_apiitself. - Durable waits when you wait for time or for a value sent into the workflow rather than for a state.