Workflows¶
- class chancy.plugins.workflow.WorkflowPlugin(*, polling_interval=30, max_workflows_per_run=1000, pruning_rule=None, max_pending_workflows=10000)[source]¶
Bases:
PluginSupport for dependency-based workflows.
Workflows allow you to easily model complex processes that involve multiple jobs, each of which may depend on the completion of one or more other jobs. Workflows are modeled as a directed acyclic graph (DAG) where each node represents a job and each edge represents a dependency between two jobs.
Workflows are implemented on top of the existing Chancy job system, meaning you can use all the existing job features, such as job retries, timeouts, scheduling, and so on.
Note
This plugin is enabled by default, you only need to provide it in the list of plugins to customize its arguments or if
no_default_pluginsis set toTrue.Enable the plugin by adding it to the list of plugins in the Chancy constructor:
from chancy.plugins.leadership import Leadership from chancy.plugins.workflow import WorkflowPlugin async with Chancy( "postgresql://localhost/postgres", plugins=[WorkflowPlugin()], ) as chancy: ...
Example¶
We’ll create a simple workflow runs the “top” job first, then the “left” and “right” jobs in parallel, and finally the “bottom” job:
example_workflow.py¶import asyncio from chancy import Chancy, job from chancy.plugins.leadership import Leadership from chancy.plugins.workflow import Workflow, WorkflowPlugin @job() async def top(): print(f"Top") @job() async def left(): print(f"Left") @job() async def right(): print(f"Right") @job() async def bottom(): print(f"Bottom") async def main(): async with Chancy( "postgresql://localhost/postgres", plugins=[Leadership(), WorkflowPlugin()] ) as chancy: workflow = ( Workflow("example") .add("top", top) .add("left", left, ["top"]) .add("right", right, ["top"]) .add("bottom", bottom, ["left", "right"]) ) await WorkflowPlugin.push(chancy, workflow) if __name__ == "__main__": asyncio.run(main())
If we visualize our newly created workflow using
generate_dot(), we get:![digraph "example" {
rankdir=TB;
node [shape=box, style="rounded,filled", fontname="Arial"];
"top" [label="top\n(pending)", fillcolor=lightblue];
"left" [label="left\n(pending)", fillcolor=lightblue];
"right" [label="right\n(pending)", fillcolor=lightblue];
"bottom" [label="bottom\n(pending)", fillcolor=lightblue];
"top" -> "left";
"top" -> "right";
"left" -> "bottom";
"right" -> "bottom";
labelloc="t";
label="Workflow: example\nState: pending";
}](_images/graphviz-4e52b05e9f5dc3c22456f2f87854a82f6146979b.png)
The full Workflow API is a little verbose if you just want to run a series of jobs in a specific order. In that case, you can use the
Sequenceclass to create a workflow from a list of jobs.Django Integration¶
This plugin can be made available to the Django ORM and Admin interface.
To enable this, you need to add the following to your Django settings:
INSTALLED_APPS = [ ..., "chancy.plugins.workflow.django", ]
You can then query the workflows and steps using the Django ORM:
from chancy.plugins.workflow.django.models import ( Workflow, WorkflowStep ) workflow = Workflow.objects.get(id="...") completed_steps = WorkflowStep.objects.filter( workflow=workflow, state=QueuedJob.State.SUCCEEDED )
- param polling_interval:
The interval between recovery polls. Notification traffic does not reset this interval.
- param max_workflows_per_run:
The maximum number of workflows to process in one polling or notification batch. Recovery polls continue where the previous batch left off.
- param max_pending_workflows:
The maximum number of distinct workflows awaiting event-driven processing. Further notifications rely on periodic polling.
- api_plugin()[source]¶
If this plugin has an associated API component, returns the import string for the plugin.
- Return type:
str | None
- async cleanup(chancy)[source]¶
Clean up any resources used by the plugin.
Should return either None, if no work was done, or the number of rows cleaned up.
Note
Normally, you don’t need to call this yourself. The Pruner plugin will call the cleanup method of all other registered plugins.
- async classmethod fetch_workflow_ex(cursor, chancy, id_)[source]¶
Fetch a single workflow from the database.
This method is a lower-level version of fetch_workflow that accepts an existing cursor object, allowing it to be used in transactions.
- async classmethod fetch_workflows(chancy, *, states=None, ids=None, limit=100)[source]¶
Fetch workflows from the database, optionally matching the given conditions.
- async static fetch_workflows_ex(cursor, chancy, *, states=None, ids=None, limit=100)[source]¶
Fetch workflows from the database, optionally matching the given conditions.
This method is a lower-level version of fetch_workflows that accepts an existing cursor object, allowing it to be used in transactions.
- Parameters:
- Returns:
A list of workflows.
- Return type:
- static get_dependencies()[source]¶
Get the identifiers of all plugins this plugin depends on, if any.
Plugins that depend on the presence of other plugins will refuse to start if those dependencies are not met.
- static get_identifier()[source]¶
Returns a unique identifier for this plugin.
This identifier should be unique across all active plugins. If a custom plugin provides compatible functionality to a built-in plugin, it may use the same identifier as the built-in plugin.
- Return type:
- migrate_package()[source]¶
Get the package name that contains the migration scripts for this plugin, if it has any.
- Return type:
- async on_jobs_updated_in_transaction(*, worker, jobs, cursor)[source]¶
Called after a batch’s SQL updates, before its transaction commits.
Use the supplied cursor for database work that must commit atomically with the job updates. Raising an exception rolls back the batch. This hook may run again on retry; avoid external or in-memory side effects. Use
on_job_updated()for work that requires committed updates.- Parameters:
worker (Worker) – The worker saving the updates.
jobs (Sequence[QueuedJob]) – Only updates whose claims matched and whose database rows were updated, in their original order, including repeated accepted updates to the same job. Rejected updates are omitted; this hook is not called if none were accepted.
cursor (AsyncCursor[dict[str, Any]]) – The cursor for the current update transaction.
- async poll(worker, chancy, cursor, *, changes=None)[source]¶
Process the next batch of up to
max_workflows_per_runpending or running workflows.Workflows are taken in ID order within a sweep whose upper bound is fixed at its start. Newer workflows join the next sweep, so arrivals cannot indefinitely postpone revisiting older workflows. A batch stops at the end of its sweep even if it has room for more workflows.
- async process_pending(worker, chancy, cursor, *, changes=None)[source]¶
Process one batch of notified workflows using the caller’s transaction.
Duplicate notifications are coalesced until their batch starts. Locked workflows and dropped hints are recovered by periodic polling.
- async static process_workflow(cursor, chancy, workflow, worker)[source]¶
Process a single iteration of the given workflow, progressing the state of each step and the overall workflow as necessary.
- Parameters:
cursor (AsyncCursor) – The cursor to use for the query.
chancy (Chancy) – The Chancy application.
workflow (Workflow) – The workflow to process.
worker (Worker) – The worker processing the workflow.
- Returns:
True if the workflow was updated, False otherwise.
- Return type:
- async classmethod push(chancy, workflow)[source]¶
Push new workflow to the database.
If the workflow already exists in the database, it will be updated instead.
- async classmethod push_ex(cursor, chancy, workflow, worker=None)[source]¶
Push new workflow to the database.
This method is a lower-level version of push that accepts an existing cursor object, allowing it to be used in transactions.
If the workflow already exists in the database, it will be updated instead.
- Parameters:
- Returns:
The UUID of the newly created workflow.
- Raises:
EmptyWorkflowError – If the workflow has no steps.
CircularDependencyError – If a circular dependency is detected.
InvalidDependencyError – If a dependency references a non-existent step.
- Return type:
- async run(worker, chancy)[source]¶
Runs the plugin.
This function can and should run indefinitely, as it will be cancelled when the worker is stopped.
- async classmethod wait_for_workflow(chancy, workflow_id, *, interval=1, timeout=None)[source]¶
Wait for a workflow to complete.
This method will loop until the workflow referenced by the provided ID has completed. The interval parameter controls how often the workflow status is checked. This will not block the event loop, so other tasks can run while waiting for the workflow to complete.
Example¶
workflow = Workflow("example") workflow.add("step1", job1) workflow.add("step2", job2, ["step1"]) workflow_id = await WorkflowPlugin.push(chancy, workflow) completed_workflow = await WorkflowPlugin.wait_for_workflow( chancy, workflow_id, timeout=300 # 5 minute timeout )
- param chancy:
The Chancy application.
- param workflow_id:
The ID of the workflow to wait for.
- param interval:
The number of seconds to wait between checks.
- param timeout:
The maximum number of seconds to wait for the workflow to complete. If not provided, the method will wait indefinitely.
- raises asyncio.TimeoutError:
If the timeout is reached before the workflow completes.
- raises KeyError:
If the workflow does not exist.
- return:
The completed Workflow object.
- class chancy.plugins.workflow.Workflow(name, steps=<factory>, state=State.PENDING, id=<factory>, created_at=None, updated_at=None)[source]¶
Bases:
objectA collection of jobs with dependencies between steps.
Define steps using
add()oradd_group(), then submit the workflow usingWorkflowPlugin.push(). Each step is queued once all of its dependencies have succeeded. Steps without dependencies can run in parallel.For jobs that should run one after another, use
Sequence.- Parameters:
- add(step_id, job, dependencies=None)[source]¶
Add a step to the workflow.
workflow.add("step_1", job) workflow.add("step_2", job, ["step_1"])
- add_group(jobs, dependencies=None)[source]¶
Add a group of steps to the workflow.
This is a convenience method for adding multiple steps to the workflow at once that are all dependent on the same set of dependencies.
workflow = Workflow("my_workflow") workflow.add("setup", setup_job) workflow.add_group([ ("step_1", job_1), ("step_2", job_2), ("step_3", job_3), ], ["setup"])
- steps: dict[str, WorkflowStep][source]¶
A dictionary of steps in the workflow, keyed by step ID.
- validate()[source]¶
Validate the workflow’s dependency graph.
This method checks for: - Empty workflows (no steps) - Circular dependencies (cycles in the dependency graph) - Invalid dependencies (references to non-existent steps)
- Raises:
EmptyWorkflowError – If the workflow has no steps.
CircularDependencyError – If a circular dependency is detected.
InvalidDependencyError – If a dependency references a non-existent step.
- Return type:
None
- class chancy.plugins.workflow.WorkflowStep(job: chancy.job.Job | chancy.job.IsAJob, step_id: str, dependencies: list[str] = <factory>, state: chancy.job.QueuedJob.State | None = <State.PENDING: 'pending'>, job_id: str | None = None)[source]¶
Bases:
object- Parameters:
- class chancy.plugins.workflow.Sequence(name, jobs=None)[source]¶
Bases:
objectA sequential workflow.
Sequences are a special case of workflows, where each step depends on the previous step. This forms a linear chain of jobs that are executed in order.
Sequences are useful for defining sequences of jobs that must be executed in order, without the complexity of full workflows.
Example¶
example_sequence.py¶import asyncio from chancy import Chancy, job from chancy.plugins.workflow import Sequence @job() async def first(): print("First") @job() async def second(): print("Second") @job() async def third(): print("Third") async def main(): async with Chancy("postgresql://localhost/postgres") as chancy: sequence = Sequence("example_workflow", [first, second, third]) await sequence.push(chancy) if __name__ == "__main__": asyncio.run(main())
- class chancy.plugins.workflow.api.WorkflowApiPlugin(api)[source]¶
Bases:
ApiPluginAPI plugin for workflows.
- async static get_workflows(request, *, chancy, worker)[source]¶
List workflows in descending ID order with optional filtering.
Pass the last returned ID as
beforeto fetch the next page of lower IDs with the same filters. Creation and update times do not affect this order. Pages are live results, not a snapshot: workflows may be removed or stop matching filters between requests.Set
pagination=trueto receive an object withitems,has_more, andnext_cursor(an ID to pass asbefore, or null on the last page). Otherwise the response remains an array.limitmust be positive and is capped at 100.total_stepscounts every workflow step.waiting_stepscounts steps not yet submitted (no job ID), including dependency-blocked steps.pending_stepscounts jobs already queued in pending state. A step whose job was deleted still contributes tototal_stepsbut is not counted as waiting or in any job state.