Skip to content

Commit 43bb3dd

Browse files
Add test for deadlock
Note that this test is most recreatable with cothread==2.19.1, but that version doesn't support Python3.6, which pythonSoftIOC still declares support for. So just leave it unpinned and assume users will likely get that version!
1 parent b9b6eca commit 43bb3dd

File tree

2 files changed

+147
-12
lines changed

2 files changed

+147
-12
lines changed

Pipfile.lock

+34-10
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/test_records.py

+113-2
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,114 @@ 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+
@requires_cothread
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+
i += 1
1197+
1198+
# Signal the IOC to cleanly shut down
1199+
await caput(device_name + ":" + "GO_AWAY", 1)
1200+
1201+
except asyncio.TimeoutError as e:
1202+
raise asyncio.TimeoutError(
1203+
f"IOC did not send data back - loop froze on iteration {i} "
1204+
"- it has probably hung/deadlocked."
1205+
) from e
1206+
1207+
finally:
1208+
monitor.close()
1209+
# Clear the cache before stopping the IOC stops
1210+
# "channel disconnected" error messages
1211+
aioca_cleanup()
1212+
1213+
process.join(timeout=TIMEOUT)
1214+
log(f"PARENT: Join completed with exitcode {process.exitcode}")
1215+
if process.exitcode is None:
1216+
process.terminate()
1217+
pytest.fail("Process did not finish cleanly, terminating")

0 commit comments

Comments
 (0)