Skip to content

Add support for async workflow activities#1053

Open
seherv wants to merge 21 commits into
dapr:mainfrom
seherv:async-compat
Open

Add support for async workflow activities#1053
seherv wants to merge 21 commits into
dapr:mainfrom
seherv:async-compat

Conversation

@seherv

@seherv seherv commented May 25, 2026

Copy link
Copy Markdown
Contributor

Description

Workflow activities can now be async, and the runtime will automatically dispatch them to the event loop. Sync activities are still dispatched to the thread pool. The user-facing API remains exactly the same.

Also added a benchmark suite to verify performance locally.

Issue reference

We strive to have all PR being opened based on an issue, where the problem or feature have been discussed prior to implementation.

Please reference the issue this PR will close: #834 #897 #975

Checklist

Please make sure you've completed the relevant tasks for this PR, out of the following list:

  • Code compiles correctly
  • Created/updated tests
  • Extended the documentation

f"Activity '{req.name}#{req.taskId}' result is too large to deliver "
f'(RESOURCE_EXHAUSTED). Failing the activity task: {rpc_error.details()}'
)
failure_res = pb.ActivityResponse(

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This nesting got hard to follow, I needed to refactor this file to understand the logic better.

@codecov

codecov Bot commented May 25, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 89.80392% with 78 lines in your changes missing coverage. Please review.
✅ Project coverage is 83.18%. Comparing base (bffb749) to head (a2d4ad8).
⚠️ Report is 142 commits behind head on main.

Files with missing lines Patch % Lines
...-workflow/dapr/ext/workflow/_durabletask/worker.py 60.56% 28 Missing ⚠️
...r-ext-workflow/dapr/ext/workflow/_bench_harness.py 91.09% 22 Missing ⚠️
...workflow/tests/test_async_activity_registration.py 92.72% 12 Missing ⚠️
...kflow/dapr/ext/workflow/_durabletask/aio/client.py 50.00% 11 Missing ⚠️
...ext-workflow/dapr/ext/workflow/workflow_runtime.py 93.02% 3 Missing ⚠️
...rkflow/tests/durabletask/test_activity_executor.py 93.33% 1 Missing ⚠️
.../tests/durabletask/test_activity_executor_async.py 97.61% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1053      +/-   ##
==========================================
- Coverage   86.63%   83.18%   -3.45%     
==========================================
  Files          84      152      +68     
  Lines        4473    15536   +11063     
==========================================
+ Hits         3875    12924    +9049     
- Misses        598     2612    +2014     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Signed-off-by: Sergio Herrera <627709+seherv@users.noreply.github.com>
@JoshVanL JoshVanL requested a review from Copilot May 26, 2026 10:45

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

@seherv seherv requested a review from Copilot May 27, 2026 07:46

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 6 comments.

Comment thread ext/dapr-ext-workflow/dapr/ext/workflow/_durabletask/worker.py Outdated
Comment thread ext/dapr-ext-workflow/docs/concurrency.md Outdated
Comment thread ext/dapr-ext-workflow/AGENTS.md Outdated
Comment thread ext/dapr-ext-workflow/AGENTS.md Outdated
Comment thread ext/dapr-ext-workflow/tests/test_async_activity_registration.py
Comment thread ext/dapr-ext-workflow/benchmarks/RESULTS.md Outdated
Signed-off-by: Sergio Herrera <627709+seherv@users.noreply.github.com>
@seherv seherv requested a review from Copilot June 1, 2026 09:12

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 11 changed files in this pull request and generated 4 comments.

Comments suppressed due to low confidence (1)

ext/dapr-ext-workflow/dapr/ext/workflow/_durabletask/worker.py:678

  • This adds an extra registry lookup (get_activity) and inspect.iscoroutinefunction check on the hot dispatch path, but _ActivityExecutor._resolve() will look up the activity again during execution. Consider plumbing activity_fn (and/or an is_async flag decided at registration time) through to the executor so each activity work item only does one lookup in total.
                                activity_handler,
                                work_item.activityRequest,
                                stub,
                                work_item.completionToken,
                            )

Comment thread ext/dapr-ext-workflow/tests/test_async_activity_registration.py Outdated
Comment thread ext/dapr-ext-workflow/dapr/ext/workflow/_durabletask/worker.py
Comment thread ext/dapr-ext-workflow/docs/concurrency.md Outdated
Comment thread ext/dapr-ext-workflow/benchmarks/bench_async_activities.py Outdated
Signed-off-by: Sergio Herrera <627709+seherv@users.noreply.github.com>
@seherv seherv requested a review from Copilot June 1, 2026 11:43

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 11 changed files in this pull request and generated 5 comments.

Comment thread ext/dapr-ext-workflow/benchmarks/bench_async_activities.py Outdated
Comment thread ext/dapr-ext-workflow/benchmarks/bench_async_activities.py Outdated
Comment thread ext/dapr-ext-workflow/tests/test_async_activity_registration.py Outdated
Comment thread ext/dapr-ext-workflow/dapr/ext/workflow/_durabletask/worker.py
Signed-off-by: Sergio Herrera <627709+seherv@users.noreply.github.com>
@seherv seherv requested a review from Copilot June 1, 2026 13:24

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 11 out of 12 changed files in this pull request and generated 4 comments.

Comment thread ext/dapr-ext-workflow/tests/test_async_activity_registration.py Outdated
Comment thread ext/dapr-ext-workflow/dapr/ext/workflow/_durabletask/worker.py
Comment thread ext/dapr-ext-workflow/dapr/ext/workflow/_durabletask/worker.py
Comment thread ext/dapr-ext-workflow/dapr/ext/workflow/_durabletask/worker.py Outdated
seherv added 3 commits June 1, 2026 16:11
Signed-off-by: Sergio Herrera <627709+seherv@users.noreply.github.com>
Signed-off-by: Sergio Herrera <627709+seherv@users.noreply.github.com>
@seherv seherv requested a review from Copilot June 1, 2026 14:16

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 13 changed files in this pull request and generated 4 comments.

Comment thread ext/dapr-ext-workflow/docs/concurrency.md
Comment thread ext/dapr-ext-workflow/AGENTS.md Outdated
Comment thread ext/dapr-ext-workflow/benchmarks/bench_async_activities.py Outdated
Comment thread ext/dapr-ext-workflow/dapr/ext/workflow/_durabletask/worker.py
Signed-off-by: Sergio Herrera <627709+seherv@users.noreply.github.com>
@seherv seherv requested a review from Copilot June 1, 2026 14:54

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 15 changed files in this pull request and generated 3 comments.

Comment thread ext/dapr-ext-workflow/dapr/ext/workflow/_durabletask/worker.py
Comment thread ext/dapr-ext-workflow/docs/concurrency.md
Signed-off-by: Sergio Herrera <627709+seherv@users.noreply.github.com>
@seherv seherv requested a review from Copilot June 2, 2026 09:34

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 15 changed files in this pull request and generated 7 comments.

Comment thread ext/dapr-ext-workflow/dapr/ext/workflow/workflow_runtime.py
Comment thread ext/dapr-ext-strands/pyproject.toml
Comment thread ext/dapr-ext-workflow/dapr/ext/workflow/_durabletask/worker.py
Comment thread ext/dapr-ext-workflow/dapr/ext/workflow/_durabletask/worker.py
Comment thread ext/dapr-ext-workflow/docs/concurrency.md
Comment thread ext/dapr-ext-workflow/dapr/ext/workflow/_durabletask/worker.py
seherv added 3 commits June 2, 2026 12:51
Signed-off-by: Sergio Herrera <627709+seherv@users.noreply.github.com>
Signed-off-by: Sergio Herrera <627709+seherv@users.noreply.github.com>
@seherv seherv marked this pull request as ready for review June 2, 2026 13:00
@seherv seherv requested review from a team as code owners June 2, 2026 13:00
Signed-off-by: Sergio Herrera <627709+seherv@users.noreply.github.com>
@seherv seherv marked this pull request as draft June 2, 2026 17:26
@seherv

seherv commented Jun 2, 2026

Copy link
Copy Markdown
Contributor Author

Improving benchmarks and regression tests before submitting for review again

seherv added 3 commits June 2, 2026 23:14
Signed-off-by: Sergio Herrera <627709+seherv@users.noreply.github.com>
Signed-off-by: Sergio Herrera <627709+seherv@users.noreply.github.com>
@seherv seherv marked this pull request as ready for review June 2, 2026 21:52
Signed-off-by: Sergio Herrera <627709+seherv@users.noreply.github.com>
@acroca

acroca commented Jun 4, 2026

Copy link
Copy Markdown
Member

Thanks for working on this! I agree with the general direction, async activity support is something we've wanted for a while (#834, #897, #975), and keeping the user-facing API unchanged is the right approach.

I'd like to share my opinion on the benchmarking side, though. I think the sync-vs-async comparison is valuable in the context of this PR, to justify the why behind the change, but not as something we keep and maintain in the codebase. Concretely:

  • The benchmark suite adds ~1,100 lines (benchmarks/, _bench_harness.py), which is more code than the feature itself, and the comparison is essentially a one-off: once async dispatch is merged, it has served its purpose.
  • _bench_harness.py sits inside dapr/ext/workflow/, so it would ship to every user as part of the package.
  • The regression tests in test_async_dispatch_regression.py mostly assert sync-vs-async ratios, which again is the one-off comparison, and they depend on the harness.

My suggestion: run the benchmarks locally and paste the results + methodology into the PR description. Then the harness, the benchmarks/ directory, and the ratio-based tests can be dropped from the PR.

That said, I do think we should keep one simple throughput regression test so the async path can't silently regress, something like running 1000 activities that each take 1 second and asserting the batch completes in well under 10 seconds (ideally it's ~1s, so a 10x bound leaves plenty of headroom for CI noise). That single assertion catches the failure mode we actually care about, async activities accidentally getting serialized, without needing the full harness, and it can be written directly against the runtime in a few dozen lines.

Thoughts?

seherv and others added 3 commits June 9, 2026 13:46
Signed-off-by: Sergio Herrera <627709+seherv@users.noreply.github.com>
Signed-off-by: Sergio Herrera <627709+seherv@users.noreply.github.com>

@sicoyle sicoyle left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

few comments so far. I'm almost half way through the files/changes :)

try:
return Path(path).read_text(encoding='utf-8', errors='ignore')
except OSError:
return ''

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could you maybe add a warn log here

Comment on lines +13 to +25
"""Sync-vs-async activity benchmarks for ``dapr-ext-workflow``.

Runs the same I/O-bound activity workload as ``def`` and ``async def`` through the
production dispatch path against a mock sidecar stub. Scenarios: a fan-out burst, a
fan-out shaped as many small workflows, and a sustained open-loop run.

Run:

uv run python ext/dapr-ext-workflow/benchmarks/bench_async_activities.py

``DAPR_BENCH_ACTIVITY_MS`` overrides the activity duration, ``DAPR_BENCH_SUSTAINED_SECONDS``
the sustained run. Writes ``benchmarks/RESULTS.md`` and asserts pass-criteria budgets.
"""

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ideally this is not needed and is included in a readme pls

return rows


async def run_sustained() -> tuple[SustainedMetrics, SustainedMetrics]:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what does this mean sustained? like constant throughput over the same duration?

Comment on lines +193 to +198
# Sync vs async activity benchmark

Generated by `bench_async_activities.py`. Re-run with:

```bash
uv run python ext/dapr-ext-workflow/benchmarks/bench_async_activities.py

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pls don't include how to run here bc this can easily become stale if we rename the file or move things.


{_format_sustained_table(sustained_sync, sustained_async)}

See `ext/dapr-ext-workflow/docs/concurrency.md` for sizing guidance.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this a doc you plan to share? or can you rm this ref pls?

end_ts: dict[int, float],
) -> tuple[Callable[..., object], Callable[..., object]]:
"""Build the activity callable and pick the matching dispatch handler for ``kind``."""
if kind == 'async':

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

const pls

else _async_sleep_factory(latency_s, start_ts, end_ts)
)
return fn, worker._execute_activity_async
if kind == 'sync':

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

const and switch pls

)


ActivityFactory = Callable[[dict[int, float], dict[int, float]], Callable[..., object]]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in general can you also split this file up with smaller appropriately named files pls?

Comment on lines +351 to +380
def _metrics(
*,
name: str,
n_items: int,
semaphore_cap: int,
thread_pool_workers: int,
server_latency_s: float,
wallclock_s: float,
e2e_samples: list[float],
sampler: _Sampler,
baseline_rss_kb: int,
) -> ScenarioMetrics:
completed = len(e2e_samples) if e2e_samples else n_items
return ScenarioMetrics(
name=name,
n_items=n_items,
semaphore_cap=semaphore_cap,
thread_pool_workers=thread_pool_workers,
server_latency_s=server_latency_s,
wallclock_s=wallclock_s,
throughput_per_s=completed / wallclock_s if wallclock_s > 0 else 0.0,
latency=LatencyStats.from_samples(e2e_samples),
peak_tasks=sampler.peak_tasks,
peak_queue_depth=sampler.peak_queue_depth,
peak_rss_delta_mb=max(0.0, (sampler.peak_rss_kb - baseline_rss_kb) / 1024.0),
)


def _make_activity_context(orchestration_id: str, task_id: int) -> task.ActivityContext:
return task.ActivityContext(orchestration_id, task_id, '', propagated_history=None)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it really worth creating helpers that are one liners?

)


async def _run_lite(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is lite meaning here?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe docstrings would help on some of these funcs pls?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: asyncio support for workflow sdk

4 participants