1
|
|
|
|
2
|
|
|
import time |
3
|
|
|
from logging import LogRecord |
4
|
|
|
from threading import Lock |
5
|
|
|
from typing import Generic, Self, TypeVar |
6
|
|
|
|
7
|
|
|
T = TypeVar("T") |
8
|
|
|
|
9
|
|
|
|
10
|
|
|
class _DoublyLinkedList(Generic[T]): |
11
|
|
|
value: T |
12
|
|
|
previous: Self |
13
|
|
|
next: Self |
14
|
|
|
|
15
|
|
|
def __init__(self, value: T): |
16
|
|
|
self.value = value |
17
|
|
|
self.previous = self |
18
|
|
|
self.next = self |
19
|
|
|
|
20
|
|
|
def swap_next(self, other: Self): |
21
|
|
|
|
22
|
|
|
curr_next = self.next |
23
|
|
|
other_curr_next = other.next |
24
|
|
|
|
25
|
|
|
self.next = other_curr_next |
26
|
|
|
other_curr_next.previous = self |
27
|
|
|
other.next = curr_next |
28
|
|
|
curr_next.previous = other |
29
|
|
|
|
30
|
|
|
def swap_previous(self, other: Self): |
31
|
|
|
curr_prev = self.previous |
32
|
|
|
other_curr_prev = other.previous |
33
|
|
|
|
34
|
|
|
self.previous = other_curr_prev |
35
|
|
|
other_curr_prev.next = self |
36
|
|
|
other.previous = curr_prev |
37
|
|
|
curr_prev.next = other |
38
|
|
|
|
39
|
|
|
def remove(self): |
40
|
|
|
self.swap_next(self.previous) |
41
|
|
|
|
42
|
|
|
def __iter__(self): |
43
|
|
|
yield self.value |
44
|
|
|
curr = self.next |
45
|
|
|
while curr is not self: |
46
|
|
|
yield curr.value |
47
|
|
|
curr = curr.next |
48
|
|
|
|
49
|
|
|
|
50
|
|
|
K = TypeVar("K") |
51
|
|
|
V = TypeVar("V") |
52
|
|
|
|
53
|
|
|
|
54
|
|
|
class _LimitedCache(Generic[K, V]): |
55
|
|
|
root: _DoublyLinkedList[tuple[K, V] | None] |
56
|
|
|
cache: dict[K, _DoublyLinkedList[tuple[K, V]]] |
57
|
|
|
max_size: int |
58
|
|
|
|
59
|
|
|
def __init__(self, max_size=512): |
60
|
|
|
self.root = _DoublyLinkedList(None) |
61
|
|
|
self.cache = {} |
62
|
|
|
self.max_size = max_size |
63
|
|
|
|
64
|
|
|
def __getitem__(self, key: K) -> V: |
65
|
|
|
entry = self.cache[key] |
66
|
|
|
_, v = entry.value |
67
|
|
|
return v |
68
|
|
|
|
69
|
|
|
def __contains__(self, key: K) -> bool: |
70
|
|
|
return key in self.cache |
71
|
|
|
|
72
|
|
|
def __setitem__(self, key: K, value: V): |
73
|
|
|
if key in self.cache: |
74
|
|
|
entry = self.cache[key] |
75
|
|
|
entry.remove() |
76
|
|
|
entry.value = (key, value) |
77
|
|
|
else: |
78
|
|
|
entry = _DoublyLinkedList( |
79
|
|
|
(key, value) |
80
|
|
|
) |
81
|
|
|
self.cache[key] = entry |
82
|
|
|
if len(self.cache) == self.max_size: |
83
|
|
|
self._remove_oldest() |
84
|
|
|
|
85
|
|
|
self.root.swap_next(entry) |
86
|
|
|
|
87
|
|
|
def __delitem__(self, key: K): |
88
|
|
|
entry = self.cache[key] |
89
|
|
|
del self.cache[key] |
90
|
|
|
entry.remove() |
91
|
|
|
|
92
|
|
|
def _remove_oldest(self): |
93
|
|
|
oldest_entry = self.root.previous |
94
|
|
|
k, _ = oldest_entry.value |
95
|
|
|
del self.cache[k] |
96
|
|
|
oldest_entry.remove() |
97
|
|
|
|
98
|
|
|
|
99
|
|
|
class RepeateMessageFilter: |
100
|
|
|
lockout_time: float |
101
|
|
|
_cache: _LimitedCache |
102
|
|
|
_lock: Lock |
103
|
|
|
|
104
|
|
|
def __init__(self, lockout_time: float, cache_size: int = 512): |
105
|
|
|
self.lockout_time = lockout_time |
106
|
|
|
self._cache = _LimitedCache(cache_size) |
107
|
|
|
self._lock = Lock() |
108
|
|
|
|
109
|
|
|
def filter(self, record: LogRecord) -> bool: |
110
|
|
|
key = self._record_key(record) |
111
|
|
|
current_time = time.time() |
112
|
|
|
with self._lock: |
113
|
|
|
if key not in self._cache: |
114
|
|
|
self._cache[key] = current_time |
115
|
|
|
return True |
116
|
|
|
elif current_time - self._cache[key] > self.lockout_time: |
117
|
|
|
self._cache[key] = current_time |
118
|
|
|
return True |
119
|
|
|
return False |
120
|
|
|
|
121
|
|
|
@staticmethod |
122
|
|
|
def _record_key(record: LogRecord): |
123
|
|
|
return (record.module, record.levelno, record.msg, record.args) |
124
|
|
|
|