Skip to content

Simplify event loop handling and remove deprecated get_event_loop#340

Open
GDYendell wants to merge 2 commits intomainfrom
simplify-event-loop
Open

Simplify event loop handling and remove deprecated get_event_loop#340
GDYendell wants to merge 2 commits intomainfrom
simplify-event-loop

Conversation

@GDYendell
Copy link
Contributor

@GDYendell GDYendell commented Mar 13, 2026


In the process of writing documentation it became clear that

  • We are using get_event_loop, which is deprecated and planned for removal in 3.16
  • The event loop handling is over complicated

Summary:

  • Remove the loop parameter to FastCS and instead require clients to call serve from an async context with the loop they want it to run in
  • Don't pass a loop to transports and instead retrieve it via get_running_loop only where required (EpicaCA and Tango)
  • Use asyncio.create_task from an async context consistently in tests, rather than run_until_complete or ensure_future
  • Improve tango test handling of DeviceTestContext to fix resource warnings

Summary by CodeRabbit

  • Refactor
    • Components no longer require supplying an explicit event loop; runtime loop is obtained internally, and task scheduling uses modern asyncio APIs.
  • Documentation
    • Added a comprehensive guide covering startup, runtime, interactive shell, and shutdown flows.
  • Tests
    • Updated tests to modern async patterns and to match the simplified startup/connect flow.

@GDYendell GDYendell requested review from coretl and shihab-dls March 13, 2026 16:47
@coderabbitai
Copy link

coderabbitai bot commented Mar 13, 2026

📝 Walkthrough
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 21.57% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title directly and accurately describes the main objective: simplifying event loop handling and removing deprecated asyncio.get_event_loop usage, which is the central theme throughout all file changes.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch simplify-event-loop
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@GDYendell
Copy link
Contributor Author

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Mar 13, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/fastcs/control_system.py`:
- Around line 54-55: The code currently installs SIGINT/SIGTERM handlers inside
serve(), which hijacks signal handling when serve() is called from an existing
async runtime; move the signal-handler installation out of serve() and into
run() so handlers are only installed when run() is explicitly invoked, and guard
the installation with a main-thread check (threading.current_thread() is
threading.main_thread()) and ensure the event loop supports add_signal_handler
before calling it (e.g., get the loop from asyncio.get_event_loop() and call
add_signal_handler only when permitted). Modify serve() to no longer call
add_signal_handler, and update run() to install the handlers conditionally for
SIGINT/SIGTERM using the same handler functions currently referenced in serve().
- Around line 164-167: The interactive_shell task created inside run()/wrapper
must be tracked and its exceptions handled: when creating the coroutine
(interactive_shell / asyncio.to_thread(partial(shell.mainloop, ...))) retain the
Task returned by asyncio.create_task and attach a done callback that
catches/logs exceptions and always calls stop_event.set() on error; also ensure
tasks spawned by run() are appended to a local registry so failures are visible
(e.g., add the created Task to a list in run() and on completion remove it), and
update wrapper to create the task via that helper so any raised exceptions won't
silently block await stop_event.wait().

In `@tests/transports/tango/test_dsr.py`:
- Around line 58-71: The current finally block's cleanup iterates
gc.get_objects() and closes any asyncio.AbstractEventLoop it finds, which is
unsafe; change the cleanup to only close loops created by this test/context by
recording references to event loops you create (e.g., the 'loop' variable and
any loops returned or owned by DeviceTestContext) or by snapshotting existing
loops before entering DeviceTestContext and closing only loops that appear
afterwards, and remove the gc.get_objects() iteration; ensure you still call
loop.run_until_complete(asyncio.sleep(0)), loop.close(),
asyncio.set_event_loop(None) and gc.collect() but limit any additional .close()
calls to the specific loop objects you tracked rather than all AbstractEventLoop
instances.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: cc07bde6-a8ed-470a-a36d-83ba46c30250

📥 Commits

Reviewing files that changed from the base of the PR and between 6839f1d and b86c206.

📒 Files selected for processing (16)
  • src/fastcs/control_system.py
  • src/fastcs/launch.py
  • src/fastcs/transports/epics/ca/ioc.py
  • src/fastcs/transports/epics/ca/transport.py
  • src/fastcs/transports/epics/pva/transport.py
  • src/fastcs/transports/graphql/transport.py
  • src/fastcs/transports/rest/transport.py
  • src/fastcs/transports/tango/transport.py
  • src/fastcs/transports/transport.py
  • tests/benchmarking/controller.py
  • tests/test_control_system.py
  • tests/transports/epics/ca/test_initial_value.py
  • tests/transports/epics/pva/test_p4p.py
  • tests/transports/graphQL/test_graphql.py
  • tests/transports/rest/test_rest.py
  • tests/transports/tango/test_dsr.py
💤 Files with no reviewable changes (1)
  • tests/transports/epics/ca/test_initial_value.py

Copy link
Contributor

@coretl coretl left a comment

Choose a reason for hiding this comment

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

@ajgdls is now our resident Tango expert so I'll defer to him to review the tango bits. Everything else looks fine

@GDYendell GDYendell force-pushed the simplify-event-loop branch 2 times, most recently from 676fac5 to e8b2558 Compare March 16, 2026 12:47
@codecov
Copy link

codecov bot commented Mar 16, 2026

Codecov Report

❌ Patch coverage is 73.46939% with 13 lines in your changes missing coverage. Please review.
✅ Project coverage is 91.02%. Comparing base (81f8fad) to head (42f07e5).

Files with missing lines Patch % Lines
src/fastcs/control_system.py 71.42% 10 Missing ⚠️
src/fastcs/transports/tango/transport.py 25.00% 3 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #340      +/-   ##
==========================================
- Coverage   91.21%   91.02%   -0.19%     
==========================================
  Files          70       70              
  Lines        2594     2596       +2     
==========================================
- Hits         2366     2363       -3     
- Misses        228      233       +5     

☔ View full report in Codecov by Sentry.
📢 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.

@GDYendell GDYendell force-pushed the simplify-event-loop branch 2 times, most recently from 941e46b to d9d0620 Compare March 16, 2026 12:53
Base automatically changed from docs to main March 17, 2026 10:36
@GDYendell GDYendell force-pushed the simplify-event-loop branch from d9d0620 to 6c77e2e Compare March 17, 2026 10:37
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (2)
src/fastcs/control_system.py (2)

150-181: ⚠️ Potential issue | 🟠 Major

The interactive-shell background tasks still need real supervision.

run() still drops the created task reference after the callback is attached, so shell-launched coroutines can disappear mid-flight, and await stop_event.wait() still blocks forever if interactive_shell() fails before setting the event.

🔧 Suggested fix
     async def _interactive_shell(self, context: dict[str, Any]):
         """Spawn interactive shell in another thread and wait for it to complete."""
         loop = asyncio.get_running_loop()
+        pending_tasks: set[asyncio.Task[Any]] = set()
+
+        def _track(task: asyncio.Task[Any]) -> asyncio.Task[Any]:
+            pending_tasks.add(task)
+            task.add_done_callback(pending_tasks.discard)
+            return task
 
         def run(coro: Coroutine[None, None, None]):
             """Run coroutine on FastCS event loop from IPython thread."""
 
             def wrapper():
-                task = asyncio.create_task(coro)
+                task = _track(asyncio.create_task(coro))
 
                 def _log_exception(t: asyncio.Task):
                     if not t.cancelled() and (exc := t.exception()):
                         logger.exception("`run` task raised exception", exc_info=exc)
 
                 task.add_done_callback(_log_exception)
 
             loop.call_soon_threadsafe(wrapper)
@@
         async def interactive_shell(
             context: dict[str, object], stop_event: asyncio.Event
         ):
             """Run interactive shell in a new thread."""
-            shell = InteractiveShellEmbed()
-            await asyncio.to_thread(partial(shell.mainloop, local_ns=context))
-
-            stop_event.set()
+            try:
+                shell = InteractiveShellEmbed()
+                await asyncio.to_thread(partial(shell.mainloop, local_ns=context))
+            finally:
+                stop_event.set()
@@
         context["run"] = run
 
         stop_event = asyncio.Event()
-        shell_task = asyncio.create_task(interactive_shell(context, stop_event))
-
-        await stop_event.wait()
-
-        if not shell_task.cancelled() and (exc := shell_task.exception()):
-            logger.exception("Interactive shell raised exception", exc_info=exc)
+        shell_task = _track(asyncio.create_task(interactive_shell(context, stop_event)))
+        wait_task = asyncio.create_task(stop_event.wait())
+        done, pending = await asyncio.wait(
+            {shell_task, wait_task},
+            return_when=asyncio.FIRST_COMPLETED,
+        )
+        for task in pending:
+            task.cancel()
+        if shell_task in done:
+            await shell_task
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/fastcs/control_system.py` around lines 150 - 181, run() currently creates
a task inside wrapper and drops its reference, and interactive_shell may fail
without setting stop_event so await stop_event.wait() can hang; fix by keeping a
supervised task set (e.g., a module/local set named supervised_tasks) and have
wrapper create the task, add it to supervised_tasks, add a done callback that
logs exceptions and removes the task from supervised_tasks, and ensure
interactive_shell always sets stop_event in a finally block (or signals failure
via setting stop_event and logging the exception) so await stop_event.wait()
cannot block forever; also use that supervised_tasks set to cancel/wait for
remaining tasks when the shell exits (referencing run, wrapper,
interactive_shell, shell_task, stop_event).

51-58: ⚠️ Potential issue | 🟠 Major

Guard add_signal_handler() for worker-thread callers.

FastCS.run() is still callable from any synchronous thread. On Unix, loop.add_signal_handler() raises outside the main thread, so this path now fails before serve() starts.

🔧 Minimal fix
 import asyncio
 import os
 import signal
+import threading
@@
-            if os.name != "nt" and task is not None:
+            if (
+                os.name != "nt"
+                and task is not None
+                and threading.current_thread() is threading.main_thread()
+            ):
                 loop.add_signal_handler(signal.SIGINT, task.cancel)
                 loop.add_signal_handler(signal.SIGTERM, task.cancel)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/fastcs/control_system.py` around lines 51 - 58, The code in async
function _serve calls loop.add_signal_handler on Unix unconditionally which
raises if FastCS.run is invoked from a non-main thread; fix by additionally
checking the current thread is the main thread before registering signals (e.g.,
import threading and wrap the add_signal_handler calls with if
threading.current_thread() is threading.main_thread(): ...), keeping the
existing os.name != "nt" check and leaving serve() untouched so signal handlers
are only installed when running on the main thread.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/explanations/control_system.md`:
- Line 80: Fix the wording typo in the shutdown section sentence that currently
reads "When then run is stopped"; replace it with a correct phrase such as "When
the run is stopped" (or "When the run is stopped, FastCS performs an orderly
teardown:") so the sentence reads smoothly and clearly in the user-facing docs;
update the sentence in the shutdown section where the phrase "When then run is
stopped" appears.

In `@src/fastcs/control_system.py`:
- Around line 143-145: The _start_scan_tasks method currently creates Tasks from
_scan_coros but doesn't supervise them; modify _start_scan_tasks to attach a
done callback to each Task that checks task.exception(), logs the exception
using the instance logger (e.g., self._logger.error with context like "scan task
failed"), and triggers a graceful shutdown routine (call an existing shutdown
method such as self._shutdown() or equivalent) so failures are surfaced and the
system stops cleanly; mirror the supervision pattern used by _interactive_shell
for callback structure and behavior.

---

Duplicate comments:
In `@src/fastcs/control_system.py`:
- Around line 150-181: run() currently creates a task inside wrapper and drops
its reference, and interactive_shell may fail without setting stop_event so
await stop_event.wait() can hang; fix by keeping a supervised task set (e.g., a
module/local set named supervised_tasks) and have wrapper create the task, add
it to supervised_tasks, add a done callback that logs exceptions and removes the
task from supervised_tasks, and ensure interactive_shell always sets stop_event
in a finally block (or signals failure via setting stop_event and logging the
exception) so await stop_event.wait() cannot block forever; also use that
supervised_tasks set to cancel/wait for remaining tasks when the shell exits
(referencing run, wrapper, interactive_shell, shell_task, stop_event).
- Around line 51-58: The code in async function _serve calls
loop.add_signal_handler on Unix unconditionally which raises if FastCS.run is
invoked from a non-main thread; fix by additionally checking the current thread
is the main thread before registering signals (e.g., import threading and wrap
the add_signal_handler calls with if threading.current_thread() is
threading.main_thread(): ...), keeping the existing os.name != "nt" check and
leaving serve() untouched so signal handlers are only installed when running on
the main thread.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3ab79b37-e4a5-49fa-9ae8-81a7cb2cebce

📥 Commits

Reviewing files that changed from the base of the PR and between b86c206 and 6c77e2e.

📒 Files selected for processing (18)
  • docs/explanations/control_system.md
  • pyproject.toml
  • src/fastcs/control_system.py
  • src/fastcs/launch.py
  • src/fastcs/transports/epics/ca/ioc.py
  • src/fastcs/transports/epics/ca/transport.py
  • src/fastcs/transports/epics/pva/transport.py
  • src/fastcs/transports/graphql/transport.py
  • src/fastcs/transports/rest/transport.py
  • src/fastcs/transports/tango/transport.py
  • src/fastcs/transports/transport.py
  • tests/benchmarking/controller.py
  • tests/test_control_system.py
  • tests/transports/epics/ca/test_initial_value.py
  • tests/transports/epics/pva/test_p4p.py
  • tests/transports/graphQL/test_graphql.py
  • tests/transports/rest/test_rest.py
  • tests/transports/tango/test_dsr.py
💤 Files with no reviewable changes (2)
  • tests/transports/epics/ca/test_initial_value.py
  • pyproject.toml
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/fastcs/transports/graphql/transport.py
  • src/fastcs/transports/epics/pva/transport.py
  • src/fastcs/transports/tango/transport.py

@GDYendell GDYendell force-pushed the simplify-event-loop branch from 6c77e2e to 42f07e5 Compare March 17, 2026 14:36
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
docs/explanations/control_system.md (1)

80-80: ⚠️ Potential issue | 🟡 Minor

Fix typo in shutdown sentence (Line 80).

The phrase “When then application is stopped” is grammatically incorrect in user-facing docs.

Suggested fix
-When then application is stopped, FastCS performs an orderly teardown:
+When the application is stopped, FastCS performs an orderly teardown:
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/explanations/control_system.md` at line 80, Replace the typo "When then
application is stopped" in docs/explanations/control_system.md (the shutdown
sentence) with the correct phrasing "When the application is stopped" so the
sentence reads "When the application is stopped, FastCS performs an orderly
teardown:"; update only that phrase to fix the grammar in the user-facing docs.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@docs/explanations/control_system.md`:
- Line 80: Replace the typo "When then application is stopped" in
docs/explanations/control_system.md (the shutdown sentence) with the correct
phrasing "When the application is stopped" so the sentence reads "When the
application is stopped, FastCS performs an orderly teardown:"; update only that
phrase to fix the grammar in the user-facing docs.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a07623b0-f9a9-4f71-a38d-570ed7f6cf90

📥 Commits

Reviewing files that changed from the base of the PR and between 6c77e2e and 42f07e5.

📒 Files selected for processing (1)
  • docs/explanations/control_system.md

@GDYendell
Copy link
Contributor Author

This is now simpler than it was initially and we only need per-test ignores on resource warnings.

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.

2 participants