Changes to Asyncio green mode servers#
Deprecation of synchronous methods in Asyncio servers#
Historically, the implementation of green modes utilized the basic synchronous PyTango code and tried to convert to asynchronous code (coroutines) on the fly. The main advantage of such an implementation was simpler code maintenance: all changes we make automatically applied to all green modes.
However, with Asyncio mode there is a major problem:
to convert all original synchronous methods we used Python’s asyncio.coroutine
function,
which was the first iteration to make coroutines by asyncio
. Unfortunately, it was deprecated in Python 3.8
and finally removed from Python 3.11.
As the temporary solution we copied the asyncio.coroutine
code to PyTango.
But then the cleaning process of the asyncio
library continued and for Python 3.12 we
had to copy run_coroutine_threadsafe
and modify it to be able to work with such legacy generator-based coroutines.
Since there is no guarantee that our copied methods will be compatible
with new versions of asyncio
we decided to change how the
Asyncio green mode of PyTango is implemented.
Note
Starting from PyTango 10.0.0 all Asyncio servers should be written with coroutine functions.
In other words, use async def
when defining methods for attribute access (read/write/is allowed),
for command access (command/is allowed), and for the “special” methods:
init_device
delete_device
dev_state
dev_status
read_attr_hardware
always_executed_hook
The base tango.server.Device
class was also modified to use async def
, so instead of
doing super().<method name>()
calls you must do await super().<method name>()
.
For example, change code like this:
class MyDevice(Device):
green_mode = tango.GreenMode.Asyncio
def init_device(self):
super().init_device()
self._attr = 1.5
@attribute
def slow_attr(self) -> float:
time.sleep(0.5)
return self._attr
To code like this:
class MyDevice(Device):
green_mode = tango.GreenMode.Asyncio
async def init_device(self):
await super().init_device()
self._attr = 1.5
@attribute
async def slow_attr(self) -> float:
await asyncio.sleep(0.5)
return self._attr
In PyTango 10.0.0 we still preserve option to run legacy servers, with synchronous user functions,
but every time server is started you will receive a DeprecationWarning
.
New methods for adding and removing dynamic attributes#
There are now coroutine functions for adding and removing attributes. Specifically,
async_add_attribute()
and async_remove_attribute()
.
Use these instead of the synchronous versions, as they can prevent deadlocks in certain cases
when clients are accessing the device during attribute creation/removal.
For example, change code like this:
class MyDevice(Device):
green_mode = tango.GreenMode.Asyncio
attr_value = None
@command
async def add_dyn_attr(self):
attr = attribute(
name="dyn_attr",
dtype=int,
access=AttrWriteType.READ_WRITE,
fget=self.read_dyn_attr,
fset=self.write_dyn_attr,
)
self.add_attribute(attr) # bad **********
@command
async def remove_dyn_attr(self):
self.remove_attribute("dyn_attr") # bad **********
async def write_dyn_attr(self, attr):
self.attr_value = attr.get_write_value()
async def read_dyn_attr(self, attr):
return self.attr_value
To code like this:
class MyDevice(Device):
green_mode = tango.GreenMode.Asyncio
attr_value = None
@command
async def add_dyn_attr(self):
attr = attribute(
name="dyn_attr",
dtype=int,
access=AttrWriteType.READ_WRITE,
fget=self.read_dyn_attr,
fset=self.write_dyn_attr,
)
await self.async_add_attribute(attr) # good **********
@command
async def remove_dyn_attr(self):
await self.async_remove_attribute("dyn_attr") # good **********
async def write_dyn_attr(self, attr):
self.attr_value = attr.get_write_value()
async def read_dyn_attr(self, attr):
return self.attr_value
One exception to this rule, is when you are overriding the standard method,
initialize_dynamic_attributes()
. That method is
synchronous, so we cannot use the async versions. Since it is only run at device
creation, we avoid the deadlock as clients cannot yet access the attributes.
Code like this does not need to change:
class MyDevice(Device):
green_mode = tango.GreenMode.Asyncio
attr_value = None
def initialize_dynamic_attributes(self):
attr = attribute(
name="dyn_attr",
dtype=int,
access=AttrWriteType.READ_WRITE,
fget=self.read_dyn_attr,
fset=self.write_dyn_attr,
)
self.add_attribute(attr) # OK, in this method
async def write_dyn_attr(self, attr):
self.attr_value = attr.get_write_value()
async def read_dyn_attr(self, attr):
return self.attr_value