plesty.lib.device.async_wrapper =============================== .. py:module:: plesty.lib.device.async_wrapper .. autoapi-nested-parse:: Async wrapper classes for synchronous device objects. Classes ------- .. autoapisummary:: plesty.lib.device.async_wrapper.AsyncWrapperBase plesty.lib.device.async_wrapper.AsyncWrapperSafe plesty.lib.device.async_wrapper.AsyncDeviceThread Functions --------- .. autoapisummary:: plesty.lib.device.async_wrapper._settle Module Contents --------------- .. py:class:: AsyncWrapperBase(obj) Base class for wrapping synchronous objects into async-compatible interfaces. Features: - Transparent async method wrapping via ``__getattr__`` - Singleton-per-object (WeakKeyDictionary) - Async context manager support (``__aenter__``/``__aexit__``) - Pass-through for non-callable attributes - Leaves already-async methods untouched Subclasses must implement: ``async def _call(self, func, *args, **kwargs)`` Initialize the wrapper around the given synchronous object. .. py:attribute:: _registry :type: weakref.WeakKeyDictionary[Any, Any] .. py:attribute:: _obj .. py:attribute:: _initialized :value: True .. py:method:: _call(func, *args, **kwargs) :abstractmethod: :async: Execute a synchronous function asynchronously. Must be implemented by subclasses. .. py:method:: __getattr__(name) Forward attribute access to the wrapped object, wrapping callables as async. .. py:method:: __dir__() Return a combined list of attributes from the wrapper and the wrapped object. .. py:method:: __aenter__() :async: Enter the async context, forwarding to the wrapped object's __enter__ if present. .. py:method:: __aexit__(exc_type, exc, tb) :async: Exit the async context, forwarding to the wrapped object's __exit__ if present. .. py:class:: AsyncWrapperSafe(obj) Bases: :py:obj:`AsyncWrapperBase` Asynchronous wrapper for synchronous device objects using thread offloading. This class converts all callable attributes of a synchronous object into asynchronous methods by executing them in a background thread via `asyncio.to_thread`. A per-instance asyncio lock ensures that only one operation is executed at a time, making it safe for thread-unsafe hardware interfaces. The wrapper is transparent: methods can be called using `await` without modifying the original device implementation. A global weak registry ensures that only one wrapper exists per device instance, preventing accidental concurrent access through multiple wrappers. .. rubric:: Example .. code-block:: python sync_device = Monochromator(...) device = AsyncWrapperSafe(sync_device) await device.goto(532) await device.set_grating(1) Internal State: Uses an internal asyncio lock to serialize access and a class-level weak registry to maintain one wrapper instance per wrapped object. .. rubric:: Notes - All wrapped methods are executed in a thread using `asyncio.to_thread`, which avoids blocking the event loop. - Calls are serialized using an asyncio lock, ensuring safe access to non-thread-safe hardware. - If the wrapped object already provides async methods, they are returned unchanged and not wrapped again. - Non-callable attributes are forwarded directly. .. warning:: - **Serialized is not the same as single-threaded.** The lock keeps calls from overlapping, but ``asyncio.to_thread`` hands each one to the *process-wide* default executor: the device owns no thread, its calls are not pinned to one, and they queue behind every unrelated ``to_thread`` job in the process. A driver that is merely thread-unsafe is protected; a driver with thread **affinity**, or one whose callbacks must run while a call is in flight, is not. - Do not mix synchronous and asynchronous access to the same device instance, as this can lead to race conditions or hardware conflicts. - Creating multiple wrappers for different instances controlling the same physical hardware (e.g., same COM port or TCP endpoint) is not prevented by this class. Use an external resource registry if needed. - Frequent high-rate calls may incur overhead due to thread creation; consider a dedicated worker thread model in such cases. :raises RuntimeError: May propagate exceptions raised by the underlying device methods during execution. When to Use: - When you need a quick, low-overhead way to integrate synchronous device APIs into an asyncio-based application. - When device calls are relatively infrequent and simplicity is preferred over maximum performance. When Not to Use: - For high-frequency polling or streaming applications. - When strict single-thread execution is required — which, measured against real instruments, is most of them. Use :class:`AsyncDeviceThread` (``build_server(..., fixed_threading=True)``, the default). .. note:: Thread affinity is not a theoretical concern. In the 2026-08 field round, migration between pool threads produced two distinct hardware failures: a LightField completion callback (Python, invoked from .NET) was starved by the very pool thread blocked waiting on it, and ZMQ sockets used across threads lost messages under real network latency. Both went away under fixed threading. This class remains for drivers with no affinity requirement and for comparison measurements. Initialize AsyncWrapperSafe, creating a per-instance asyncio lock. .. py:attribute:: _lock .. py:method:: _call(func, *args, **kwargs) :async: Execute a synchronous function asynchronously. Must be implemented by subclasses. .. py:function:: _settle(future: asyncio.Future, result: Any, error: BaseException | None) -> None Complete *future*, unless whoever was waiting has already given up. A call abandoned by the caller — a client-side timeout, which the server implements with ``asyncio.wait_for`` — leaves a future that is already cancelled or timed out. The device call is still running on the worker thread, and when it finishes it has a result for nobody. Setting one on a settled future raises ``InvalidStateError`` inside the event loop, which surfaces as an asyncio callback error in the server log on every abandoned call. The result is genuinely discarded here, and that is correct: the client stopped waiting for it, and delivering it late would answer the *next* request with the previous one's value. :param future: The future the caller was awaiting. :param result: The device call's return value. :param error: The exception it raised, if any. .. py:class:: AsyncDeviceThread(obj) Bases: :py:obj:`AsyncWrapperBase` Asynchronous wrapper for synchronous device objects using a dedicated worker thread. This class provides a transparent async interface for a synchronous device by executing all method calls in a single background thread. Calls are queued and processed sequentially (FIFO), ensuring strict ordering and eliminating concurrent access to the underlying device. Unlike thread-per-call approaches, this design avoids repeated thread creation and is well-suited for high-frequency or continuous device interactions. The wrapper is transparent: callable attributes of the underlying device can be accessed as async methods using `await`. .. rubric:: Example .. code-block:: python sync_device = Wavemeter(...) device = AsyncDeviceThread(sync_device) wavelength = await device.get_wavelength() await device.set_mode("fast") Internal State: Uses a per-instance call queue plus a dedicated worker thread and a class-level weak registry to maintain one wrapper per object. .. rubric:: Notes - All device operations are executed in a single dedicated thread, guaranteeing thread safety for non-thread-safe hardware APIs. - Calls are processed in FIFO order, ensuring deterministic execution. - Results and exceptions are safely communicated back to the asyncio event loop using Futures. - If the wrapped object already provides async methods, they are returned unchanged and not wrapped again. - Non-callable attributes are forwarded directly. .. warning:: - Do not mix synchronous and asynchronous access to the same device instance, as this may lead to race conditions or undefined behavior. - Creating multiple device instances for the same physical hardware (e.g., same COM port or TCP endpoint) is not prevented by this class. Use a resource registry or device server for distributed coordination. - Long-running or blocking device calls will block the worker thread and delay subsequent queued operations. :raises RuntimeError: May propagate exceptions raised by the underlying device methods during execution. :raises asyncio.TimeoutError: If a timeout is specified and exceeded. When to Use: - When working with thread-unsafe hardware (serial, TCP, DLL). - For high-frequency polling or continuous control loops. - When strict ordering and single-thread execution are required. - When performance matters and thread creation overhead must be avoided. When Not to Use: - For simple or infrequent device interactions where a lightweight wrapper is sufficient. - When the underlying API is already fully asynchronous. Initialize AsyncDeviceThread, starting the background worker thread. .. py:attribute:: _registry :type: weakref.WeakKeyDictionary[Any, Any] .. py:attribute:: _queue .. py:attribute:: _thread .. py:method:: _worker() Worker thread: executes device calls sequentially. .. py:method:: _call(func, *args, **kwargs) :async: Schedule a function call in the worker thread and await result.