Skip to content

fix(actors): Tornado-to-asyncio conversion audit — tracking issue #685

Description

@LaikaN57

Tornado-to-Asyncio Conversion: Critical Analysis

Code References

Scope

This document is a line-by-line, pattern-by-pattern comparison of the old Tornado-based codebase (commit c2c791ee) against the current asyncio-based codebase (main branch, ede78930, v8.1.0). The analysis focuses on correctness, behavioral regressions, and subtle semantic differences that may not be caught by existing tests.


Executive Summary

The conversion is broadly correct in the happy path but contains several behavioral differences, some of which change failure semantics in ways that could matter in production deployments. The most critical findings are:

  1. group.Async task scheduling is fundamentally different -- tasks start immediately in Tornado, but are deferred in asyncio until the event loop yields. This changes timing and interleaving behavior.
  2. TaskGroup changes error semantics -- The IAM actors switched from Tornado's yield [list] (which surfaces the first exception) to asyncio.TaskGroup (which wraps exceptions in ExceptionGroup). The BaseActor.execute() handler for ExceptionGroup only partially compensates.
  3. The @dry decorator lost its gen.coroutine wrapper -- In Tornado, the @dry decorator called gen.coroutine(f) on the wrapped function, meaning the wrapped function did NOT need to be a coroutine. In asyncio, the wrapper calls await f(), which requires f to be async def. This works only because all call sites were also converted.
  4. The @timer decorator has a stacking order change -- Tornado: @gen.coroutine @timer (timer wraps coroutine). Asyncio: @timer async def execute (timer wraps async). The Tornado timer internally called gen.coroutine(f), so the function it wrapped did NOT need to be a coroutine. This is subtly different.
  5. HTTP client completely replaced -- tornado.httpclient.AsyncHTTPClient was replaced with synchronous urllib.request.urlopen running in a ThreadPoolExecutor. This changes connection pooling, redirect behavior, timeout handling, and error types.
  6. GenericHTTP._execute dry-run has a fire-and-forget coroutine bug in Tornado that was fixed -- The old code had raise gen.Return(self._execute_dry()) which returned the coroutine object without awaiting it. The new code correctly uses return await self._execute_dry().

Prioritized Work Plan

Priority Issue Risk Action Effort
🔴 1 #682 — IAM TaskGroup cancellation 🔴 HIGH Replace TaskGroup with asyncio.gather(return_exceptions=True) to restore Tornado's "let everything finish" semantics 🟠 Medium
🔴 2 #677 — No HTTP timeout 🔴 MEDIUM-HIGH Add timeout=30 to urlopen() in _fetch() and _get_macro() 🟢 Small
🟠 3 #673 — ExceptionGroup severity 🟠 MEDIUM Scan all exceptions in group for UnrecoverableActorFailure before deciding to swallow 🟢 Small
🟠 4 #680 — HTTP client lacks pooling 🟠 MEDIUM Evaluate replacing urllib with urllib3 or httpx for pooling + timeouts 🔴 Large
🟡 5 #674 — Async task scheduling order 🟡 LOW-MEDIUM Document or test interleaving differences; no code change likely needed 🟢 Small
🟡 6 #676 — Deprecated get_event_loop() 🟡 LOW Find-and-replace get_event_loop()get_running_loop() across 4 files 🟢 Small
🟡 7 #675 — @dry/@timer async assertion 🟡 LOW Add assert iscoroutinefunction(f) in decorators 🟢 Small
✅ 8 #672 — Timeout mechanism ✅ NONE Correct — no action needed
✅ 9 #678 — AWS api_call() ✅ NONE Correct — no action needed
✅ 10 #679 — ApiCallQueue ✅ NONE Correct — no action needed
✅ 11 #681 — CloudFormation ✅ NONE Correct — no action needed
✅ 12 #683 — S3 Bucket ✅ NONE Correct — no action needed
✅ 13 #684 — deploy.py ✅ NONE Improvement — no action needed

🔴 = fix now   🟠 = fix soon   🟡 = fix when convenient   ✅ = verified correct


Detailed Findings

1. Core Framework: actors/base.py — Timeout Mechanism

#672

2. Core Framework: actors/base.py — ExceptionGroup Handler in execute()

#673

3. actors/group.py — Async Task Scheduling Semantics

#674

4. actors/utils.py@dry and @timer Decorator Conversions

#675

5. kingpin/utils.py — Deprecated get_event_loop() Usage

#676

6. actors/misc.py — HTTP Calls Have No Timeout

#677

7. actors/aws/base.py — AWS Base Actor api_call() Conversion

#678

8. actors/aws/api_call_queue.py — Queue and Consumer Task Conversion

#679

9. HTTPBaseActor — HTTP Client Replacement (Tornado AsyncHTTPClient → urllib)

#680

10. actors/aws/cloudformation.py — CloudFormation Actor Conversion

#681

11. actors/aws/iam.py — TaskGroup Cancels Remaining Tasks on Failure (HIGH RISK)

#682

12. actors/aws/s3.py — S3 Bucket Actor Conversion

#683

13. bin/deploy.py — CLI Entry Point Shutdown Semantics

#684


Summary of Risks

HIGH Risk

Issue Location Description
#682 iam.py asyncio.TaskGroup cancels remaining tasks on first failure. Tornado yield [list] let them continue. Changes partial-failure behavior.

MEDIUM Risk

Issue Location Description
#677 misc.py, base.py urllib.request.urlopen has no timeout, can hang forever
#673 base.py Only first exception from ExceptionGroup is re-raised; severity classification may be wrong
#674 group.py ensure_future schedules on next tick vs Tornado's immediate scheduling

LOW Risk

Issue Location Description
#676 utils.py, aws/base.py, api_call_queue.py Should use get_running_loop() in Python 3.10+
#680 base.py urllib has no connection pooling or per-request timeouts
#675 actors/utils.py @dry/@timer should assert wrapped function is async

No Risk (Correct / Improvements)

Issue Location Description
#672 base.py Timeout conversion correct; asyncio.shield() preserves old behavior
#678 aws/base.py api_call() conversion correct
#679 api_call_queue.py Queue and consumer task conversion correct
#681 cloudformation.py Mechanical conversion, no semantic changes
#683 s3.py Sequential EnsurableBaseActor, no concurrency concerns
#684 deploy.py Stricter shutdown is an improvement; dry-run bug fixed

Metadata

Metadata

Assignees

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions