Skip to content

Commit afbf760

Browse files
committed
Fix race condition in InMemoryDataLogVariable which results in sporadic test failures
Cherry-pick of 2fb3324 from 0.10.0 release
1 parent 905aed0 commit afbf760

1 file changed

Lines changed: 24 additions & 6 deletions

File tree

shc/interfaces/in_memory_data_logging.py

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,20 +29,30 @@ def __init__(self, type_: Type[T], keep: datetime.timedelta):
2929
self.data: List[Tuple[datetime.datetime, T]] = []
3030
self.keep = keep
3131
self._data_log_subscribers: List[LiveDataLogView] = []
32+
# Mutex to prevent race condition between _write() and retrieve_log_sync(). See comment in _write() for more
33+
# details. The mutex is not created in the __init__() method to allow construction of the object before starting
34+
# the asyncio EventLoop.
35+
self._mutex: Optional[asyncio.Lock] = None
3236

3337
async def _write(self, value: T, origin: List[Any]) -> None:
38+
if self._mutex is None:
39+
self._mutex = asyncio.Lock()
3440
self.clean_up()
3541
entry = (datetime.datetime.now(datetime.timezone.utc), value)
36-
self.data.append(entry)
3742
# We do not need the complicated locking, queuing and flushing from WritableDataLogVariable here, since querying
3843
# and appending the in-memory log is "atomic" (in the sense of asyncio tasks), i.e. does not include an 'await'
3944
# statement.
40-
tasks = [subscriber._new_log_values_written([entry])
45+
# However, notifying the subscribers is not atomic in this sense. So, we need to make sure that a consumer
46+
# cannot be initialized between updating the data array and notifying the subscribers. Thus, we need to lock
47+
# access to retrieve_log_sync() for the duration of the update.
48+
async with self._mutex:
49+
self.data.append(entry)
50+
tasks = [subscriber._new_log_values_written([entry])
4151
for subscriber in self._data_log_subscribers]
42-
if len(tasks) == 1:
43-
await tasks[0]
44-
else:
45-
await asyncio.gather(*tasks)
52+
if len(tasks) == 1:
53+
await tasks[0]
54+
else:
55+
await asyncio.gather(*tasks)
4656

4757
def clean_up(self) -> None:
4858
begin = datetime.datetime.now(datetime.timezone.utc) - self.keep
@@ -61,6 +71,14 @@ async def read(self) -> T:
6171
def subscribe_data_log(self, subscriber: LiveDataLogView) -> None:
6272
self._data_log_subscribers.append(subscriber)
6373

74+
async def retrieve_log_sync(
75+
self, start_time: datetime.datetime, end_time: datetime.datetime, include_previous: bool = True
76+
) -> List[Tuple[datetime.datetime, T]]:
77+
if self._mutex is None:
78+
self._mutex = asyncio.Lock()
79+
async with self._mutex:
80+
return await self.retrieve_log(start_time, end_time, include_previous)
81+
6482
async def retrieve_log(self, start_time: datetime.datetime, end_time: datetime.datetime,
6583
include_previous: bool = False) -> List[Tuple[datetime.datetime, T]]:
6684
iterator = iter(enumerate(self.data))

0 commit comments

Comments
 (0)