| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -61,75 +61,6 @@ def __init__(self) -> None: | |||
| 61 | 61 | # Functions prefixed with async_ are NOT threadsafe and must | |
| 62 | 62 | # be run in the event loop. | |
| 63 | 63 | ||
| 64 | - def _async_add(self, record: _DNSRecord) -> bool: | ||
| 65 | - """Store a record, returning True when it was not cached before. | ||
| 66 | - | ||
| 67 | - Event loop only; not threadsafe. | ||
| 68 | - """ | ||
| 69 | - # Previously storage of records was implemented as a list | ||
| 70 | - # instead a dict. Since DNSRecords are now hashable, the implementation | ||
| 71 | - # uses a dict to ensure that adding a new record to the cache | ||
| 72 | - # replaces any existing records that are __eq__ to each other which | ||
| 73 | - # removes the risk that accessing the cache from the wrong | ||
| 74 | - # direction would return the old incorrect entry. | ||
| 75 | - store = self.cache.get(record.key) | ||
| 76 | - is_new = store is None or record not in store | ||
| 77 | - # Bound total cache size; evict closest-to-expiration entry to | ||
| 78 | - # make room before inserting a new record. Prevents a LAN-local | ||
| 79 | - # flood of unique-name records from growing the cache without | ||
| 80 | - # bound (RFC 6762 §10 advisory caching, defense-in-depth). | ||
| 81 | - if is_new and self._total_records >= _MAX_CACHE_RECORDS: | ||
| 82 | - self._async_evict_oldest() | ||
| 83 | - # The victim may have been the last record under | ||
| 84 | - # ``record.key``, in which case ``_remove_key`` deleted | ||
| 85 | - # the bucket. Re-fetch before creating below. | ||
| 86 | - store = self.cache.get(record.key) | ||
| 87 | - if store is None: | ||
| 88 | - store = self.cache[record.key] = {} | ||
| 89 | - new = is_new and not isinstance(record, DNSNsec) | ||
| 90 | - if is_new: | ||
| 91 | - self._total_records += 1 | ||
| 92 | - store[record] = record | ||
| 93 | - when = record.created + (record.ttl * 1000) | ||
| 94 | - if self._expirations.get(record) != when: | ||
| 95 | - heappush(self._expire_heap, (when, record)) | ||
| 96 | - self._expirations[record] = when | ||
| 97 | - # Re-adds of an existing record with a new TTL push a fresh | ||
| 98 | - # entry but leave the prior tuple behind as stale, so a peer | ||
| 99 | - # that just replays cached records can grow ``_expire_heap`` | ||
| 100 | - # without ever tripping the cap. Rebuild when stale entries | ||
| 101 | - # dominate. | ||
| 102 | - self._maybe_rebuild_heap() | ||
| 103 | - | ||
| 104 | - if isinstance(record, DNSService): | ||
| 105 | - service_record = record | ||
| 106 | - if (service_store := self.service_cache.get(service_record.server_key)) is None: | ||
| 107 | - service_store = self.service_cache[service_record.server_key] = {} | ||
| 108 | - service_store[service_record] = service_record | ||
| 109 | - return new | ||
| 110 | - | ||
| 111 | - def _async_evict_oldest(self) -> None: | ||
| 112 | - """Drop the closest-to-expiration record to make room for a new one.""" | ||
| 113 | - while self._expire_heap: | ||
| 114 | - when_record = heappop(self._expire_heap) | ||
| 115 | - record = when_record[1] | ||
| 116 | - if self._expirations.get(record) != when_record[0]: | ||
| 117 | - continue | ||
| 118 | - self._async_remove(record) | ||
| 119 | - return | ||
| 120 | - | ||
| 121 | - def _maybe_rebuild_heap(self) -> None: | ||
| 122 | - """Rebuild ``_expire_heap`` when stale entries dominate live ones.""" | ||
| 123 | - expire_heap_len = len(self._expire_heap) | ||
| 124 | - if ( | ||
| 125 | - expire_heap_len > _MIN_SCHEDULED_RECORD_EXPIRATION | ||
| 126 | - and expire_heap_len > len(self._expirations) * 2 | ||
| 127 | - ): | ||
| 128 | - self._expire_heap = [ | ||
| 129 | - entry for entry in self._expire_heap if self._expirations.get(entry[1]) == entry[0] | ||
| 130 | - ] | ||
| 131 | - heapify(self._expire_heap) | ||
| 132 | - | ||
| 133 | 64 | def async_add_records(self, entries: Iterable[DNSRecord]) -> bool: | |
| 134 | 65 | """Add multiple records. | |
| 135 | 66 | ||
@@ -143,22 +74,33 @@ def async_add_records(self, entries: Iterable[DNSRecord]) -> bool: | |||
| 143 | 74 | new = True | |
| 144 | 75 | return new | |
| 145 | 76 | ||
| 146 | - def _async_remove(self, record: _DNSRecord) -> None: | ||
| 147 | - """Drop a record from the cache. Event loop only; not threadsafe.""" | ||
| 148 | - if isinstance(record, DNSService): | ||
| 149 | - service_record = record | ||
| 150 | - _remove_key(self.service_cache, service_record.server_key, service_record) | ||
| 151 | - _remove_key(self.cache, record.key, record) | ||
| 152 | - self._expirations.pop(record, None) | ||
| 153 | - self._total_records -= 1 | ||
| 154 | - | ||
| 155 | - def async_remove_records(self, entries: Iterable[DNSRecord]) -> None: | ||
| 156 | - """Remove multiple records. | ||
| 77 | + def async_all_by_details(self, name: _str, type_: _int, class_: _int) -> list[DNSRecord]: | ||
| 78 | + """Gets all matching entries by details. | ||
| 157 | 79 | ||
| 158 | - This function must be run in from event loop. | ||
| 80 | + This function is not thread-safe and must be called from | ||
| 81 | + the event loop. | ||
| 159 | 82 | """ | |
| 160 | - for entry in entries: | ||
| 161 | - self._async_remove(entry) | ||
| 83 | + key = name.lower() | ||
| 84 | + records = self.cache.get(key) | ||
| 85 | + matches: list[DNSRecord] = [] | ||
| 86 | + if records is None: | ||
| 87 | + return matches | ||
| 88 | + for record in records.values(): | ||
| 89 | + if type_ == record.type and class_ == record.class_: | ||
| 90 | + matches.append(record) | ||
| 91 | + return matches | ||
| 92 | + | ||
| 93 | + def async_entries_with_name(self, name: str) -> list[DNSRecord]: | ||
| 94 | + """All cached records for a name; event loop only, not threadsafe.""" | ||
| 95 | + return self.entries_with_name(name) | ||
| 96 | + | ||
| 97 | + def async_entries_with_server(self, name: str) -> list[DNSRecord]: | ||
| 98 | + """All cached records for a server name; event loop only, not threadsafe.""" | ||
| 99 | + return self.entries_with_server(name) | ||
| 100 | + | ||
| 101 | + # The below functions are threadsafe and do not need to be run in the | ||
| 102 | + # event loop, however they all make copies so they significantly | ||
| 103 | + # inefficient. | ||
| 162 | 104 | ||
| 163 | 105 | def async_expire(self, now: _float) -> list[DNSRecord]: | |
| 164 | 106 | """Purge expired entries from the cache. | |
@@ -197,33 +139,53 @@ def async_get_unique(self, entry: _UniqueRecordsType) -> DNSRecord | None: | |||
| 197 | 139 | return None | |
| 198 | 140 | return store.get(entry) | |
| 199 | 141 | ||
| 200 | - def async_all_by_details(self, name: _str, type_: _int, class_: _int) -> list[DNSRecord]: | ||
| 201 | - """Gets all matching entries by details. | ||
| 142 | + def async_mark_unique_records_older_than_1s_to_expire( | ||
| 143 | + self, | ||
| 144 | + unique_types: set[_UniqueType], | ||
| 145 | + answers: Iterable[DNSRecord], | ||
| 146 | + now: _float, | ||
| 147 | + ) -> None: | ||
| 148 | + # rfc6762#section-10.2 para 2 | ||
| 149 | + # Since unique is set, all old records with that name, rrtype, | ||
| 150 | + # and rrclass that were received more than one second ago are declared | ||
| 151 | + # invalid, and marked to expire from the cache in one second. | ||
| 152 | + answers_rrset = set(answers) | ||
| 153 | + for name, type_, class_ in unique_types: | ||
| 154 | + for record in self.async_all_by_details(name, type_, class_): | ||
| 155 | + created_double = record.created | ||
| 156 | + if (now - created_double > _ONE_SECOND) and record not in answers_rrset: | ||
| 157 | + # Expire in 1s | ||
| 158 | + self._async_set_created_ttl(record, now, 1) | ||
| 202 | 159 | ||
| 203 | - This function is not thread-safe and must be called from | ||
| 204 | - the event loop. | ||
| 160 | + def async_remove_records(self, entries: Iterable[DNSRecord]) -> None: | ||
| 161 | + """Remove multiple records. | ||
| 162 | + | ||
| 163 | + This function must be run in from event loop. | ||
| 205 | 164 | """ | |
| 206 | - key = name.lower() | ||
| 207 | - records = self.cache.get(key) | ||
| 208 | - matches: list[DNSRecord] = [] | ||
| 209 | - if records is None: | ||
| 210 | - return matches | ||
| 211 | - for record in records.values(): | ||
| 212 | - if type_ == record.type and class_ == record.class_: | ||
| 213 | - matches.append(record) | ||
| 214 | - return matches | ||
| 165 | + for entry in entries: | ||
| 166 | + self._async_remove(entry) | ||
| 215 | 167 | ||
| 216 | - def async_entries_with_name(self, name: str) -> list[DNSRecord]: | ||
| 217 | - """All cached records for a name; event loop only, not threadsafe.""" | ||
| 218 | - return self.entries_with_name(name) | ||
| 168 | + def current_entry_with_name_and_alias(self, name: str, alias: str) -> DNSRecord | None: | ||
| 169 | + now = current_time_millis() | ||
| 170 | + for record in reversed(self.entries_with_name(name)): | ||
| 171 | + if ( | ||
| 172 | + record.type == _TYPE_PTR | ||
| 173 | + and not record.is_expired(now) | ||
| 174 | + and cast(DNSPointer, record).alias == alias | ||
| 175 | + ): | ||
| 176 | + return record | ||
| 177 | + return None | ||
| 219 | 178 | ||
| 220 | - def async_entries_with_server(self, name: str) -> list[DNSRecord]: | ||
| 221 | - """All cached records for a server name; event loop only, not threadsafe.""" | ||
| 222 | - return self.entries_with_server(name) | ||
| 179 | + def entries_with_name(self, name: str) -> list[DNSRecord]: | ||
| 180 | + if entries := self.cache.get(name.lower()): | ||
| 181 | + return list(entries.values()) | ||
| 182 | + return [] | ||
| 223 | 183 | ||
| 224 | - # The below functions are threadsafe and do not need to be run in the | ||
| 225 | - # event loop, however they all make copies so they significantly | ||
| 226 | - # inefficient. | ||
| 184 | + def entries_with_server(self, server: str) -> list[DNSRecord]: | ||
| 185 | + """All cached records whose server field matches.""" | ||
| 186 | + if entries := self.service_cache.get(server.lower()): | ||
| 187 | + return list(entries.values()) | ||
| 188 | + return [] | ||
| 227 | 189 | ||
| 228 | 190 | def get(self, entry: DNSEntry) -> DNSRecord | None: | |
| 229 | 191 | if isinstance(entry, _UNIQUE_RECORD_TYPES): | |
@@ -233,6 +195,14 @@ def get(self, entry: DNSEntry) -> DNSRecord | None: | |||
| 233 | 195 | return cached_entry | |
| 234 | 196 | return None | |
| 235 | 197 | ||
| 198 | + def get_all_by_details(self, name: str, type_: _int, class_: _int) -> list[DNSRecord]: | ||
| 199 | + """Gets all matching entries by details.""" | ||
| 200 | + key = name.lower() | ||
| 201 | + records = self.cache.get(key) | ||
| 202 | + if records is None: | ||
| 203 | + return [] | ||
| 204 | + return [entry for entry in list(records.values()) if type_ == entry.type and class_ == entry.class_] | ||
| 205 | + | ||
| 236 | 206 | def get_by_details(self, name: str, type_: _int, class_: _int) -> DNSRecord | None: | |
| 237 | 207 | """Gets the first matching entry by details. Returns None if no entries match. | |
| 238 | 208 | ||
@@ -254,61 +224,91 @@ def get_by_details(self, name: str, type_: _int, class_: _int) -> DNSRecord | No | |||
| 254 | 224 | return cached_entry | |
| 255 | 225 | return None | |
| 256 | 226 | ||
| 257 | - def get_all_by_details(self, name: str, type_: _int, class_: _int) -> list[DNSRecord]: | ||
| 258 | - """Gets all matching entries by details.""" | ||
| 259 | - key = name.lower() | ||
| 260 | - records = self.cache.get(key) | ||
| 261 | - if records is None: | ||
| 262 | - return [] | ||
| 263 | - return [entry for entry in list(records.values()) if type_ == entry.type and class_ == entry.class_] | ||
| 227 | + def names(self) -> list[str]: | ||
| 228 | + """Return a copy of the list of current cache names.""" | ||
| 229 | + return list(self.cache) | ||
| 264 | 230 | ||
| 265 | - def entries_with_server(self, server: str) -> list[DNSRecord]: | ||
| 266 | - """All cached records whose server field matches.""" | ||
| 267 | - if entries := self.service_cache.get(server.lower()): | ||
| 268 | - return list(entries.values()) | ||
| 269 | - return [] | ||
| 231 | + def _async_add(self, record: _DNSRecord) -> bool: | ||
| 232 | + """Store a record, returning True when it was not cached before. | ||
| 270 | 233 | ||
| 271 | - def entries_with_name(self, name: str) -> list[DNSRecord]: | ||
| 272 | - if entries := self.cache.get(name.lower()): | ||
| 273 | - return list(entries.values()) | ||
| 274 | - return [] | ||
| 234 | + Event loop only; not threadsafe. | ||
| 235 | + """ | ||
| 236 | + # Previously storage of records was implemented as a list | ||
| 237 | + # instead a dict. Since DNSRecords are now hashable, the implementation | ||
| 238 | + # uses a dict to ensure that adding a new record to the cache | ||
| 239 | + # replaces any existing records that are __eq__ to each other which | ||
| 240 | + # removes the risk that accessing the cache from the wrong | ||
| 241 | + # direction would return the old incorrect entry. | ||
| 242 | + store = self.cache.get(record.key) | ||
| 243 | + is_new = store is None or record not in store | ||
| 244 | + # Bound total cache size; evict closest-to-expiration entry to | ||
| 245 | + # make room before inserting a new record. Prevents a LAN-local | ||
| 246 | + # flood of unique-name records from growing the cache without | ||
| 247 | + # bound (RFC 6762 §10 advisory caching, defense-in-depth). | ||
| 248 | + if is_new and self._total_records >= _MAX_CACHE_RECORDS: | ||
| 249 | + self._async_evict_oldest() | ||
| 250 | + # The victim may have been the last record under | ||
| 251 | + # ``record.key``, in which case ``_remove_key`` deleted | ||
| 252 | + # the bucket. Re-fetch before creating below. | ||
| 253 | + store = self.cache.get(record.key) | ||
| 254 | + if store is None: | ||
| 255 | + store = self.cache[record.key] = {} | ||
| 256 | + new = is_new and not isinstance(record, DNSNsec) | ||
| 257 | + if is_new: | ||
| 258 | + self._total_records += 1 | ||
| 259 | + store[record] = record | ||
| 260 | + when = record.created + (record.ttl * 1000) | ||
| 261 | + if self._expirations.get(record) != when: | ||
| 262 | + heappush(self._expire_heap, (when, record)) | ||
| 263 | + self._expirations[record] = when | ||
| 264 | + # Re-adds of an existing record with a new TTL push a fresh | ||
| 265 | + # entry but leave the prior tuple behind as stale, so a peer | ||
| 266 | + # that just replays cached records can grow ``_expire_heap`` | ||
| 267 | + # without ever tripping the cap. Rebuild when stale entries | ||
| 268 | + # dominate. | ||
| 269 | + self._maybe_rebuild_heap() | ||
| 275 | 270 | ||
| 276 | - def current_entry_with_name_and_alias(self, name: str, alias: str) -> DNSRecord | None: | ||
| 277 | - now = current_time_millis() | ||
| 278 | - for record in reversed(self.entries_with_name(name)): | ||
| 279 | - if ( | ||
| 280 | - record.type == _TYPE_PTR | ||
| 281 | - and not record.is_expired(now) | ||
| 282 | - and cast(DNSPointer, record).alias == alias | ||
| 283 | - ): | ||
| 284 | - return record | ||
| 285 | - return None | ||
| 271 | + if isinstance(record, DNSService): | ||
| 272 | + service_record = record | ||
| 273 | + if (service_store := self.service_cache.get(service_record.server_key)) is None: | ||
| 274 | + service_store = self.service_cache[service_record.server_key] = {} | ||
| 275 | + service_store[service_record] = service_record | ||
| 276 | + return new | ||
| 286 | 277 | ||
| 287 | - def names(self) -> list[str]: | ||
| 288 | - """Return a copy of the list of current cache names.""" | ||
| 289 | - return list(self.cache) | ||
| 278 | + def _async_evict_oldest(self) -> None: | ||
| 279 | + """Drop the closest-to-expiration record to make room for a new one.""" | ||
| 280 | + while self._expire_heap: | ||
| 281 | + when_record = heappop(self._expire_heap) | ||
| 282 | + record = when_record[1] | ||
| 283 | + if self._expirations.get(record) != when_record[0]: | ||
| 284 | + continue | ||
| 285 | + self._async_remove(record) | ||
| 286 | + return | ||
| 290 | 287 | ||
| 291 | - def async_mark_unique_records_older_than_1s_to_expire( | ||
| 292 | - self, | ||
| 293 | - unique_types: set[_UniqueType], | ||
| 294 | - answers: Iterable[DNSRecord], | ||
| 295 | - now: _float, | ||
| 296 | - ) -> None: | ||
| 297 | - # rfc6762#section-10.2 para 2 | ||
| 298 | - # Since unique is set, all old records with that name, rrtype, | ||
| 299 | - # and rrclass that were received more than one second ago are declared | ||
| 300 | - # invalid, and marked to expire from the cache in one second. | ||
| 301 | - answers_rrset = set(answers) | ||
| 302 | - for name, type_, class_ in unique_types: | ||
| 303 | - for record in self.async_all_by_details(name, type_, class_): | ||
| 304 | - created_double = record.created | ||
| 305 | - if (now - created_double > _ONE_SECOND) and record not in answers_rrset: | ||
| 306 | - # Expire in 1s | ||
| 307 | - self._async_set_created_ttl(record, now, 1) | ||
| 288 | + def _async_remove(self, record: _DNSRecord) -> None: | ||
| 289 | + """Drop a record from the cache. Event loop only; not threadsafe.""" | ||
| 290 | + if isinstance(record, DNSService): | ||
| 291 | + service_record = record | ||
| 292 | + _remove_key(self.service_cache, service_record.server_key, service_record) | ||
| 293 | + _remove_key(self.cache, record.key, record) | ||
| 294 | + self._expirations.pop(record, None) | ||
| 295 | + self._total_records -= 1 | ||
| 308 | 296 | ||
| 309 | 297 | def _async_set_created_ttl(self, record: DNSRecord, now: _float, ttl: _int) -> None: | |
| 310 | 298 | """Stamp a record with a new ttl and creation moment, then cache it.""" | |
| 311 | 299 | # It would be better if we made a copy instead of mutating the record | |
| 312 | 300 | # in place, but records currently don't have a copy method. | |
| 313 | 301 | record._set_created_ttl(now, ttl) | |
| 314 | 302 | self._async_add(record) | |
| 303 | + | ||
| 304 | + def _maybe_rebuild_heap(self) -> None: | ||
| 305 | + """Rebuild ``_expire_heap`` when stale entries dominate live ones.""" | ||
| 306 | + expire_heap_len = len(self._expire_heap) | ||
| 307 | + if ( | ||
| 308 | + expire_heap_len > _MIN_SCHEDULED_RECORD_EXPIRATION | ||
| 309 | + and expire_heap_len > len(self._expirations) * 2 | ||
| 310 | + ): | ||
| 311 | + self._expire_heap = [ | ||
| 312 | + entry for entry in self._expire_heap if self._expirations.get(entry[1]) == entry[0] | ||
| 313 | + ] | ||
| 314 | + heapify(self._expire_heap) | ||
| Back | FazBrowse Home | New Git URL |
0 commit comments