openrvdas.logger.readers.composed_reader
1#!/usr/bin/env python3 2 3import logging 4import threading 5 6from logger.readers.reader import Reader # noqa: E402 7 8# How long to a reader thread should lie dormant before shutting down 9# and counting on getting restarted again if/when needed. We need this 10# so that our readers eventually terminate. 11READER_TIMEOUT_WAIT = 0.25 12 13 14################################################################################ 15class ComposedReader(Reader): 16 """ 17 Read lines from one or more Readers (in parallel) and process their 18 responses through zero or more Transforms (in series). 19 20 NOTE: we make the rash assumption that transforms are thread-safe, 21 that is, that no mischief or corrupted internal state will result if 22 more than one thread calls a transform at the same time. To be 23 thread-safe, a transform must protect any changes to its internal 24 state with a non-re-entrant thread lock, as described in the threading 25 module. 26 27 Also NOTE: Most of the messy logic in this class comes from the desire 28 to only call read() on our component readers when we actually need new 29 records (NOTE: this desire may be misplaced!). 30 31 So when we get a request, we fire up threads and ask each of our 32 readers for a record. We return the first one we get, and let the 33 others pile up in a queue that we'll feed from the next time we're 34 asked. 35 36 But we don't want to fire up a new thread for each reader every time 37 the queue is empty, so we have threads (in run_reader()) hang out for 38 a little while, waiting for another queue_needs_record event. If they 39 get one, the call their own read() methods again. If they haven't been 40 called on in READER_TIMEOUT_WAIT seconds, they exit, but will get 41 fired up again by read() if/when the queue is empty and we're is asked 42 for another record. 43 44 It's important to have the run_reader threads time out, or any process 45 using a ComposedReader will never naturally terminate. 46 """ 47 ############################ 48 49 def __init__(self, readers, transforms=[], **kwargs): 50 """ 51 Instantiation: 52 ``` 53 reader = ComposedReader(readers, transforms=[], check_format=True) 54 55 readers A single Reader or a list of Readers. 56 57 transforms A single Transform or list of zero or more Transforms. 58 ``` 59 Use: 60 ``` 61 record = reader.read() 62 ``` 63 Sample: 64 ``` 65 reader = ComposedReader(readers=[UDPReader(port=6221), 66 UDPReader(port=6223)], 67 transforms=[TimestampTransform()]) 68 ``` 69 """ 70 super().__init__(**kwargs) 71 72 # Make readers a list, even if it's only a single reader. 73 self.readers = readers if type(readers) is list else [readers] 74 self.num_readers = len(self.readers) 75 76 # Transforms can be empty. But if not empty, make it a list, even 77 # if it's only a single transform. 78 if not isinstance(transforms, list): 79 self.transforms = [transforms] 80 else: 81 self.transforms = transforms 82 83 # List where we're going to store reader threads 84 self.reader_threads = [None] * self.num_readers 85 86 # Whether reader[i] has returned EOF since we've last asked it 87 self.reader_returned_eof = [False] * self.num_readers 88 89 # One lock per reader, to save us from accidental re-entry 90 self.reader_locks = [threading.Lock() for i in range(self.num_readers)] 91 92 # Queue where we'll store extra records, and lock so only one 93 # thread can touch queue at a time 94 self.queue = [] 95 self.queue_lock = threading.Lock() 96 97 # The two events, queue_has_record and queue_needs_record interact 98 # in a sort of a dance: 99 # 100 # has = False, need = False: Everything is quiescent 101 # has = False, need = True: A request has been made, call readers 102 # has = True, need = True: Momentary condition when we get needed rec 103 # has = True, need = False: We've got spare records in the queue 104 # 105 # Set when the queue is empty and we need a record 106 self.queue_needs_record = threading.Event() 107 108 # Set when a reader adds something to the queue 109 self.queue_has_record = threading.Event() 110 111 ############################ 112 def read(self): 113 """ 114 Get the next record from queue or readers. 115 """ 116 # If we only have one reader, there's no point making things 117 # complicated. Just read, transform, return. 118 if len(self.readers) == 1: 119 return self._apply_transforms(self.readers[0].read()) 120 121 # Do we have anything in the queue? Note: safe to check outside of 122 # lock, because we're the only method that actually *removes* 123 # anything. So if tests True here, we're assured that there's 124 # something there, and we lock before retrieving it. Advantage of 125 # doing it this way is that we don't tie up queue lock while 126 # processing transforms. 127 if self.queue: 128 logging.debug('read() - read requested; queue len is %d', 129 len(self.queue)) 130 with self.queue_lock: 131 record = self.queue.pop(0) 132 return self._apply_transforms(record) 133 134 # If here, nothing's in the queue. Note that, if we wanted to be 135 # careful to never unnecessarily ask for more records, we should 136 # put a lock around this, but the failure mode is somewhat benign: 137 # we ask for more records when some are already on the way. 138 logging.debug('read() - read requested and nothing in the queue.') 139 140 # Some threads may have timed out while waiting to be called to 141 # action; restart them. 142 for i in range(len(self.readers)): 143 if not self.reader_threads[i] \ 144 or not self.reader_threads[i].is_alive() \ 145 and not self.reader_returned_eof[i]: 146 logging.info('read() - starting thread for Reader #%d', i) 147 self.reader_returned_eof[i] = False 148 thread = threading.Thread(target=self._run_reader, args=(i,), 149 daemon=True) 150 self.reader_threads[i] = thread 151 thread.start() 152 153 # Now notify all threads that we do in fact need a record. 154 self.queue_needs_record.set() 155 156 # Keep checking/sleeping until we've either got a record in the 157 # queue or all readers have given us an EOF. 158 while False in self.reader_returned_eof: 159 logging.debug('read() - waiting for queue lock') 160 with self.queue_lock: 161 logging.debug('read() - acquired queue lock, queue length is %d', 162 len(self.queue)) 163 if self.queue: 164 record = self.queue.pop(0) 165 if not self.queue: 166 self.queue_has_record.clear() # only set/clear inside queue_lock 167 168 logging.debug('read() - got record') 169 return self._apply_transforms(record) 170 else: 171 self.queue_has_record.clear() 172 173 # If here, nothing in queue yet. Wait 174 logging.debug('read() - clear of queue lock, waiting for record') 175 self.queue_has_record.wait(READER_TIMEOUT_WAIT) 176 177 if not self.queue_has_record.is_set(): 178 logging.debug('read() - timed out waiting for record. Looping') 179 logging.debug('read() - readers returned EOF: %s', 180 self.reader_returned_eof) 181 182 # All readers have given us an EOF 183 logging.debug('read() - all threads returned None; returning None') 184 return None 185 186 ############################ 187 def _run_reader(self, index): 188 """ 189 Cycle through reading records from a readers[i] and putting them in queue. 190 """ 191 while True: 192 logging.debug(' Reader #%d waiting until record needed.', index) 193 self.queue_needs_record.wait(READER_TIMEOUT_WAIT) 194 195 # If we timed out waiting for someone to need a record, go 196 # home. We'll get started up again if needed. 197 if not self.queue_needs_record.is_set(): 198 logging.debug(' Reader #%d timed out - exiting.', index) 199 return 200 201 # Else someone needs a record - leap into action 202 logging.debug(' Reader #%d waking up - record needed!', index) 203 204 # Guard against re-entry 205 with self.reader_locks[index]: 206 record = self.readers[index].read() 207 208 # If reader returns None, it's done and has no more data for 209 # us. Note that it's given us an EOF and exit. 210 if record is None: 211 logging.info(' Reader #%d returned None, is done', index) 212 self.reader_returned_eof[index] = True 213 return 214 215 logging.debug(' Reader #%d has record, released reader_lock.', index) 216 217 # Add record to queue and note that an append event has 218 # happened. 219 with self.queue_lock: 220 # No one else can mess with queue while we add record. Once we've 221 # added it, set flag to say there's something in the queue. 222 logging.debug(' Reader #%d has queue lock - adding and notifying.', 223 index) 224 self.queue.append(record) 225 self.queue_has_record.set() 226 self.queue_needs_record.clear() 227 228 # Now clear of queue_lock 229 logging.debug(' Reader #%d released queue_lock - looping', index) 230 231 ############################ 232 def _apply_transforms(self, record): 233 """ 234 Apply the transforms in series. 235 """ 236 if record: 237 for t in self.transforms: 238 record = t.transform(record) 239 if not record: 240 break 241 return record
16class ComposedReader(Reader): 17 """ 18 Read lines from one or more Readers (in parallel) and process their 19 responses through zero or more Transforms (in series). 20 21 NOTE: we make the rash assumption that transforms are thread-safe, 22 that is, that no mischief or corrupted internal state will result if 23 more than one thread calls a transform at the same time. To be 24 thread-safe, a transform must protect any changes to its internal 25 state with a non-re-entrant thread lock, as described in the threading 26 module. 27 28 Also NOTE: Most of the messy logic in this class comes from the desire 29 to only call read() on our component readers when we actually need new 30 records (NOTE: this desire may be misplaced!). 31 32 So when we get a request, we fire up threads and ask each of our 33 readers for a record. We return the first one we get, and let the 34 others pile up in a queue that we'll feed from the next time we're 35 asked. 36 37 But we don't want to fire up a new thread for each reader every time 38 the queue is empty, so we have threads (in run_reader()) hang out for 39 a little while, waiting for another queue_needs_record event. If they 40 get one, the call their own read() methods again. If they haven't been 41 called on in READER_TIMEOUT_WAIT seconds, they exit, but will get 42 fired up again by read() if/when the queue is empty and we're is asked 43 for another record. 44 45 It's important to have the run_reader threads time out, or any process 46 using a ComposedReader will never naturally terminate. 47 """ 48 ############################ 49 50 def __init__(self, readers, transforms=[], **kwargs): 51 """ 52 Instantiation: 53 ``` 54 reader = ComposedReader(readers, transforms=[], check_format=True) 55 56 readers A single Reader or a list of Readers. 57 58 transforms A single Transform or list of zero or more Transforms. 59 ``` 60 Use: 61 ``` 62 record = reader.read() 63 ``` 64 Sample: 65 ``` 66 reader = ComposedReader(readers=[UDPReader(port=6221), 67 UDPReader(port=6223)], 68 transforms=[TimestampTransform()]) 69 ``` 70 """ 71 super().__init__(**kwargs) 72 73 # Make readers a list, even if it's only a single reader. 74 self.readers = readers if type(readers) is list else [readers] 75 self.num_readers = len(self.readers) 76 77 # Transforms can be empty. But if not empty, make it a list, even 78 # if it's only a single transform. 79 if not isinstance(transforms, list): 80 self.transforms = [transforms] 81 else: 82 self.transforms = transforms 83 84 # List where we're going to store reader threads 85 self.reader_threads = [None] * self.num_readers 86 87 # Whether reader[i] has returned EOF since we've last asked it 88 self.reader_returned_eof = [False] * self.num_readers 89 90 # One lock per reader, to save us from accidental re-entry 91 self.reader_locks = [threading.Lock() for i in range(self.num_readers)] 92 93 # Queue where we'll store extra records, and lock so only one 94 # thread can touch queue at a time 95 self.queue = [] 96 self.queue_lock = threading.Lock() 97 98 # The two events, queue_has_record and queue_needs_record interact 99 # in a sort of a dance: 100 # 101 # has = False, need = False: Everything is quiescent 102 # has = False, need = True: A request has been made, call readers 103 # has = True, need = True: Momentary condition when we get needed rec 104 # has = True, need = False: We've got spare records in the queue 105 # 106 # Set when the queue is empty and we need a record 107 self.queue_needs_record = threading.Event() 108 109 # Set when a reader adds something to the queue 110 self.queue_has_record = threading.Event() 111 112 ############################ 113 def read(self): 114 """ 115 Get the next record from queue or readers. 116 """ 117 # If we only have one reader, there's no point making things 118 # complicated. Just read, transform, return. 119 if len(self.readers) == 1: 120 return self._apply_transforms(self.readers[0].read()) 121 122 # Do we have anything in the queue? Note: safe to check outside of 123 # lock, because we're the only method that actually *removes* 124 # anything. So if tests True here, we're assured that there's 125 # something there, and we lock before retrieving it. Advantage of 126 # doing it this way is that we don't tie up queue lock while 127 # processing transforms. 128 if self.queue: 129 logging.debug('read() - read requested; queue len is %d', 130 len(self.queue)) 131 with self.queue_lock: 132 record = self.queue.pop(0) 133 return self._apply_transforms(record) 134 135 # If here, nothing's in the queue. Note that, if we wanted to be 136 # careful to never unnecessarily ask for more records, we should 137 # put a lock around this, but the failure mode is somewhat benign: 138 # we ask for more records when some are already on the way. 139 logging.debug('read() - read requested and nothing in the queue.') 140 141 # Some threads may have timed out while waiting to be called to 142 # action; restart them. 143 for i in range(len(self.readers)): 144 if not self.reader_threads[i] \ 145 or not self.reader_threads[i].is_alive() \ 146 and not self.reader_returned_eof[i]: 147 logging.info('read() - starting thread for Reader #%d', i) 148 self.reader_returned_eof[i] = False 149 thread = threading.Thread(target=self._run_reader, args=(i,), 150 daemon=True) 151 self.reader_threads[i] = thread 152 thread.start() 153 154 # Now notify all threads that we do in fact need a record. 155 self.queue_needs_record.set() 156 157 # Keep checking/sleeping until we've either got a record in the 158 # queue or all readers have given us an EOF. 159 while False in self.reader_returned_eof: 160 logging.debug('read() - waiting for queue lock') 161 with self.queue_lock: 162 logging.debug('read() - acquired queue lock, queue length is %d', 163 len(self.queue)) 164 if self.queue: 165 record = self.queue.pop(0) 166 if not self.queue: 167 self.queue_has_record.clear() # only set/clear inside queue_lock 168 169 logging.debug('read() - got record') 170 return self._apply_transforms(record) 171 else: 172 self.queue_has_record.clear() 173 174 # If here, nothing in queue yet. Wait 175 logging.debug('read() - clear of queue lock, waiting for record') 176 self.queue_has_record.wait(READER_TIMEOUT_WAIT) 177 178 if not self.queue_has_record.is_set(): 179 logging.debug('read() - timed out waiting for record. Looping') 180 logging.debug('read() - readers returned EOF: %s', 181 self.reader_returned_eof) 182 183 # All readers have given us an EOF 184 logging.debug('read() - all threads returned None; returning None') 185 return None 186 187 ############################ 188 def _run_reader(self, index): 189 """ 190 Cycle through reading records from a readers[i] and putting them in queue. 191 """ 192 while True: 193 logging.debug(' Reader #%d waiting until record needed.', index) 194 self.queue_needs_record.wait(READER_TIMEOUT_WAIT) 195 196 # If we timed out waiting for someone to need a record, go 197 # home. We'll get started up again if needed. 198 if not self.queue_needs_record.is_set(): 199 logging.debug(' Reader #%d timed out - exiting.', index) 200 return 201 202 # Else someone needs a record - leap into action 203 logging.debug(' Reader #%d waking up - record needed!', index) 204 205 # Guard against re-entry 206 with self.reader_locks[index]: 207 record = self.readers[index].read() 208 209 # If reader returns None, it's done and has no more data for 210 # us. Note that it's given us an EOF and exit. 211 if record is None: 212 logging.info(' Reader #%d returned None, is done', index) 213 self.reader_returned_eof[index] = True 214 return 215 216 logging.debug(' Reader #%d has record, released reader_lock.', index) 217 218 # Add record to queue and note that an append event has 219 # happened. 220 with self.queue_lock: 221 # No one else can mess with queue while we add record. Once we've 222 # added it, set flag to say there's something in the queue. 223 logging.debug(' Reader #%d has queue lock - adding and notifying.', 224 index) 225 self.queue.append(record) 226 self.queue_has_record.set() 227 self.queue_needs_record.clear() 228 229 # Now clear of queue_lock 230 logging.debug(' Reader #%d released queue_lock - looping', index) 231 232 ############################ 233 def _apply_transforms(self, record): 234 """ 235 Apply the transforms in series. 236 """ 237 if record: 238 for t in self.transforms: 239 record = t.transform(record) 240 if not record: 241 break 242 return record
Read lines from one or more Readers (in parallel) and process their responses through zero or more Transforms (in series).
NOTE: we make the rash assumption that transforms are thread-safe, that is, that no mischief or corrupted internal state will result if more than one thread calls a transform at the same time. To be thread-safe, a transform must protect any changes to its internal state with a non-re-entrant thread lock, as described in the threading module.
Also NOTE: Most of the messy logic in this class comes from the desire to only call read() on our component readers when we actually need new records (NOTE: this desire may be misplaced!).
So when we get a request, we fire up threads and ask each of our readers for a record. We return the first one we get, and let the others pile up in a queue that we'll feed from the next time we're asked.
But we don't want to fire up a new thread for each reader every time the queue is empty, so we have threads (in run_reader()) hang out for a little while, waiting for another queue_needs_record event. If they get one, the call their own read() methods again. If they haven't been called on in READER_TIMEOUT_WAIT seconds, they exit, but will get fired up again by read() if/when the queue is empty and we're is asked for another record.
It's important to have the run_reader threads time out, or any process using a ComposedReader will never naturally terminate.
50 def __init__(self, readers, transforms=[], **kwargs): 51 """ 52 Instantiation: 53 ``` 54 reader = ComposedReader(readers, transforms=[], check_format=True) 55 56 readers A single Reader or a list of Readers. 57 58 transforms A single Transform or list of zero or more Transforms. 59 ``` 60 Use: 61 ``` 62 record = reader.read() 63 ``` 64 Sample: 65 ``` 66 reader = ComposedReader(readers=[UDPReader(port=6221), 67 UDPReader(port=6223)], 68 transforms=[TimestampTransform()]) 69 ``` 70 """ 71 super().__init__(**kwargs) 72 73 # Make readers a list, even if it's only a single reader. 74 self.readers = readers if type(readers) is list else [readers] 75 self.num_readers = len(self.readers) 76 77 # Transforms can be empty. But if not empty, make it a list, even 78 # if it's only a single transform. 79 if not isinstance(transforms, list): 80 self.transforms = [transforms] 81 else: 82 self.transforms = transforms 83 84 # List where we're going to store reader threads 85 self.reader_threads = [None] * self.num_readers 86 87 # Whether reader[i] has returned EOF since we've last asked it 88 self.reader_returned_eof = [False] * self.num_readers 89 90 # One lock per reader, to save us from accidental re-entry 91 self.reader_locks = [threading.Lock() for i in range(self.num_readers)] 92 93 # Queue where we'll store extra records, and lock so only one 94 # thread can touch queue at a time 95 self.queue = [] 96 self.queue_lock = threading.Lock() 97 98 # The two events, queue_has_record and queue_needs_record interact 99 # in a sort of a dance: 100 # 101 # has = False, need = False: Everything is quiescent 102 # has = False, need = True: A request has been made, call readers 103 # has = True, need = True: Momentary condition when we get needed rec 104 # has = True, need = False: We've got spare records in the queue 105 # 106 # Set when the queue is empty and we need a record 107 self.queue_needs_record = threading.Event() 108 109 # Set when a reader adds something to the queue 110 self.queue_has_record = threading.Event()
Instantiation:
reader = ComposedReader(readers, transforms=[], check_format=True)
readers A single Reader or a list of Readers.
transforms A single Transform or list of zero or more Transforms.
Use:
record = reader.read()
Sample:
reader = ComposedReader(readers=[UDPReader(port=6221),
UDPReader(port=6223)],
transforms=[TimestampTransform()])
113 def read(self): 114 """ 115 Get the next record from queue or readers. 116 """ 117 # If we only have one reader, there's no point making things 118 # complicated. Just read, transform, return. 119 if len(self.readers) == 1: 120 return self._apply_transforms(self.readers[0].read()) 121 122 # Do we have anything in the queue? Note: safe to check outside of 123 # lock, because we're the only method that actually *removes* 124 # anything. So if tests True here, we're assured that there's 125 # something there, and we lock before retrieving it. Advantage of 126 # doing it this way is that we don't tie up queue lock while 127 # processing transforms. 128 if self.queue: 129 logging.debug('read() - read requested; queue len is %d', 130 len(self.queue)) 131 with self.queue_lock: 132 record = self.queue.pop(0) 133 return self._apply_transforms(record) 134 135 # If here, nothing's in the queue. Note that, if we wanted to be 136 # careful to never unnecessarily ask for more records, we should 137 # put a lock around this, but the failure mode is somewhat benign: 138 # we ask for more records when some are already on the way. 139 logging.debug('read() - read requested and nothing in the queue.') 140 141 # Some threads may have timed out while waiting to be called to 142 # action; restart them. 143 for i in range(len(self.readers)): 144 if not self.reader_threads[i] \ 145 or not self.reader_threads[i].is_alive() \ 146 and not self.reader_returned_eof[i]: 147 logging.info('read() - starting thread for Reader #%d', i) 148 self.reader_returned_eof[i] = False 149 thread = threading.Thread(target=self._run_reader, args=(i,), 150 daemon=True) 151 self.reader_threads[i] = thread 152 thread.start() 153 154 # Now notify all threads that we do in fact need a record. 155 self.queue_needs_record.set() 156 157 # Keep checking/sleeping until we've either got a record in the 158 # queue or all readers have given us an EOF. 159 while False in self.reader_returned_eof: 160 logging.debug('read() - waiting for queue lock') 161 with self.queue_lock: 162 logging.debug('read() - acquired queue lock, queue length is %d', 163 len(self.queue)) 164 if self.queue: 165 record = self.queue.pop(0) 166 if not self.queue: 167 self.queue_has_record.clear() # only set/clear inside queue_lock 168 169 logging.debug('read() - got record') 170 return self._apply_transforms(record) 171 else: 172 self.queue_has_record.clear() 173 174 # If here, nothing in queue yet. Wait 175 logging.debug('read() - clear of queue lock, waiting for record') 176 self.queue_has_record.wait(READER_TIMEOUT_WAIT) 177 178 if not self.queue_has_record.is_set(): 179 logging.debug('read() - timed out waiting for record. Looping') 180 logging.debug('read() - readers returned EOF: %s', 181 self.reader_returned_eof) 182 183 # All readers have given us an EOF 184 logging.debug('read() - all threads returned None; returning None') 185 return None
Get the next record from queue or readers.