Skip to content

Commit

Permalink
Try to remove background thread
Browse files Browse the repository at this point in the history
  • Loading branch information
marcelldls committed Jan 21, 2025
1 parent f67c15a commit f005662
Show file tree
Hide file tree
Showing 29 changed files with 560 additions and 206 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ coverage.xml
cov.xml
.pytest_cache/
.mypy_cache/
.benchmarks/

# Translations
*.mo
Expand Down
2 changes: 1 addition & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"console": "integratedTerminal",
"env": {
// Enable break on exception when debugging tests (see: tests/conftest.py)
"PYTEST_RAISE": "1",
"PYTEST_RAISE": "1"
},
}
]
Expand Down
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ dev = [
"pydata-sphinx-theme>=0.12",
"pyright",
"pytest",
"pytest-benchmark",
"pytest-cov",
"pytest-mock",
"pytest-asyncio",
Expand Down Expand Up @@ -69,7 +70,7 @@ reportMissingImports = false # Ignore missing stubs in imported modules
[tool.pytest.ini_options]
# Run pytest with all our checkers, and don't spam us with massive tracebacks on error
addopts = """
--tb=native -vv --doctest-modules --doctest-glob="*.rst"
--tb=native -vv --doctest-modules --doctest-glob="*.rst" --benchmark-sort=mean --benchmark-autosave --benchmark-columns="mean, min, max, outliers, ops, rounds"
"""
# https://iscinumpy.gitlab.io/post/bound-version-constraints/#watch-for-warnings
filterwarnings = "error"
Expand Down
7 changes: 4 additions & 3 deletions src/fastcs/attributes.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,10 @@ def __init__(
allowed_values: list[T] | None = None,
description: str | None = None,
) -> None:
assert (
datatype.dtype in ATTRIBUTE_TYPES
), f"Attr type must be one of {ATTRIBUTE_TYPES}, received type {datatype.dtype}"
assert datatype.dtype in ATTRIBUTE_TYPES, (
f"Attr type must be one of {ATTRIBUTE_TYPES}"
f", received type {datatype.dtype}"
)
self._datatype: DataType[T] = datatype
self._access_mode: AttrMode = access_mode
self._group = group
Expand Down
49 changes: 20 additions & 29 deletions src/fastcs/backend.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
import asyncio
from collections import defaultdict
from collections.abc import Callable
from concurrent.futures import Future
from types import MethodType

from softioc.asyncio_dispatcher import AsyncioDispatcher

from .attributes import AttrR, AttrW, Sender, Updater
from .controller import Controller, SingleMapping
from .exceptions import FastCSException
Expand All @@ -15,19 +12,15 @@ class Backend:
def __init__(
self,
controller: Controller,
loop: asyncio.AbstractEventLoop | None = None,
loop: asyncio.AbstractEventLoop,
):
self.dispatcher = AsyncioDispatcher(loop)
self._loop = self.dispatcher.loop
self._loop = loop
self._controller = controller

self._initial_coros = [controller.connect]
self._scan_futures: set[Future] = set()

asyncio.run_coroutine_threadsafe(
self._controller.initialise(), self._loop
).result()
self._scan_tasks: set[asyncio.Task] = set()

loop.run_until_complete(self._controller.initialise())
self._link_process_tasks()

def _link_process_tasks(self):
Expand All @@ -36,28 +29,26 @@ def _link_process_tasks(self):
_link_attribute_sender_class(single_mapping)

def __del__(self):
self.stop_scan_futures()
self._stop_scan_tasks()

def run(self):
self._run_initial_futures()
self.start_scan_futures()
async def serve(self):
await self._run_initial_tasks()
await self._start_scan_tasks()

def _run_initial_futures(self):
async def _run_initial_tasks(self):
for coro in self._initial_coros:
future = asyncio.run_coroutine_threadsafe(coro(), self._loop)
future.result()
await coro()

def start_scan_futures(self):
self._scan_futures = {
asyncio.run_coroutine_threadsafe(coro(), self._loop)
for coro in _get_scan_coros(self._controller)
async def _start_scan_tasks(self):
self._scan_tasks = {
self._loop.create_task(coro()) for coro in _get_scan_coros(self._controller)
}

def stop_scan_futures(self):
for future in self._scan_futures:
if not future.done():
def _stop_scan_tasks(self):
for task in self._scan_tasks:
if not task.done():
try:
future.cancel()
task.cancel()
except asyncio.CancelledError:
pass

Expand All @@ -83,9 +74,9 @@ def _link_attribute_sender_class(single_mapping: SingleMapping) -> None:
for attr_name, attribute in single_mapping.attributes.items():
match attribute:
case AttrW(sender=Sender()):
assert (
not attribute.has_process_callback()
), f"Cannot assign both put method and Sender object to {attr_name}"
assert not attribute.has_process_callback(), (
f"Cannot assign both put method and Sender object to {attr_name}"
)

callback = _create_sender_callback(attribute, single_mapping.controller)
attribute.set_process_callback(callback)
Expand Down
100 changes: 58 additions & 42 deletions src/fastcs/launch.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import asyncio
import inspect
import json
from pathlib import Path
Expand All @@ -19,7 +20,9 @@
from .transport.tango.options import TangoOptions

# Define a type alias for transport options
TransportOptions: TypeAlias = EpicsOptions | TangoOptions | RestOptions | GraphQLOptions
TransportOptions: TypeAlias = list[
EpicsOptions | TangoOptions | RestOptions | GraphQLOptions
]


class FastCS:
Expand All @@ -28,48 +31,63 @@ def __init__(
controller: Controller,
transport_options: TransportOptions,
):
self._backend = Backend(controller)
self._transport: TransportAdapter
match transport_options:
case EpicsOptions():
from .transport.epics.adapter import EpicsTransport

self._transport = EpicsTransport(
controller,
self._backend.dispatcher,
transport_options,
)
case GraphQLOptions():
from .transport.graphQL.adapter import GraphQLTransport

self._transport = GraphQLTransport(
controller,
transport_options,
)
case TangoOptions():
from .transport.tango.adapter import TangoTransport

self._transport = TangoTransport(
controller,
transport_options,
)
case RestOptions():
from .transport.rest.adapter import RestTransport

self._transport = RestTransport(
controller,
transport_options,
)
self._loop = asyncio.get_event_loop()
self._backend = Backend(controller, self._loop)
transport: TransportAdapter
self._transports: list[TransportAdapter] = []
for option in transport_options:
match option:
case EpicsOptions():
from .transport.epics.adapter import EpicsTransport

transport = EpicsTransport(
controller,
self._loop,
option,
)
case TangoOptions():
from .transport.tango.adapter import TangoTransport

transport = TangoTransport(
controller,
self._loop,
option,
)
case RestOptions():
from .transport.rest.adapter import RestTransport

transport = RestTransport(
controller,
option,
)
case GraphQLOptions():
from .transport.graphQL.adapter import GraphQLTransport

transport = GraphQLTransport(
controller,
option,
)
self._transports.append(transport)

def create_docs(self) -> None:
self._transport.create_docs()
for transport in self._transports:
if hasattr(transport.options, "docs"):
transport.create_docs()

Check warning on line 75 in src/fastcs/launch.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/launch.py#L73-L75

Added lines #L73 - L75 were not covered by tests

def create_gui(self) -> None:
self._transport.create_gui()
for transport in self._transports:
if hasattr(transport.options, "gui"):
transport.create_docs()

Check warning on line 80 in src/fastcs/launch.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/launch.py#L78-L80

Added lines #L78 - L80 were not covered by tests

def run(self) -> None:
self._backend.run()
self._transport.run()
def run(self):
self._loop.run_until_complete(
self.serve(),
)

async def serve(self) -> None:
coros = [self._backend.serve()]
coros.extend([transport.serve() for transport in self._transports])
await asyncio.gather(*coros)


def launch(
Expand Down Expand Up @@ -158,10 +176,8 @@ def run(
instance_options.transport,
)

if "gui" in options_yaml["transport"]:
instance.create_gui()
if "docs" in options_yaml["transport"]:
instance.create_docs()
instance.create_gui()
instance.create_docs()
instance.run()

@launch_typer.command(name="version", help=f"{controller_class.__name__} version")
Expand Down
8 changes: 7 additions & 1 deletion src/fastcs/transport/adapter.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
from abc import ABC, abstractmethod
from typing import Any


class TransportAdapter(ABC):
@property
@abstractmethod
def run(self) -> None:
def options(self) -> Any:
pass

Check warning on line 9 in src/fastcs/transport/adapter.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/transport/adapter.py#L9

Added line #L9 was not covered by tests

@abstractmethod
async def serve(self) -> None:
pass

@abstractmethod
Expand Down
24 changes: 17 additions & 7 deletions src/fastcs/transport/epics/adapter.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from softioc.asyncio_dispatcher import AsyncioDispatcher
import asyncio

from fastcs.controller import Controller
from fastcs.transport.adapter import TransportAdapter
Expand All @@ -13,20 +13,30 @@ class EpicsTransport(TransportAdapter):
def __init__(
self,
controller: Controller,
dispatcher: AsyncioDispatcher,
loop: asyncio.AbstractEventLoop,
options: EpicsOptions | None = None,
) -> None:
self.options = options or EpicsOptions()
self._controller = controller
self._dispatcher = dispatcher
self._loop = loop
self._options = options or EpicsOptions()
self._pv_prefix = self.options.ioc.pv_prefix
self._ioc = EpicsIOC(self.options.ioc.pv_prefix, controller)
self._ioc = EpicsIOC(
self.options.ioc.pv_prefix,
controller,
self._options.ioc,
)

@property
def options(self) -> EpicsOptions:
return self._options

def create_docs(self) -> None:
EpicsDocs(self._controller).create_docs(self.options.docs)

def create_gui(self) -> None:
EpicsGUI(self._controller, self._pv_prefix).create_gui(self.options.gui)

def run(self):
self._ioc.run(self._dispatcher)
async def serve(self) -> None:
self._ioc.run(self._loop)
while True:
await asyncio.sleep(1)
13 changes: 4 additions & 9 deletions src/fastcs/transport/epics/ioc.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import asyncio
from collections.abc import Callable
from dataclasses import asdict
from types import MethodType
Expand Down Expand Up @@ -50,7 +51,7 @@ def __init__(
controller: Controller,
options: EpicsIOCOptions | None = None,
):
self.options = options or EpicsIOCOptions()
self._options = options or EpicsIOCOptions()
self._controller = controller
_add_pvi_info(f"{pv_prefix}:PVI")
_add_sub_controller_pvi_info(pv_prefix, controller)
Expand All @@ -60,18 +61,12 @@ def __init__(

def run(
self,
dispatcher: AsyncioDispatcher,
loop: asyncio.AbstractEventLoop,
) -> None:
dispatcher = AsyncioDispatcher(loop) # Needs running loop
builder.LoadDatabase()
softioc.iocInit(dispatcher)

if self.options.terminal:
context = {
"dispatcher": dispatcher,
"controller": self._controller,
}
softioc.interactive_ioc(context)


def _add_pvi_info(
pvi: str,
Expand Down
1 change: 0 additions & 1 deletion src/fastcs/transport/epics/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ class EpicsGUIOptions:

@dataclass
class EpicsIOCOptions:
terminal: bool = True
pv_prefix: str = "MY-DEVICE-PREFIX"


Expand Down
6 changes: 3 additions & 3 deletions src/fastcs/transport/epics/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,9 @@ def attr_is_enum(attribute: Attribute) -> bool:
"""
match attribute:
case Attribute(
datatype=String(), allowed_values=allowed_values
) if allowed_values is not None and len(allowed_values) <= MBB_MAX_CHOICES:
case Attribute(datatype=String(), allowed_values=allowed_values) if (
allowed_values is not None and len(allowed_values) <= MBB_MAX_CHOICES
):
return True
case _:
return False
Expand Down
Loading

0 comments on commit f005662

Please sign in to comment.