@@ -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