Skip to content

Commit 569596a

Browse files
Add test for deadlock
cothread version is bumped as that version seems to be much more likely to trigger the deadlock situation. It's a dev-only dependency.
1 parent b9b6eca commit 569596a

File tree

3 files changed

+149
-13
lines changed

3 files changed

+149
-13
lines changed

Pipfile.lock

Lines changed: 34 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

setup.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ dev =
5656
sphinx-rtd-theme-github-versions
5757
pytest-asyncio
5858
aioca >=1.6
59-
cothread; sys_platform != "win32"
59+
cothread>=2.19.1; sys_platform != "win32"
6060
p4p
6161

6262
[flake8]

tests/test_records.py

Lines changed: 114 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import asyncio
2-
import multiprocessing
2+
import subprocess
3+
import sys
34
import numpy
45
import os
56
import pytest
@@ -347,7 +348,6 @@ def test_record_wrapper_str():
347348
# If we never receive R it probably means an assert failed
348349
select_and_recv(parent_conn, "R")
349350

350-
351351
def validate_fixture_names(params):
352352
"""Provide nice names for the out_records fixture in TestValidate class"""
353353
return params[0].__name__
@@ -1104,3 +1104,115 @@ async def test_set_too_long_value(self):
11041104
log(f"PARENT: Join completed with exitcode {process.exitcode}")
11051105
if process.exitcode is None:
11061106
pytest.fail("Process did not terminate")
1107+
1108+
1109+
class TestRecursiveSet:
1110+
"""Tests related to recursive set() calls. See original issue here:
1111+
https://github.com/dls-controls/pythonSoftIOC/issues/119"""
1112+
1113+
recursive_record_name = "RecursiveLongOut"
1114+
1115+
def recursive_set_func(self, device_name, conn):
1116+
from cothread import Event
1117+
1118+
def useless_callback(value):
1119+
log("CHILD: In callback ", value)
1120+
useless_pv.set(0)
1121+
log("CHILD: Exiting callback")
1122+
1123+
def go_away(*args):
1124+
log("CHILD: received exit signal ", args)
1125+
event.Signal()
1126+
1127+
builder.SetDeviceName(device_name)
1128+
1129+
1130+
useless_pv = builder.aOut(
1131+
self.recursive_record_name,
1132+
initial_value=0,
1133+
on_update=useless_callback
1134+
)
1135+
event = Event()
1136+
builder.Action("GO_AWAY", on_update = go_away)
1137+
1138+
builder.LoadDatabase()
1139+
softioc.iocInit()
1140+
1141+
conn.send("R") # "Ready"
1142+
log("CHILD: Sent R over Connection to Parent")
1143+
1144+
log("CHILD: About to wait")
1145+
event.Wait()
1146+
log("CHILD: Exiting")
1147+
1148+
1149+
@pytest.mark.asyncio
1150+
async def test_recursive_set(self):
1151+
"""Test that recursive sets do not cause a deadlock"""
1152+
ctx = get_multiprocessing_context()
1153+
parent_conn, child_conn = ctx.Pipe()
1154+
1155+
device_name = create_random_prefix()
1156+
1157+
process = ctx.Process(
1158+
target=self.recursive_set_func,
1159+
args=(device_name, child_conn),
1160+
)
1161+
1162+
process.start()
1163+
1164+
log("PARENT: Child started, waiting for R command")
1165+
1166+
from aioca import caput, camonitor
1167+
1168+
try:
1169+
# Wait for message that IOC has started
1170+
select_and_recv(parent_conn, "R")
1171+
log("PARENT: received R command")
1172+
1173+
record = device_name + ":" + self.recursive_record_name
1174+
1175+
log(f"PARENT: monitoring {record}")
1176+
queue = asyncio.Queue()
1177+
monitor = camonitor(record, queue.put, all_updates=True)
1178+
1179+
log("PARENT: Beginning first wait")
1180+
1181+
# Expected initial state
1182+
new_val = await asyncio.wait_for(queue.get(), TIMEOUT)
1183+
log(f"PARENT: initial new_val: {new_val}")
1184+
assert new_val == 0
1185+
1186+
# Try a series of caput calls, to maximise chance to trigger
1187+
# the deadlock
1188+
i = 1
1189+
while i < 500:
1190+
log(f"PARENT: begin loop with i={i}")
1191+
await caput(record, i)
1192+
new_val = await asyncio.wait_for(queue.get(), 1)
1193+
assert new_val == i
1194+
new_val = await asyncio.wait_for(queue.get(), 1)
1195+
assert new_val == 0 # .set() should reset value
1196+
await asyncio.sleep(0.1)
1197+
i += 1
1198+
1199+
# Signal the IOC to cleanly shut down
1200+
await caput(device_name + ":" + "GO_AWAY", 1)
1201+
1202+
except asyncio.TimeoutError as e:
1203+
raise asyncio.TimeoutError(
1204+
f"IOC did not send data back - loop froze on iteration {i} "
1205+
"- it has probably hung/deadlocked."
1206+
) from e
1207+
1208+
finally:
1209+
monitor.close()
1210+
# Clear the cache before stopping the IOC stops
1211+
# "channel disconnected" error messages
1212+
aioca_cleanup()
1213+
1214+
process.join(timeout=TIMEOUT)
1215+
log(f"PARENT: Join completed with exitcode {process.exitcode}")
1216+
if process.exitcode is None:
1217+
process.terminate()
1218+
pytest.fail("Process did not finish cleanly, terminating")

0 commit comments

Comments
 (0)