Changelog¶
All notable changes to RADICAL AsyncFlow will be documented in this file.
The format is based on Keep a Changelog.
[Unreleased]¶
Fixed¶
- A component's uid is set when the call returns — calling a task or block returned its future before the engine assigned the component's uid, so
future.iddid not exist until the next event-loop step. A caller that read it right after the call got anAttributeError, and could not key its own records on the uid that telemetry events and spans carry astask_id. The uid is now assigned at the call, so uids also follow call order. A call whose registration fails keeps its uid on the future, which carries the exception. - Telemetry names the backend that ran each task — with several backends, every task span carried the default backend in its
backendattribute, even for a task routed elsewhere withbackend=. So did each task'sTaskCreated,asyncflow.TaskResolved,TaskStarted,TaskCompleted,TaskFailedandTaskCanceledevents, and the task metrics labelled from them; onlyTaskQueuedandTaskSubmittednamed the right backend. Every task event now names the backend the task is routed to, and the task span takes that name fromTaskCreated. "Thebackendattribute" indocs/telemetry.mdsays what the attribute means. - Shutting down one engine no longer makes another reuse uids (#85) — component uids came from one module-level counter that every
WorkflowEngine.shutdown()reset, so once one engine shut down, another engine still running in the same process numbered its next componenttask.000001again. That component replaced the earlier one inflow.components, and its future never completed. The counter is no longer reset, so a uid names one component across all engines in a process, and an engine created after another has shut down continues the numbering instead of starting again from000001. Thereset_uid_counter()helper inradical.asyncflow.utilsis removed.
Added¶
future.uid— the full uid ("task.000001","block.000002"), set when the call returns, beside the suffix infuture.id. It is the component's key inflow.componentsand thetask_idof its telemetry events and, for a task, of its span.- Task identity and trace linking documentation — "Task identity" in
docs/async_workflows.md, and "Linking tasks to your own traces" indocs/telemetry.md: the span that is current when a task is called, in-process or extracted from a W3Ctraceparentcarrier, becomes the task span's parent.
[0.6.0] 2026-10-19¶
Fixed¶
-
A
CancelledErrorinside a block body no longer hangs the block —execute_blockcaught onlyException, so when a block body raisedasyncio.CancelledError(e.g. by awaiting a member task that was cancelled) the block's future was never resolved, and the block and every component depending on it waited forever. The block is now cancelled, like a block cancelled from outside: its still-running members and its dependents are cancelled too. -
Explicit file dependencies no longer shut the engine down — an
OutputFile→InputFilematch made the engine calllink_explicit_data_depson every backend and read a"target"from its return value, but only few backends returns one, so on all other backends the resultingTypeErrorshut the whole engine down. The data-linking hooks are now called only on backends that declare the matching capability (supports_implicit_data_linking/supports_explicit_data_linking, read once when the backend is attached). The built-in backends declare neither, and backends without the attributes are treated the same way. RADICAL-Pilot data linking requires RHAPSODY newer than 0.6.0, which declares the capabilities. -
Futures passed as keyword arguments are now dependencies —
task(x=other())previously ignoredotherwhen building the dependency graph, so the task could start beforeotherfinished and fail withInvalidStateError, a failedothersurfaced as its raw exception instead ofDependencyFailureError, and a cancelledotherstopped the engine's scheduler loop silently. Keyword-argument futures are now tracked exactly like positional ones, and a cancelled argument future cancels the dependent task instead of stopping the scheduler. - Same future passed more than once no longer hangs (#98) —
task(a, a)countedaas two dependencies but released only one, so the task never ran. A component passed several times is now a single dependency, and every argument still receives its result.
Changed¶
- Component state handlers renamed —
WorkflowEngine.handle_task_success,handle_task_failureandhandle_task_cancellationare nowhandle_component_success,handle_component_failureandhandle_component_cancellation. They handle blocks as well as tasks:execute_blockandshutdown()now route block results, failures and cancellations through them, so every component changes state in one place. The old names are removed.
Added¶
- Task Dependencies documentation page (
docs/dependencies.md) — how to specify dependencies (positional and keyword arguments) and exactly what counts as one.
[0.5.1] - 2026-08-20¶
Fixed¶
- Engine creation on a non-main-thread event loop —
WorkflowEngine.create()no longer crashes when the event loop it runs on is started from a secondary thread. Signal handlers (SIGHUP/SIGTERM/SIGINT) can only be installed from the main thread;_setup_signal_handlers()now detects this case and skips registration with a warning instead of raising, leaving shutdown to be managed by the host process.
Added¶
- Conda recipe — new
recipe/meta.yamlfor a conda-forgestaged-recipessubmission (noarch: python). Python floor set to 3.10, matching conda-forge's floor and avoiding a CPython 3.9 bug whereimportlib.metadatadoesn't normalize dots in distribution names, which breaksversion("radical.asyncflow")at import time. - ReadTheDocs configuration —
.readthedocs.yamlbuilds the mkdocs site on Python 3.12, installing the.[doc]extra.
[0.5.0] - 2026-06-16¶
Added¶
-
future.stateattribute — every component future (task and block) now exposes a.statestring attribute tracking its full lifecycle:PENDING→RUNNING→DONE/FAILED/CANCELLED. Block futures now correctly transition through all states; previously they remained atPENDINGfor the duration of their execution. Readable at any point without awaiting. -
workflow_id=call-time kwarg — any task or block call now acceptsworkflow_id="<id>"as a keyword argument to tag that specific component. Takes precedence over any activeworkflow_scope(). The kwarg is stripped before the function body is invoked.
Fixed¶
-
shutdown()no longer crashes when blocks are still running — block futures are now cancelled directly instead of routing throughhandle_task_cancellation, which assumedoriginal_cancelwas set (only true for task futures). -
Block future state lifecycle — block futures now transition to
RUNNINGimmediately afterasyncio.create_task, toDONEon normal completion, and toFAILEDwhen the block body raises. Previously all three transitions were missing. -
_block_memberscleanup on non-cancelled outcomes — member sets are now removed from_block_membersfor every terminal outcome (DONE, FAILED, CANCELLED). Previously the cleanup only ran on cancellation, leaving stale entries after normal or failed block completion. -
patched_cancelforwardsmsgargument —fut.cancel(msg=...)now correctly forwards all positional and keyword arguments to the underlyingasyncio.Future.cancel(). Previously themsgwas silently dropped, breaking callers that rely on the cancellation message being propagated throughCancelledError. -
Pending-task cancellation sets
future.state— when a pending task is cancelled locally viapatched_cancel,future.stateis now set to"CANCELLED". All other cancellation paths already set this attribute; this was the only missing case. -
_clear_internal_recordscompleteness —resolved,running,_task_submit_times, and_task_start_timesare now cleared alongside the other internal structures. Previously these accumulated stale entries across engine reuse aftershutdown(). -
_block_asyncio_taskscleared on shutdown —_clear_internal_recordsnow also clears the asyncio.Task registry for blocks, eliminating stale references after engine reset.
Changed¶
self.runningchanged fromlisttoset— membership check (uid in self.running) and removal (running.discard) in the run loop andpatched_cancelare now O(1) instead of O(n). No public API change.
[0.4.0] - 2026-05-18¶
Added¶
-
WorkflowEngine.workflow_scope(workflow_id=None)— new async context manager that groups all tasks registered inside under a sharedasyncflow.workflow_id. Internally sets_workflow_id_ctx(a module-levelContextVar), which asyncio copies into everycreate_task/gatherbranch automatically so concurrent workflow instances remain isolated. When telemetry is active, also opens an OTel"workflow"span, making all task spans inside structural children in the trace hierarchy. Auto-generates a short UUID ifworkflow_idisNone. -
_workflow_id_ctxContextVar — per-instanceContextVar(defaultNone) that carries the active workflow ID across asyncio task boundaries without explicit argument passing. EachWorkflowEngineinstance owns its own uniquely-named ContextVar (asyncflow_workflow_id.<uid>), preventing context leakage when multiple engines are used within the same coroutine.@flow.blockexecution sets it to the block's UID so tasks inside a block inherit the workflow ID automatically. -
WorkflowEngine.start_telemetry()new parameters —span_processors,metric_readers,resource— forwarded toTelemetryManager.__init__(), mirroringSession.start_telemetry(). Enables passing pre-built OTel exporters (OTLP, Prometheus, Jaeger) without any RHAPSODY code changes. -
Span enricher for
asyncflow.workflow_id— registered automatically bystart_telemetry()viaregister_span_enricher(). Stampsasyncflow.workflow_idonto every task OTel span when the task was created inside aworkflow_scope()or@flow.block. Enables per-workflow Gantt views and span filtering in Jaeger / Grafana Tempo. -
_emit()workflow_idkwarg — whenworkflow_idis set, injectsasyncflow.workflow_idinto the event'sattributesdict. All task lifecycle events emitted byWorkflowEngine(TaskCreated, asyncflow.TaskResolved, TaskSubmitted) carry the active workflow ID. -
Example
01-workflow_grouping.py— complete rewrite as an HPC Campaign Manager simulation. Models 4 workflow types with resource tracking and dependency chains: simulate(4 tasks, GPU, no deps) — molecular dynamics runsanalyze(4 tasks, GPU, deps=simulate) — post-processing per simulationtrain(8 tasks, GPU, no deps) — distributed ML training-
evaluate(8 tasks, CPU, deps=train) — lightweight model evaluation UsesResourcePool(asyncio-queue-based GPU/CPU slot tracking) and emitscampaign.ResourceAssignedcustom events to record per-instance resource assignments in the JSONL checkpoint. -
plot_campaign.py— new plotting script producing a two-panel Campaign Manager timeline figure: - Top panel: Gantt chart with rows per workflow instance, bars coloured by workflow type and labelled with the assigned resource (
gpu:N/cpu:N). Right-margin annotations show priority / cpu / gpu per type. Right column renders a config table, scheduler description box, and dependency graph. -
Bottom panel: GPU (left axis) and CPU (right axis) utilisation as step functions over elapsed time, with total-capacity dashed reference lines.
-
capture_stdiodecorator parameter —@flow.executable_task(capture_stdio=True)redirects stdout/stderr from executable tasks directly to files instead of collecting them in memory. The awaited future resolves to the stdout file path ({work_dir}/{uid}/{task_uid}.stdout) instead of a decoded string. Honoured by all RHAPSODY backends (ConcurrentExecutionBackend,DaskExecutionBackend,DragonExecutionBackendV3); silently ignored for function tasks.capture_stdiois stored as a top-level field incomp_desc(not nested insidetask_backend_specific_kwargs) so RHAPSODY backends can read it astask.get("capture_stdio"). WorkflowEngineuidandwork_dirparameters —WorkflowEngine.create(uid=..., work_dir=...)sets the engine's unique identifier (UUID) and working directory. The engine is now the single authority forbackend._work_dir; it setsbackend._work_dir = {work_dir}/{uid}and creates the directory before any tasks run (mirrorsSession.add_backend()in RHAPSODY).WorkflowEngine._attach_backend()— new internal method that replaces the inlineregister_callbackloop. Handles_work_dirinjection,is_attachedflag, and callback registration in one place.- Unit tests for
capture_stdiofield placement, default value,_work_dirauthority, and end-to-end file I/O withConcurrentExecutionBackend.
Fixed¶
-
Block spans parented correctly —
execute_block()now benefits from thespan_scope()session-span fallback added in RHAPSODY: block spans that run in a context with no active OTel span (e.g. spawned fromrun()beforestart_telemetry()was awaited) now correctly nest under the session root span instead of floating as unrooted traces. -
execute_block()dead branch removed — therun_in_executorcode path (used when the wrapped function was sync) is removed.WorkflowEngineenforces a strict async API; all block functions must beasync def.
Changed¶
-
execute_block()usesnullcontext— the no-telemetry code path uses stdlibnullcontext(Python >= 3.7) instead of the former custom_null_context()helper, which is removed. -
execute_block()sets_workflow_id_ctx— the block's UID is set as the active_workflow_id_ctxfor the duration of block execution so every task registered inside the block inherits it asasyncflow.workflow_idwithout requiring an explicitworkflow_scope()call.
Docs¶
docs/telemetry.md:- Added "Forwarding to an external backend" section with corrected
span_processors/metric_readerscode examples (replaces the brokenset_tracer_provider()pattern). - Added "
workflow_scope()context manager" section with usage examples and auto-ID generation. - Added "OTel span hierarchy" section showing the four-level
session -> workflow -> block -> tasktree and explaining howasyncflow.workflow_idpropagates to every span attribute and JSONL event. - Updated
start_telemetry()signature to includespan_processors,metric_readers, andresourceparameters.
[0.3.1] - 2026-03-09¶
Fixed¶
executable_taskcommand parsing —shlex.splitis now applied correctly to the command string returned by the decorated function, ensuring commands with arguments and quoted strings are parsed properly.
[0.3.0] - 2026-02-24¶
Added¶
prompt_taskdecorator — new first-class task type alongsidefunction_taskandexecutable_task. The decorated async function returns a prompt string that is forwarded to an AI execution backend (e.g.DragonVllmInferenceBackendfrom RHAPSODY).NoopExecutionBackendreturns"Dummy Prompt Output"for testing;LocalExecutionBackendraisesNotImplementedErrorwith a clear message directing users to register an AI backend.- Multi-backend registry —
WorkflowEngine.create(backend=...)now accepts either a single backend or a list of pre-initialized named backends (each must expose a.nameattribute). The first backend in the list is the default. Built-in backends (LocalExecutionBackend,NoopExecutionBackend) gain anameparameter defaulting to"default". - Per-task backend routing — task decorators (
function_task,executable_task,prompt_task) accept an optionalbackend="<name>"parameter to route a specific task to a named backend. Tasks withoutbackend=are sent to the default backend. - Parallel backend submission — when a batch contains tasks routed to multiple backends,
submit()dispatches all backend calls concurrently viaasyncio.gather.shutdown()similarly drains all registered backends in parallel. - Example
04-concurrent_backends.py— demonstrates a compute + AI dual-backend workflow running document processing pipelines concurrently. - Tests — unit tests (
tests/unit/test_prompt_task.py) and integration tests (tests/integration/test_multi_backend.py) coveringprompt_taskregistration, validation, multi-backend routing, parallel submission, and graceful shutdown.
Fixed¶
async_wrapperexception propagation — errors raised during task registration (e.g.prompt_taskreturningNone, orexecutable_taskreturning a non-string) are now propagated to the task'sasyncio.Futureinstead of being silently swallowed by the detachedasyncio.create_task. Previously, such errors would causeawait task(...)to hang indefinitely.
Changed¶
- Documentation — all HPC backend references across docs, examples, and the Jupyter tutorial now point to the official RHAPSODY documentation instead of the GitHub repository, and include a direct link to the AsyncFlow integration guide.
- Examples — HPC example scripts (
04-dask_execution_backend.py,05-radical_execution_backend.py,06-dragon_execution_backend.py) updated with module-level docstrings referencing RHAPSODY docs and the AsyncFlow integration guide. - Proof-of-concept examples — all basic/getting-started examples (
basic.md,async_workflows.md,composite_workflow.md, examples 01–03) consistently useLocalExecutionBackend; RHAPSODY backends are only shown in dedicated HPC sections. - Install command — RHAPSODY install command corrected to
pip install rhapsody-pyacross all docs and examples.
[0.2.0] - 2026-02-12¶
Added¶
- Local execution backend —
LocalExecutionBackendships built-in, supporting bothThreadPoolExecutorandProcessPoolExecutorwithcloudpickleserialization. - No-op backend —
NoopExecutionBackendfor testing anddry_runworkflows without computational overhead. - HPC execution via RHAPSODY — HPC backends (
RadicalExecutionBackend,DaskExecutionBackend,DragonExecutionBackendV3) are now provided by the separate RHAPSODY package and plug into AsyncFlow via simple import. - Composite workflows —
@flow.blockdecorator for grouping tasks into logical units with support for nested blocks and inter-block dependencies. - Task dependency resolution — automatic DAG-based scheduling with implicit and explicit data dependencies between
function_taskandexecutable_tasktypes. - Pluggable backend architecture — any backend conforming to the execution interface can be passed to
WorkflowEngine.create(), enabling custom and third-party backends. - Pre-commit pipeline — ruff, docformatter, typos, actionlint, detect-secrets, and standard pre-commit hooks.
- CI/CD — GitHub Actions workflows for unit tests, integration tests, pre-commit checks, and documentation deployment across Python 3.9-3.13.
Changed¶
- Backend decoupling — HPC backends (Dask, RadicalPilot, Concurrent, Dragon) moved from
radical.asyncflowto the standalonerhapsodypackage. AsyncFlow now only shipsLocalExecutionBackendandNoopExecutionBackend. - Imports for HPC backends — HPC backends are now imported from
rhapsody.backendsinstead ofradical.asyncflow. - Documentation — all docs and examples updated to reflect the new architecture, with clear separation between built-in local backends and RHAPSODY HPC backends.
[0.1.0] - 2025-12-07¶
Initial public release of RADICAL AsyncFlow.
Added¶
- Async-first workflow engine with DAG-based task scheduling.
- Support for async and sync function execution within workflows.
- Execution backend abstraction with pluggable backend interface.
- RADICAL-based HPC execution backend integration.
- Agentic workflow support.
- Event-driven shutdown mechanism for backend lifecycle management.
- Task cancellation signaling and cooperative cancellation support.
- Enhanced async context manager support for workflow execution.
- Unit and integration testing framework.
- Initial project documentation.
- Migration to
pyproject.tomlwithtox-based test environments.
Changed¶
- Renamed project from
flowtoasyncflow. - Removed legacy synchronous execution paradigm in favor of fully async architecture.
- Refactored execution loop for thread-based backend.
- Updated RADICAL execution backend callback mechanism to support service-style operation.
- Improved error propagation for failed tasks.
- Refactored internal utilities to remove dependency on
radical.utils. - Eliminated redundant data dependency linking logic.
Fixed¶
- Corrected handling of task failure without explicit
wait(). - Fixed
handle_task_failurelogic for consistent error reporting. - Resolved process-based function execution launch issues.
- Ensured
pre_execdirectives are properly applied. - Addressed multiple execution edge cases in RADICAL backend.