Tasks¶
A task is one unit of work. It knows how to do the work, and how to report that the work is already done.
The contract¶
Two methods form the contract:
run()makes a change.check()reports whether the wanted end state is already true.
Winslow calls check() before it runs a task. If check() returns true, Winslow marks the task completed and
does not run it. The run() method is optional, and a task that omits it only reports a state. See
the introduction for the full explanation.
A task reads the config of its workflow¶
Winslow builds each task with the workflow config of the run. A task reads a value from
self.workflow_config, and reads the environment name from self.env.
class WriteReport(Task):
def run(self):
rows = self.fetch(limit=self.workflow_config.limit)
self.write(rows, region=self.workflow_config.region)
The values thus change between two runs of the same workflow. A task can read them in any method, which includes the gates below.
The order of the methods¶
Winslow calls the methods of a task in this order:
is_eligible(): is this task relevant for the current config?can_check(): may Winslow check the task now?check(): is the work already done? If it is, the task is complete andrun()does not happen.can_run(): may the task run now?run(): do the work.can_check()andcheck()again: confirm the work.
Steps 1, 2 and 4 are gates, and each one returns true by default. A simple task implements only check() and
run().
Winslow runs a task only after the tasks it depends on. See Dependencies for the order between tasks.
Control the outcome¶
Three gates decide what happens to a task. Each gate has a method that you override:
| Gate | The method | The question |
|---|---|---|
| Eligibility | is_eligible() |
Is this task relevant for the current config? |
| Checkability | can_check() |
May Winslow check the task now? |
| Runnability | can_run() |
May the task run now? |
A gate method returns true or false. It returns true by default:
Signal a decision with a reason¶
A gate returns true or false. A signal method instead attaches a reason to a stop, and records the message on the task log:
class Publish(Task):
def can_run(self):
self.block_if(self.workflow_config.maintenance, "a maintenance window is open")
return True
Each method has its own signal:
| The method | The signal | The status |
|---|---|---|
is_eligible() |
self.skip(msg) |
SKIPPED |
can_run() |
self.block(msg) |
BLOCKED |
can_check() |
self.block(msg) |
BLOCKED |
check() |
self.fail(msg) |
FAILED |
check() |
self.require_action(msg) |
ACTION REQUIRED |
A gate must return true when no signal fires
A signal method raises. The block_if form raises only when its condition is true, so a gate that uses it
falls through to the return True. A gate that returns nothing stops the task. Keep the return True
after a conditional signal.
The signal forms¶
The skip, block and fail signals have an _if form: skip_if, block_if and fail_if. It signals
only when the condition is true. The plain form signals every time, so call it inside your own logic:
class NightlyReport(Task):
def is_eligible(self):
if self.env not in ("staging", "prod"):
self.skip("this task runs in staging and production only")
return True
A signal belongs to one method
A signal that a method does not accept becomes an ERROR. The skip signal is for is_eligible, the
block signal is for can_run and can_check, and the fail signal is for check. A skip inside
can_run, or a fail inside run, thus errors the task.
Share a value between check and run¶
A transient_property computes a value once, and caches it for one execution of the task. The first check
and the run after it share the cache. The final check starts with an empty cache:
from winslow import Task, transient_property
class SyncCatalog(Task):
@transient_property
def remote_rows(self):
return fetch_remote_rows() # An expensive call.
def run(self):
write_rows(self.remote_rows)
def check(self):
return count_local_rows() == len(self.remote_rows)
The first check computes remote_rows, and run reads the same value. A second computation could disagree
with the check. The final check computes the value again, so the confirmation reads the state that the run
produced.
A complete example¶
The release workflow shows every gate in one flow. It builds an artifact, tests it, verifies the report, and
publishes it. Each gate reacts to the environment or to a config flag.
from pathlib import Path
from winslow import Workflow, Task, ConfigOption
STATE = Path("state")
ARTIFACT = STATE / "artifact"
REPORT = STATE / "report"
PUBLISHED = STATE / "published"
class Release(Workflow):
"""A small release flow. It shows the task gates next to the run and check
methods. Delete the state directory to run it again."""
maintenance = ConfigOption(
action="store_true", default=False, help_text="Block the publish task."
)
offline = ConfigOption(
action="store_true", default=False, help_text="Block the verify check."
)
class Build(Task):
def run(self):
STATE.mkdir(exist_ok=True)
ARTIFACT.write_text("built\n")
def check(self):
return ARTIFACT.exists()
class Test(Task):
dependencies = Build
def run(self):
REPORT.write_text("tests passed\n")
def check(self):
return REPORT.exists()
class Verify(Task):
"""A check-only task. It has no run method, so it only reports a state. The
can_check gate postpones the check when the service is offline."""
dependencies = Test
def can_check(self):
self.block_if(self.workflow_config.offline, "the service is offline")
return True
def check(self):
return REPORT.exists()
class Publish(Task):
"""Runs only in the production environment. The can_run gate blocks it during
a maintenance window."""
dependencies = Verify
def is_eligible(self):
return self.env == "prod"
def can_run(self):
self.block_if(self.workflow_config.maintenance, "a maintenance window is open")
return True
def run(self):
PUBLISHED.write_text("published\n")
def check(self):
return PUBLISHED.exists()
Run it headless with winslow run --mode headless --workflow release. Each variation shows one gate:
| The variation | The result |
|---|---|
The default, in the dev environment |
Build, test and verify complete. Publish is skipped, because the environment is not production. |
WINSLOW_ENV=prod |
Every task completes. |
WINSLOW_ENV=prod and --maintenance |
Publish is blocked, because a maintenance window is open. |
WINSLOW_ENV=prod and --offline |
Verify is blocked, and publish waits on it. |
Each task writes a marker into the state directory. Delete the directory to run the flow again.