Skip to content

Commit fdeca46

Browse files
Refactor dispatchers to remove implementation leak
By creating/modifying the dispatchers we can easily handle the __completion being called after the __on_update. Without doing this there seem to be unavoidable implementation leaks, where the device.py file would have to care about the difference between cothread and asyncio.
1 parent 5791af4 commit fdeca46

File tree

6 files changed

+49
-44
lines changed

6 files changed

+49
-44
lines changed

softioc/asyncio_dispatcher.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,18 @@ def aioJoin(worker=worker, loop=self.loop):
3232
else:
3333
self.loop = loop
3434

35-
def __call__(self, func, *args):
35+
def __call__(self, func, completion, func_args=(), completion_args=()):
3636
async def async_wrapper():
3737
try:
38-
ret = func(*args)
39-
if inspect.isawaitable(ret):
40-
await ret
38+
if inspect.iscoroutinefunction(func):
39+
await func(*func_args)
40+
else:
41+
ret = func(*func_args)
42+
# Handle the case of a synchronous function that returns a
43+
# coroutine, like the lambda for on_update_name does
44+
if inspect.isawaitable(ret):
45+
await ret
46+
completion(*completion_args)
4147
except Exception:
4248
logging.exception("Exception when awaiting callback")
4349
asyncio.run_coroutine_threadsafe(async_wrapper(), self.loop)

softioc/cothread_dispatcher.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
2+
class CothreadDispatcher:
3+
def __init__(self):
4+
"""A dispatcher for `cothread` based IOCs, suitable to be passed to
5+
`softioc.iocInit`. """
6+
# Import here to ensure we don't instantiate any of cothread's global
7+
# state unless we have to
8+
import cothread
9+
# Create our own cothread callback queue so that our callbacks
10+
# processing doesn't interfere with other callback processing.
11+
self.__dispatcher = cothread.cothread._Callback()
12+
13+
def __call__(self, func, completion, func_args=(), completion_args=()):
14+
def wrapper():
15+
func(*func_args)
16+
completion(*completion_args)
17+
self.__dispatcher(wrapper)

softioc/device.py

Lines changed: 6 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -150,19 +150,6 @@ def __init__(self, name, **kargs):
150150
else:
151151
self.__on_update = None
152152

153-
# We cannot simply use `iscoroutinefunction` at the point of calling
154-
# on_update as the lambda used for on_update_name is NOT a coroutine.
155-
# There's no way to examine the lambda to see if it'd return a
156-
# coroutine without calling it, so we must keep track of it ourselves.
157-
# This is an unfortunate, unavoidable, leak of implementation detail
158-
# that really should be contained in the dispatcher.
159-
self.__on_update_is_coroutine = False
160-
if (
161-
iscoroutinefunction(on_update)
162-
or iscoroutinefunction(on_update_name)
163-
):
164-
self.__on_update_is_coroutine = True
165-
166153
self.__validate = kargs.pop('validate', None)
167154
self.__always_update = kargs.pop('always_update', False)
168155
self.__enable_write = True
@@ -203,14 +190,6 @@ def __completion(self, record):
203190
signal_processing_complete(record, self._callback)
204191
pass
205192

206-
def __wrap_completion(self, value, record):
207-
self.__on_update(value)
208-
self.__completion(record)
209-
210-
async def __async_wrap_completion(self, future, record):
211-
await future
212-
self.__completion(record)
213-
214193
def _process(self, record):
215194
'''Processing suitable for output records. Performs immediate value
216195
validation and asynchronous update notification.'''
@@ -237,21 +216,12 @@ def _process(self, record):
237216
record.UDF = 0
238217
if self.__on_update and self.__enable_write:
239218
record.PACT = self._blocking
240-
241-
if self.__on_update_is_coroutine:
242-
# This is an unfortunate, but unavoidable, leak of
243-
# implementation detail that really should be kept within
244-
# the dispatcher, but cannot be. This is due to asyncio not
245-
# allowing its event loop to be nested, thus either
246-
# requiring an additional call to the dispatcher once you
247-
# acquire the Future from the coroutine, or doing this.
248-
dispatcher(
249-
self.__async_wrap_completion,
250-
self.__on_update(python_value),
251-
record
252-
)
253-
else:
254-
dispatcher(self.__wrap_completion, python_value, record)
219+
dispatcher(
220+
self.__on_update,
221+
self.__completion,
222+
func_args=(python_value,),
223+
completion_args=(record,)
224+
)
255225

256226
return EPICS_OK
257227

softioc/softioc.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from epicsdbbuilder.recordset import recordset
88

99
from . import imports, device
10+
from . import cothread_dispatcher
1011

1112
__all__ = ['dbLoadDatabase', 'iocInit', 'interactive_ioc']
1213

@@ -31,10 +32,7 @@ def iocInit(dispatcher=None):
3132
'''
3233
if dispatcher is None:
3334
# Fallback to cothread
34-
import cothread
35-
# Create our own cothread callback queue so that our callbacks
36-
# processing doesn't interfere with other callback processing.
37-
dispatcher = cothread.cothread._Callback()
35+
dispatcher = cothread_dispatcher.CothreadDispatcher()
3836
# Set the dispatcher for record processing callbacks
3937
device.dispatcher = dispatcher
4038
imports.iocInit()

tests/sim_asyncio_ioc.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,17 @@ async def callback(value):
3636
# Create a record to set the alarm
3737
t_ao = builder.aOut('ALARM', on_update=callback)
3838

39+
async def on_update_name_callback(value, name):
40+
print(name, "value", value)
41+
42+
builder.longOut(
43+
"NAME-CALLBACK",
44+
initial_value = 3,
45+
always_update=True,
46+
on_update_name=on_update_name_callback,
47+
blocking=True
48+
)
49+
3950
# Run the IOC
4051
builder.LoadDatabase()
4152
softioc.iocInit(asyncio_dispatcher.AsyncioDispatcher())

tests/test_asyncio.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ async def test_asyncio_ioc(asyncio_ioc):
3939

4040
await caput(pre + ":ALARM", 3, wait=True)
4141

42+
await caput(pre + ":NAME-CALLBACK", 12, wait=True)
43+
4244
# Confirm the ALARM callback has completed
4345
select_and_recv(conn, "C") # "Complete"
4446

@@ -68,6 +70,7 @@ async def test_asyncio_ioc(asyncio_ioc):
6870
assert "%s:ALARM.VAL 0 -> 3" % pre in out
6971
assert 'on_update %s:AO : 3.0' % pre in out
7072
assert 'async update 3.0 (23.45)' in out
73+
assert "%s:NAME-CALLBACK value 12" % pre in out
7174
assert 'Starting iocInit' in err
7275
assert 'iocRun: All initialization complete' in err
7376
except Exception:

0 commit comments

Comments
 (0)