openrvdas.logger.readers.timeout_reader
1#!/usr/bin/env python3 2 3import threading 4import time 5 6from logger.readers.reader import Reader # noqa: E402 7 8 9############################## 10class ReaderTimeout(StopIteration): 11 """A custom exception we can raise when we hit timeout.""" 12 pass 13 14 15################################################################################ 16class TimeoutReader(Reader): 17 """Instantiated with a client Reader instance (such as a 18 NetworkReader), an interval, a timeout and optional message. When its 19 read() method is called, it iteratively calls its passed reader's 20 read() method every interval seconds, discarding the received 21 output. It only returns if/when the client reader fails to return a 22 record within timeout seconds, in which case it returns either the 23 passed timeout message or a default one, warning that no records have 24 been received within the specified timeout. 25 26 In general, it's better if you can structure your logger configuration 27 so that it uses TimeoutWriters rather than TimeoutReaders. The former 28 are more robust and less computationally intensive. 29 """ 30 ############################ 31 32 def __init__(self, reader, timeout, message=None, resume_message=None, 33 empty_is_okay=False, none_is_okay=False, **kwargs): 34 """ 35 ``` 36 reader A client reader instance 37 38 timeout Timeout interval in seconds 39 40 message Message to be returned if client reader fails to return 41 a record within the timeout interval 42 43 resume_message Message to be returned when client returns a record after 44 having timed out 45 46 empty_is_okay If True, receiving an empty record is sufficient to reset 47 the timer. 48 none_is_okay If True, receiving a 'None' record is sufficient to reset 49 the timer. 50 ``` 51 Sample: 52 ``` 53 gyr1_reader = ComposedReader(NetworkReader(':6224'), 54 RegexFilterTransform('^gyr1')) 55 reader = TimeoutReader(reader=gyr1_reader, 56 timeout=15, 57 message='No Gyroscope records received for 15 seconds') 58 ``` 59 """ 60 super().__init__(**kwargs) 61 62 self.reader = reader 63 self.timeout = timeout 64 self.message = message or ('Timeout: no %s record received in %d seconds' 65 % (reader, timeout)) 66 self.resume_message = resume_message or ('Timeout: %s record received' 67 % reader) 68 self.empty_is_okay = empty_is_okay 69 self.none_is_okay = none_is_okay 70 71 # When we got our last record (or were instantiated) 72 self.last_record = time.time() 73 74 # Keep track of whether we're currently timed out or not 75 self.timed_out = False 76 77 # Protect self.last_record and self.timed_out 78 self.timeout_lock = threading.Lock() 79 80 # To let us cleanly exit _timeout_thread; this gets set to False 81 # when our read() method is called. 82 self.keep_reading = False 83 84 # Placeholder for the timeout loop we'll run in a separate thread 85 # when called. 86 self.timeout_thread = None 87 88 ############################ 89 def __del__(self): 90 self.quit() 91 92 ############################ 93 def quit(self): 94 self.keep_reading = False 95 96 ############################ 97 def _timeout_thread(self): 98 """Repeatedly call the client read() method, and keep track of when we 99 get records from it. 100 """ 101 while self.keep_reading: 102 # Loop until we get a record that matches our standards 103 record = None 104 while not record: 105 record = self.reader.read() 106 if self.empty_is_okay: 107 break 108 if record is None and self.none_is_okay: 109 break 110 111 # We've got a record! 112 with self.timeout_lock: 113 self.last_record = time.time() 114 # If we were timed out, we aren't anymore, and the read will 115 # return. Stop trying to read from our client (until we get 116 # another read() request). 117 if self.timed_out: 118 self.keep_reading = False 119 120 ############################ 121 def read(self): 122 """Block and return either a 'timeout' message or a 'resumed' message, 123 only when our client reader either hasn't given us a record in N 124 seconds or has resumed giving us records after having timed out. 125 """ 126 # Don't sleep the full interval because we want to quickly catch 127 # if we get a 'resume' 128 max_sleep_interval = 1 # Don't sleep more than one second 129 130 # We want the timeout_thread to start calling records for us. 131 self.keep_reading = True 132 133 # Set our 'last_record' to 'now' so that we start counting toward 134 # the timeout from now 135 with self.timeout_lock: 136 self.last_record = time.time() 137 138 # Start up timeout_thread if it's not running 139 if not self.timeout_thread or not self.timeout_thread.is_alive(): 140 self.timeout_thread = threading.Thread(target=self._timeout_thread, 141 name='timeout_thread', daemon=True) 142 self.timeout_thread.start() 143 144 # Loop until 1) we've timed out, 2) we've resumed after a timeout, 145 # or 3) we've been told to quit 146 while self.keep_reading: 147 now = time.time() 148 with self.timeout_lock: 149 time_since_last_record = now - self.last_record 150 151 # If we're timed out and we've seen a record inside our 152 # timeout window, we're not timed out anymore. Return a 153 # 'resumed' message and stop trying to read. 154 if self.timed_out and time_since_last_record < self.timeout: 155 self.timed_out = False 156 self.keep_reading = False 157 return self.resume_message 158 159 # Otherwise, figure how long to sleep before we need to see 160 # our next record. 161 time_to_sleep = self.timeout - time_since_last_record 162 163 # If we're overdue for a record... 164 if time_to_sleep < 0: 165 # If we weren't already timed out, we are now. Send message. 166 if not self.timed_out: 167 self.timed_out = True 168 self.keep_reading = False 169 return self.message 170 171 # Check again in a little while 172 time_to_sleep = max_sleep_interval 173 174 # Whether or not we're timed out, snooze a bit before checking 175 # again. 176 time.sleep(min(time_to_sleep, max_sleep_interval))
11class ReaderTimeout(StopIteration): 12 """A custom exception we can raise when we hit timeout.""" 13 pass
A custom exception we can raise when we hit timeout.
17class TimeoutReader(Reader): 18 """Instantiated with a client Reader instance (such as a 19 NetworkReader), an interval, a timeout and optional message. When its 20 read() method is called, it iteratively calls its passed reader's 21 read() method every interval seconds, discarding the received 22 output. It only returns if/when the client reader fails to return a 23 record within timeout seconds, in which case it returns either the 24 passed timeout message or a default one, warning that no records have 25 been received within the specified timeout. 26 27 In general, it's better if you can structure your logger configuration 28 so that it uses TimeoutWriters rather than TimeoutReaders. The former 29 are more robust and less computationally intensive. 30 """ 31 ############################ 32 33 def __init__(self, reader, timeout, message=None, resume_message=None, 34 empty_is_okay=False, none_is_okay=False, **kwargs): 35 """ 36 ``` 37 reader A client reader instance 38 39 timeout Timeout interval in seconds 40 41 message Message to be returned if client reader fails to return 42 a record within the timeout interval 43 44 resume_message Message to be returned when client returns a record after 45 having timed out 46 47 empty_is_okay If True, receiving an empty record is sufficient to reset 48 the timer. 49 none_is_okay If True, receiving a 'None' record is sufficient to reset 50 the timer. 51 ``` 52 Sample: 53 ``` 54 gyr1_reader = ComposedReader(NetworkReader(':6224'), 55 RegexFilterTransform('^gyr1')) 56 reader = TimeoutReader(reader=gyr1_reader, 57 timeout=15, 58 message='No Gyroscope records received for 15 seconds') 59 ``` 60 """ 61 super().__init__(**kwargs) 62 63 self.reader = reader 64 self.timeout = timeout 65 self.message = message or ('Timeout: no %s record received in %d seconds' 66 % (reader, timeout)) 67 self.resume_message = resume_message or ('Timeout: %s record received' 68 % reader) 69 self.empty_is_okay = empty_is_okay 70 self.none_is_okay = none_is_okay 71 72 # When we got our last record (or were instantiated) 73 self.last_record = time.time() 74 75 # Keep track of whether we're currently timed out or not 76 self.timed_out = False 77 78 # Protect self.last_record and self.timed_out 79 self.timeout_lock = threading.Lock() 80 81 # To let us cleanly exit _timeout_thread; this gets set to False 82 # when our read() method is called. 83 self.keep_reading = False 84 85 # Placeholder for the timeout loop we'll run in a separate thread 86 # when called. 87 self.timeout_thread = None 88 89 ############################ 90 def __del__(self): 91 self.quit() 92 93 ############################ 94 def quit(self): 95 self.keep_reading = False 96 97 ############################ 98 def _timeout_thread(self): 99 """Repeatedly call the client read() method, and keep track of when we 100 get records from it. 101 """ 102 while self.keep_reading: 103 # Loop until we get a record that matches our standards 104 record = None 105 while not record: 106 record = self.reader.read() 107 if self.empty_is_okay: 108 break 109 if record is None and self.none_is_okay: 110 break 111 112 # We've got a record! 113 with self.timeout_lock: 114 self.last_record = time.time() 115 # If we were timed out, we aren't anymore, and the read will 116 # return. Stop trying to read from our client (until we get 117 # another read() request). 118 if self.timed_out: 119 self.keep_reading = False 120 121 ############################ 122 def read(self): 123 """Block and return either a 'timeout' message or a 'resumed' message, 124 only when our client reader either hasn't given us a record in N 125 seconds or has resumed giving us records after having timed out. 126 """ 127 # Don't sleep the full interval because we want to quickly catch 128 # if we get a 'resume' 129 max_sleep_interval = 1 # Don't sleep more than one second 130 131 # We want the timeout_thread to start calling records for us. 132 self.keep_reading = True 133 134 # Set our 'last_record' to 'now' so that we start counting toward 135 # the timeout from now 136 with self.timeout_lock: 137 self.last_record = time.time() 138 139 # Start up timeout_thread if it's not running 140 if not self.timeout_thread or not self.timeout_thread.is_alive(): 141 self.timeout_thread = threading.Thread(target=self._timeout_thread, 142 name='timeout_thread', daemon=True) 143 self.timeout_thread.start() 144 145 # Loop until 1) we've timed out, 2) we've resumed after a timeout, 146 # or 3) we've been told to quit 147 while self.keep_reading: 148 now = time.time() 149 with self.timeout_lock: 150 time_since_last_record = now - self.last_record 151 152 # If we're timed out and we've seen a record inside our 153 # timeout window, we're not timed out anymore. Return a 154 # 'resumed' message and stop trying to read. 155 if self.timed_out and time_since_last_record < self.timeout: 156 self.timed_out = False 157 self.keep_reading = False 158 return self.resume_message 159 160 # Otherwise, figure how long to sleep before we need to see 161 # our next record. 162 time_to_sleep = self.timeout - time_since_last_record 163 164 # If we're overdue for a record... 165 if time_to_sleep < 0: 166 # If we weren't already timed out, we are now. Send message. 167 if not self.timed_out: 168 self.timed_out = True 169 self.keep_reading = False 170 return self.message 171 172 # Check again in a little while 173 time_to_sleep = max_sleep_interval 174 175 # Whether or not we're timed out, snooze a bit before checking 176 # again. 177 time.sleep(min(time_to_sleep, max_sleep_interval))
Instantiated with a client Reader instance (such as a NetworkReader), an interval, a timeout and optional message. When its read() method is called, it iteratively calls its passed reader's read() method every interval seconds, discarding the received output. It only returns if/when the client reader fails to return a record within timeout seconds, in which case it returns either the passed timeout message or a default one, warning that no records have been received within the specified timeout.
In general, it's better if you can structure your logger configuration so that it uses TimeoutWriters rather than TimeoutReaders. The former are more robust and less computationally intensive.
33 def __init__(self, reader, timeout, message=None, resume_message=None, 34 empty_is_okay=False, none_is_okay=False, **kwargs): 35 """ 36 ``` 37 reader A client reader instance 38 39 timeout Timeout interval in seconds 40 41 message Message to be returned if client reader fails to return 42 a record within the timeout interval 43 44 resume_message Message to be returned when client returns a record after 45 having timed out 46 47 empty_is_okay If True, receiving an empty record is sufficient to reset 48 the timer. 49 none_is_okay If True, receiving a 'None' record is sufficient to reset 50 the timer. 51 ``` 52 Sample: 53 ``` 54 gyr1_reader = ComposedReader(NetworkReader(':6224'), 55 RegexFilterTransform('^gyr1')) 56 reader = TimeoutReader(reader=gyr1_reader, 57 timeout=15, 58 message='No Gyroscope records received for 15 seconds') 59 ``` 60 """ 61 super().__init__(**kwargs) 62 63 self.reader = reader 64 self.timeout = timeout 65 self.message = message or ('Timeout: no %s record received in %d seconds' 66 % (reader, timeout)) 67 self.resume_message = resume_message or ('Timeout: %s record received' 68 % reader) 69 self.empty_is_okay = empty_is_okay 70 self.none_is_okay = none_is_okay 71 72 # When we got our last record (or were instantiated) 73 self.last_record = time.time() 74 75 # Keep track of whether we're currently timed out or not 76 self.timed_out = False 77 78 # Protect self.last_record and self.timed_out 79 self.timeout_lock = threading.Lock() 80 81 # To let us cleanly exit _timeout_thread; this gets set to False 82 # when our read() method is called. 83 self.keep_reading = False 84 85 # Placeholder for the timeout loop we'll run in a separate thread 86 # when called. 87 self.timeout_thread = None
reader A client reader instance
timeout Timeout interval in seconds
message Message to be returned if client reader fails to return
a record within the timeout interval
resume_message Message to be returned when client returns a record after
having timed out
empty_is_okay If True, receiving an empty record is sufficient to reset
the timer.
none_is_okay If True, receiving a 'None' record is sufficient to reset
the timer.
Sample:
gyr1_reader = ComposedReader(NetworkReader(':6224'),
RegexFilterTransform('^gyr1'))
reader = TimeoutReader(reader=gyr1_reader,
timeout=15,
message='No Gyroscope records received for 15 seconds')
122 def read(self): 123 """Block and return either a 'timeout' message or a 'resumed' message, 124 only when our client reader either hasn't given us a record in N 125 seconds or has resumed giving us records after having timed out. 126 """ 127 # Don't sleep the full interval because we want to quickly catch 128 # if we get a 'resume' 129 max_sleep_interval = 1 # Don't sleep more than one second 130 131 # We want the timeout_thread to start calling records for us. 132 self.keep_reading = True 133 134 # Set our 'last_record' to 'now' so that we start counting toward 135 # the timeout from now 136 with self.timeout_lock: 137 self.last_record = time.time() 138 139 # Start up timeout_thread if it's not running 140 if not self.timeout_thread or not self.timeout_thread.is_alive(): 141 self.timeout_thread = threading.Thread(target=self._timeout_thread, 142 name='timeout_thread', daemon=True) 143 self.timeout_thread.start() 144 145 # Loop until 1) we've timed out, 2) we've resumed after a timeout, 146 # or 3) we've been told to quit 147 while self.keep_reading: 148 now = time.time() 149 with self.timeout_lock: 150 time_since_last_record = now - self.last_record 151 152 # If we're timed out and we've seen a record inside our 153 # timeout window, we're not timed out anymore. Return a 154 # 'resumed' message and stop trying to read. 155 if self.timed_out and time_since_last_record < self.timeout: 156 self.timed_out = False 157 self.keep_reading = False 158 return self.resume_message 159 160 # Otherwise, figure how long to sleep before we need to see 161 # our next record. 162 time_to_sleep = self.timeout - time_since_last_record 163 164 # If we're overdue for a record... 165 if time_to_sleep < 0: 166 # If we weren't already timed out, we are now. Send message. 167 if not self.timed_out: 168 self.timed_out = True 169 self.keep_reading = False 170 return self.message 171 172 # Check again in a little while 173 time_to_sleep = max_sleep_interval 174 175 # Whether or not we're timed out, snooze a bit before checking 176 # again. 177 time.sleep(min(time_to_sleep, max_sleep_interval))
Block and return either a 'timeout' message or a 'resumed' message, only when our client reader either hasn't given us a record in N seconds or has resumed giving us records after having timed out.