DefStream 0.1.15__53-py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- DefStream/Broker/Configuration.py +18 -0
- DefStream/Broker/Connection.py +141 -0
- DefStream/Broker/Consumer.py +59 -0
- DefStream/Broker/Errors.py +14 -0
- DefStream/Broker/Event.py +35 -0
- DefStream/Broker/Implements/RabbitMQ/Configuration.py +48 -0
- DefStream/Broker/Implements/RabbitMQ/Connection.py +542 -0
- DefStream/Broker/Implements/RabbitMQ/Consumer.py +359 -0
- DefStream/Broker/Implements/RabbitMQ/Event.py +127 -0
- DefStream/Broker/Implements/RabbitMQ/Exchange.py +278 -0
- DefStream/Broker/Implements/RabbitMQ/Policies.py +9 -0
- DefStream/Broker/Implements/RabbitMQ/Queue.py +319 -0
- DefStream/Broker/Implements/RabbitMQ/Serializer.py +61 -0
- DefStream/Broker/Implements/RabbitMQ/Stream.py +179 -0
- DefStream/Broker/Implements/RabbitMQ/Subscription.py +74 -0
- DefStream/Broker/Implements/RabbitMQ/__init__.py +47 -0
- DefStream/Broker/Implements/Redis/Configuration.py +59 -0
- DefStream/Broker/Implements/Redis/Connection.py +265 -0
- DefStream/Broker/Implements/Redis/Consumer.py +364 -0
- DefStream/Broker/Implements/Redis/Event.py +191 -0
- DefStream/Broker/Implements/Redis/Queue.py +284 -0
- DefStream/Broker/Implements/Redis/Serializer.py +48 -0
- DefStream/Broker/Implements/Redis/Subscription.py +46 -0
- DefStream/Broker/Implements/Redis/__init__.py +29 -0
- DefStream/Broker/Implements/__init__.py +1 -0
- DefStream/Broker/Policies.py +25 -0
- DefStream/Broker/Priority.py +26 -0
- DefStream/Broker/Protocol.py +45 -0
- DefStream/Broker/__init__.py +59 -0
- DefStream/Descriptor/DataObject.py +409 -0
- DefStream/Descriptor/Types.py +248 -0
- DefStream/Descriptor/Value.py +188 -0
- DefStream/Descriptor/__init__.py +172 -0
- DefStream/Inspector/Extension.py +69 -0
- DefStream/Inspector/Extensions/Console.html +373 -0
- DefStream/Inspector/Extensions/Console.py +415 -0
- DefStream/Inspector/Extensions/Dashboard.html +206 -0
- DefStream/Inspector/Extensions/Dashboard.py +124 -0
- DefStream/Inspector/Extensions/HealthCheck.py +107 -0
- DefStream/Inspector/Extensions/Metrics.py +316 -0
- DefStream/Inspector/Extensions/Specifications.py +176 -0
- DefStream/Inspector/Extensions/__init__.py +13 -0
- DefStream/Inspector/Inspector.py +347 -0
- DefStream/Inspector/Response.py +14 -0
- DefStream/Inspector/__init__.py +9 -0
- DefStream/Validator/Pattern.py +36 -0
- DefStream/Validator/Patterns/Array.py +30 -0
- DefStream/Validator/Patterns/Common.py +92 -0
- DefStream/Validator/Patterns/Compare.py +180 -0
- DefStream/Validator/Patterns/Condition.py +60 -0
- DefStream/Validator/Patterns/DataObject.py +85 -0
- DefStream/Validator/Patterns/DateTime.py +111 -0
- DefStream/Validator/Patterns/Number.py +43 -0
- DefStream/Validator/Patterns/Object.py +150 -0
- DefStream/Validator/Patterns/Size.py +110 -0
- DefStream/Validator/Patterns/String.py +85 -0
- DefStream/Validator/Patterns/Type.py +350 -0
- DefStream/Validator/Patterns/__init__.py +137 -0
- DefStream/Validator/Validate.py +29 -0
- DefStream/Validator/Validator.py +30 -0
- DefStream/Validator/__init__.py +11 -0
- DefStream/Worker/Executor.py +371 -0
- DefStream/Worker/Message.py +180 -0
- DefStream/Worker/Pool.py +245 -0
- DefStream/Worker/Run.py +357 -0
- DefStream/Worker/Specification.py +87 -0
- DefStream/Worker/Startup.py +174 -0
- DefStream/Worker/Worker.py +421 -0
- DefStream/Worker/__init__.py +47 -0
- DefStream/__init__.py +0 -0
- defstream-0.1.15.dist-info/METADATA +1152 -0
- defstream-0.1.15.dist-info/RECORD +75 -0
- defstream-0.1.15.dist-info/WHEEL +6 -0
- defstream-0.1.15.dist-info/licenses/LICENSE +674 -0
- defstream-0.1.15.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
from collections import deque
|
|
2
|
+
from logging import getLogger
|
|
3
|
+
from queue import (
|
|
4
|
+
Empty,
|
|
5
|
+
SimpleQueue,
|
|
6
|
+
)
|
|
7
|
+
from threading import Event as Wakeup
|
|
8
|
+
from typing import (
|
|
9
|
+
Any,
|
|
10
|
+
Callable,
|
|
11
|
+
Dict,
|
|
12
|
+
Optional,
|
|
13
|
+
Tuple,
|
|
14
|
+
Union,
|
|
15
|
+
)
|
|
16
|
+
from uuid import uuid4
|
|
17
|
+
|
|
18
|
+
from redis import Redis
|
|
19
|
+
from redis.cluster import RedisCluster
|
|
20
|
+
|
|
21
|
+
from DefStream.Broker import (
|
|
22
|
+
Consumer as BaseConsumer,
|
|
23
|
+
Event as BaseEvent,
|
|
24
|
+
EventError,
|
|
25
|
+
EventHandler,
|
|
26
|
+
Requeue,
|
|
27
|
+
)
|
|
28
|
+
from .Event import Event
|
|
29
|
+
from .Queue import Queue
|
|
30
|
+
from .Serializer import (
|
|
31
|
+
Decoder,
|
|
32
|
+
Encoder,
|
|
33
|
+
)
|
|
34
|
+
from .Subscription import Subscription
|
|
35
|
+
|
|
36
|
+
__all__ = (
|
|
37
|
+
'Consumer',
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class Consumer(BaseConsumer):
|
|
42
|
+
logger = getLogger('DefStream.Broker.Implements.Redis.Consumer')
|
|
43
|
+
|
|
44
|
+
def __init__(
|
|
45
|
+
self,
|
|
46
|
+
connection: Union[Redis, RedisCluster],
|
|
47
|
+
encode: Encoder,
|
|
48
|
+
decode: Decoder,
|
|
49
|
+
handler: Optional[EventHandler] = None,
|
|
50
|
+
qos: int = 1,
|
|
51
|
+
reclaim_idle: Optional[int] = 60000,
|
|
52
|
+
):
|
|
53
|
+
if isinstance(
|
|
54
|
+
qos,
|
|
55
|
+
bool,
|
|
56
|
+
) or not isinstance(
|
|
57
|
+
qos,
|
|
58
|
+
int,
|
|
59
|
+
) or qos < 1:
|
|
60
|
+
raise ValueError('Redis qos must be a positive integer')
|
|
61
|
+
if reclaim_idle is not None and reclaim_idle < 1:
|
|
62
|
+
raise ValueError('Redis reclaim_idle must be positive milliseconds or None')
|
|
63
|
+
self.connection = connection
|
|
64
|
+
self.encode = encode
|
|
65
|
+
self.decode = decode
|
|
66
|
+
self.handler = handler
|
|
67
|
+
self.qos = qos
|
|
68
|
+
self.reclaim_idle = reclaim_idle
|
|
69
|
+
self.name = uuid4().hex
|
|
70
|
+
self.subscriptions: Dict[str, Tuple[Queue, EventHandler]] = {}
|
|
71
|
+
self.rotation = deque()
|
|
72
|
+
self.pending: Dict[Tuple[str, str], Event] = {}
|
|
73
|
+
self.cursors: Dict[str, str] = {}
|
|
74
|
+
self.callbacks: SimpleQueue[Callable[[], None]] = SimpleQueue()
|
|
75
|
+
self.wakeup = Wakeup()
|
|
76
|
+
self.stopped = False
|
|
77
|
+
self.closed = False
|
|
78
|
+
self.logger.debug(
|
|
79
|
+
'Consumer created: consumer=%s qos=%s reclaim_idle=%sms',
|
|
80
|
+
self.name,
|
|
81
|
+
self.qos,
|
|
82
|
+
self.reclaim_idle,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
def subs(
|
|
86
|
+
self,
|
|
87
|
+
queue: Any,
|
|
88
|
+
handler: Optional[EventHandler] = None,
|
|
89
|
+
) -> None:
|
|
90
|
+
if self.closed:
|
|
91
|
+
raise RuntimeError('Redis consumer is closed')
|
|
92
|
+
subscription = Subscription.parse(queue)
|
|
93
|
+
handler = handler if handler is not None else self.handler
|
|
94
|
+
if handler is None:
|
|
95
|
+
raise ValueError('subs requires a handler or a default handler')
|
|
96
|
+
if subscription.queue in self.subscriptions:
|
|
97
|
+
raise ValueError('A Run is already subscribed to queue {}'.format(subscription.queue))
|
|
98
|
+
self.logger.debug(
|
|
99
|
+
'Preparing Consumer subscription: queue=%s group=%s consumer=%s',
|
|
100
|
+
subscription.queue,
|
|
101
|
+
subscription.group,
|
|
102
|
+
self.name,
|
|
103
|
+
)
|
|
104
|
+
stream = Queue(
|
|
105
|
+
self.connection,
|
|
106
|
+
self.encode,
|
|
107
|
+
self.decode,
|
|
108
|
+
subscription.queue,
|
|
109
|
+
subscription.group,
|
|
110
|
+
)
|
|
111
|
+
stream.create_group()
|
|
112
|
+
self.subscriptions[subscription.queue] = (
|
|
113
|
+
stream,
|
|
114
|
+
handler,
|
|
115
|
+
)
|
|
116
|
+
self.cursors[subscription.queue] = '0-0'
|
|
117
|
+
self.rotation.append(subscription.queue)
|
|
118
|
+
self.logger.info(
|
|
119
|
+
'Consumer subscribed: queue=%s group=%s consumer=%s qos=%s reclaim_idle=%sms',
|
|
120
|
+
subscription.queue,
|
|
121
|
+
subscription.group,
|
|
122
|
+
self.name,
|
|
123
|
+
self.qos,
|
|
124
|
+
self.reclaim_idle,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
def call(
|
|
128
|
+
self,
|
|
129
|
+
callback: Callable[[], None],
|
|
130
|
+
) -> None:
|
|
131
|
+
if self.closed:
|
|
132
|
+
raise RuntimeError('Redis consumer is closed')
|
|
133
|
+
self.callbacks.put(callback)
|
|
134
|
+
self.wakeup.set()
|
|
135
|
+
self.logger.debug(
|
|
136
|
+
'Consumer callback scheduled: consumer=%s',
|
|
137
|
+
self.name,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
def fail(
|
|
141
|
+
self,
|
|
142
|
+
event: BaseEvent,
|
|
143
|
+
policy: Optional[EventError] = None,
|
|
144
|
+
) -> None:
|
|
145
|
+
self.logger.debug(
|
|
146
|
+
'Applying message failure policy: consumer=%s policy=%s',
|
|
147
|
+
self.name,
|
|
148
|
+
type(policy).__name__ if policy is not None else 'Requeue',
|
|
149
|
+
)
|
|
150
|
+
(policy if policy is not None else Requeue())(event)
|
|
151
|
+
|
|
152
|
+
def drain(self) -> None:
|
|
153
|
+
while True:
|
|
154
|
+
try:
|
|
155
|
+
callback = self.callbacks.get_nowait()
|
|
156
|
+
except Empty:
|
|
157
|
+
break
|
|
158
|
+
self.logger.debug(
|
|
159
|
+
'Executing Consumer callback: consumer=%s',
|
|
160
|
+
self.name,
|
|
161
|
+
)
|
|
162
|
+
try:
|
|
163
|
+
callback()
|
|
164
|
+
except Exception as error:
|
|
165
|
+
self.logger.error(
|
|
166
|
+
'Consumer callback failed: consumer=%s error_type=%s',
|
|
167
|
+
self.name,
|
|
168
|
+
type(error).__name__,
|
|
169
|
+
)
|
|
170
|
+
raise
|
|
171
|
+
pending_count = len(self.pending)
|
|
172
|
+
self.pending = {
|
|
173
|
+
key: event
|
|
174
|
+
for key, event in self.pending.items()
|
|
175
|
+
if not event.settled
|
|
176
|
+
}
|
|
177
|
+
settled_count = pending_count - len(self.pending)
|
|
178
|
+
if settled_count:
|
|
179
|
+
self.logger.debug(
|
|
180
|
+
'Settled Events removed: consumer=%s removed=%s pending=%s',
|
|
181
|
+
self.name,
|
|
182
|
+
settled_count,
|
|
183
|
+
len(self.pending),
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
def deliver(
|
|
187
|
+
self,
|
|
188
|
+
stream: Queue,
|
|
189
|
+
handler: EventHandler,
|
|
190
|
+
entries: list,
|
|
191
|
+
) -> None:
|
|
192
|
+
for id, fields in entries:
|
|
193
|
+
key = (
|
|
194
|
+
stream.queue,
|
|
195
|
+
id.decode('ascii'),
|
|
196
|
+
)
|
|
197
|
+
if key in self.pending:
|
|
198
|
+
self.logger.debug(
|
|
199
|
+
'Skipping delivery of already pending message: queue=%s group=%s id=%s',
|
|
200
|
+
stream.queue,
|
|
201
|
+
stream.group,
|
|
202
|
+
key[1],
|
|
203
|
+
)
|
|
204
|
+
continue
|
|
205
|
+
event = stream.event(
|
|
206
|
+
id,
|
|
207
|
+
fields,
|
|
208
|
+
stream.group,
|
|
209
|
+
self.name,
|
|
210
|
+
)
|
|
211
|
+
self.pending[key] = event
|
|
212
|
+
self.logger.debug(
|
|
213
|
+
'Delivering message: queue=%s group=%s consumer=%s id=%s pending=%s',
|
|
214
|
+
stream.queue,
|
|
215
|
+
stream.group,
|
|
216
|
+
self.name,
|
|
217
|
+
event.id,
|
|
218
|
+
len(self.pending),
|
|
219
|
+
)
|
|
220
|
+
try:
|
|
221
|
+
handler(event)
|
|
222
|
+
except Exception as error:
|
|
223
|
+
self.logger.error(
|
|
224
|
+
'Message handler dispatch failed: queue=%s group=%s consumer=%s id=%s error_type=%s',
|
|
225
|
+
stream.queue,
|
|
226
|
+
stream.group,
|
|
227
|
+
self.name,
|
|
228
|
+
event.id,
|
|
229
|
+
type(error).__name__,
|
|
230
|
+
)
|
|
231
|
+
raise
|
|
232
|
+
self.drain()
|
|
233
|
+
|
|
234
|
+
def run(self) -> None:
|
|
235
|
+
if self.closed:
|
|
236
|
+
raise RuntimeError('Redis consumer is closed')
|
|
237
|
+
if not self.subscriptions:
|
|
238
|
+
raise RuntimeError('Subscribe before running the Redis consumer')
|
|
239
|
+
self.logger.info(
|
|
240
|
+
'Starting Consumer consumption loop: consumer=%s subscriptions=%s qos=%s reclaim_idle=%sms',
|
|
241
|
+
self.name,
|
|
242
|
+
len(self.subscriptions),
|
|
243
|
+
self.qos,
|
|
244
|
+
self.reclaim_idle,
|
|
245
|
+
)
|
|
246
|
+
qos_limited = False
|
|
247
|
+
while not self.stopped:
|
|
248
|
+
self.wakeup.clear()
|
|
249
|
+
self.drain()
|
|
250
|
+
for _ in range(len(self.rotation)):
|
|
251
|
+
available = self.qos - len(self.pending)
|
|
252
|
+
if available <= 0:
|
|
253
|
+
if not qos_limited:
|
|
254
|
+
self.logger.debug(
|
|
255
|
+
'Deferring reads at QoS limit: consumer=%s qos=%s pending=%s',
|
|
256
|
+
self.name,
|
|
257
|
+
self.qos,
|
|
258
|
+
len(self.pending),
|
|
259
|
+
)
|
|
260
|
+
qos_limited = True
|
|
261
|
+
break
|
|
262
|
+
qos_limited = False
|
|
263
|
+
if self.stopped:
|
|
264
|
+
break
|
|
265
|
+
name = self.rotation.popleft()
|
|
266
|
+
self.rotation.append(name)
|
|
267
|
+
stream, handler = self.subscriptions[name]
|
|
268
|
+
if self.reclaim_idle is not None:
|
|
269
|
+
old_cursor = self.cursors[stream.queue]
|
|
270
|
+
try:
|
|
271
|
+
claimed = self.connection.xautoclaim(
|
|
272
|
+
stream.queue,
|
|
273
|
+
stream.group,
|
|
274
|
+
self.name,
|
|
275
|
+
self.reclaim_idle,
|
|
276
|
+
start_id=old_cursor,
|
|
277
|
+
count=available,
|
|
278
|
+
)
|
|
279
|
+
except Exception as error:
|
|
280
|
+
self.logger.error(
|
|
281
|
+
'Pending message reclaim failed: queue=%s group=%s consumer=%s error_type=%s',
|
|
282
|
+
stream.queue,
|
|
283
|
+
stream.group,
|
|
284
|
+
self.name,
|
|
285
|
+
type(error).__name__,
|
|
286
|
+
)
|
|
287
|
+
raise
|
|
288
|
+
self.cursors[stream.queue] = claimed[0].decode('ascii')
|
|
289
|
+
if claimed[1] or self.cursors[stream.queue] != old_cursor:
|
|
290
|
+
self.logger.debug(
|
|
291
|
+
'Pending message reclaim result: queue=%s group=%s consumer=%s count=%s cursor=%s',
|
|
292
|
+
stream.queue,
|
|
293
|
+
stream.group,
|
|
294
|
+
self.name,
|
|
295
|
+
len(claimed[1]),
|
|
296
|
+
self.cursors[stream.queue],
|
|
297
|
+
)
|
|
298
|
+
self.deliver(
|
|
299
|
+
stream,
|
|
300
|
+
handler,
|
|
301
|
+
claimed[1],
|
|
302
|
+
)
|
|
303
|
+
available = self.qos - len(self.pending)
|
|
304
|
+
if available <= 0:
|
|
305
|
+
if not qos_limited:
|
|
306
|
+
self.logger.debug(
|
|
307
|
+
'Deferring reads at QoS limit: consumer=%s qos=%s pending=%s',
|
|
308
|
+
self.name,
|
|
309
|
+
self.qos,
|
|
310
|
+
len(self.pending),
|
|
311
|
+
)
|
|
312
|
+
qos_limited = True
|
|
313
|
+
continue
|
|
314
|
+
qos_limited = False
|
|
315
|
+
if self.stopped:
|
|
316
|
+
continue
|
|
317
|
+
try:
|
|
318
|
+
messages = self.connection.xreadgroup(
|
|
319
|
+
stream.group,
|
|
320
|
+
self.name,
|
|
321
|
+
{stream.queue: '>'},
|
|
322
|
+
count=available,
|
|
323
|
+
)
|
|
324
|
+
except Exception as error:
|
|
325
|
+
self.logger.error(
|
|
326
|
+
'New message read failed: queue=%s group=%s consumer=%s error_type=%s',
|
|
327
|
+
stream.queue,
|
|
328
|
+
stream.group,
|
|
329
|
+
self.name,
|
|
330
|
+
type(error).__name__,
|
|
331
|
+
)
|
|
332
|
+
raise
|
|
333
|
+
for _, entries in messages:
|
|
334
|
+
self.deliver(
|
|
335
|
+
stream,
|
|
336
|
+
handler,
|
|
337
|
+
entries,
|
|
338
|
+
)
|
|
339
|
+
self.wakeup.wait(0.05)
|
|
340
|
+
self.logger.info(
|
|
341
|
+
'Consumer consumption loop ended: consumer=%s pending=%s',
|
|
342
|
+
self.name,
|
|
343
|
+
len(self.pending),
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
def stop(self) -> None:
|
|
347
|
+
if not self.stopped:
|
|
348
|
+
self.logger.info(
|
|
349
|
+
'Consumer stop requested: consumer=%s pending=%s',
|
|
350
|
+
self.name,
|
|
351
|
+
len(self.pending),
|
|
352
|
+
)
|
|
353
|
+
self.stopped = True
|
|
354
|
+
self.wakeup.set()
|
|
355
|
+
|
|
356
|
+
def close(self) -> None:
|
|
357
|
+
if not self.closed:
|
|
358
|
+
self.logger.info(
|
|
359
|
+
'Consumer closed: consumer=%s pending=%s',
|
|
360
|
+
self.name,
|
|
361
|
+
len(self.pending),
|
|
362
|
+
)
|
|
363
|
+
self.stop()
|
|
364
|
+
self.closed = True
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
from logging import getLogger
|
|
2
|
+
from typing import (
|
|
3
|
+
Any,
|
|
4
|
+
Optional,
|
|
5
|
+
Union,
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
from redis import Redis
|
|
9
|
+
from redis.cluster import RedisCluster
|
|
10
|
+
|
|
11
|
+
from DefStream.Broker import Event as BaseEvent
|
|
12
|
+
|
|
13
|
+
__all__ = (
|
|
14
|
+
'Event',
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Event(BaseEvent):
|
|
19
|
+
logger = getLogger('DefStream.Broker.Implements.Redis.Event')
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
connection: Union[Redis, RedisCluster],
|
|
24
|
+
queue: str,
|
|
25
|
+
id: str,
|
|
26
|
+
body: Any,
|
|
27
|
+
group: Optional[str] = None,
|
|
28
|
+
consumer: Optional[str] = None,
|
|
29
|
+
):
|
|
30
|
+
self.connection = connection
|
|
31
|
+
self.queue = queue
|
|
32
|
+
self.id = id
|
|
33
|
+
self.payload = body
|
|
34
|
+
self.group = group
|
|
35
|
+
self.consumer = consumer
|
|
36
|
+
self.settled = False
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def body(self) -> Any:
|
|
40
|
+
return self.payload
|
|
41
|
+
|
|
42
|
+
@body.setter
|
|
43
|
+
def body(
|
|
44
|
+
self,
|
|
45
|
+
body: Any,
|
|
46
|
+
) -> None:
|
|
47
|
+
self.payload = body
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def src(self) -> str:
|
|
51
|
+
return self.queue
|
|
52
|
+
|
|
53
|
+
def ack(self) -> None:
|
|
54
|
+
if self.group is None:
|
|
55
|
+
self.logger.warning(
|
|
56
|
+
'Rejected ACK for history Event without a consumer group: queue=%s id=%s',
|
|
57
|
+
self.queue,
|
|
58
|
+
self.id,
|
|
59
|
+
)
|
|
60
|
+
raise RuntimeError('Events from read() have no consumer group to acknowledge')
|
|
61
|
+
if not self.settled:
|
|
62
|
+
self.logger.debug(
|
|
63
|
+
'Attempting Event ACK: queue=%s group=%s consumer=%s id=%s',
|
|
64
|
+
self.queue,
|
|
65
|
+
self.group,
|
|
66
|
+
self.consumer,
|
|
67
|
+
self.id,
|
|
68
|
+
)
|
|
69
|
+
try:
|
|
70
|
+
result = self.connection.xack(
|
|
71
|
+
self.queue,
|
|
72
|
+
self.group,
|
|
73
|
+
self.id,
|
|
74
|
+
)
|
|
75
|
+
except Exception as error:
|
|
76
|
+
self.logger.error(
|
|
77
|
+
'Event ACK failed: queue=%s group=%s consumer=%s id=%s error_type=%s',
|
|
78
|
+
self.queue,
|
|
79
|
+
self.group,
|
|
80
|
+
self.consumer,
|
|
81
|
+
self.id,
|
|
82
|
+
type(error).__name__,
|
|
83
|
+
)
|
|
84
|
+
raise
|
|
85
|
+
self.settled = True
|
|
86
|
+
if isinstance(result, int) and result > 0:
|
|
87
|
+
self.logger.debug(
|
|
88
|
+
'Event ACK completed: queue=%s group=%s consumer=%s id=%s count=%s',
|
|
89
|
+
self.queue,
|
|
90
|
+
self.group,
|
|
91
|
+
self.consumer,
|
|
92
|
+
self.id,
|
|
93
|
+
result,
|
|
94
|
+
)
|
|
95
|
+
else:
|
|
96
|
+
self.logger.debug(
|
|
97
|
+
'No Event ACK target: queue=%s group=%s consumer=%s id=%s count=%s',
|
|
98
|
+
self.queue,
|
|
99
|
+
self.group,
|
|
100
|
+
self.consumer,
|
|
101
|
+
self.id,
|
|
102
|
+
result,
|
|
103
|
+
)
|
|
104
|
+
else:
|
|
105
|
+
self.logger.debug(
|
|
106
|
+
'Skipping ACK for settled Event: queue=%s group=%s consumer=%s id=%s',
|
|
107
|
+
self.queue,
|
|
108
|
+
self.group,
|
|
109
|
+
self.consumer,
|
|
110
|
+
self.id,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
def nack(self) -> None:
|
|
114
|
+
if self.group is None:
|
|
115
|
+
self.logger.warning(
|
|
116
|
+
'Rejected NACK for history Event without a consumer group: queue=%s id=%s',
|
|
117
|
+
self.queue,
|
|
118
|
+
self.id,
|
|
119
|
+
)
|
|
120
|
+
raise RuntimeError('Events from read() have no consumer group to requeue')
|
|
121
|
+
if not self.settled:
|
|
122
|
+
self.logger.debug(
|
|
123
|
+
'Attempting Event requeue: queue=%s group=%s consumer=%s id=%s',
|
|
124
|
+
self.queue,
|
|
125
|
+
self.group,
|
|
126
|
+
self.consumer,
|
|
127
|
+
self.id,
|
|
128
|
+
)
|
|
129
|
+
# Requeue and acknowledge atomically on one stream (also valid in Cluster).
|
|
130
|
+
try:
|
|
131
|
+
result = self.connection.eval(
|
|
132
|
+
"""
|
|
133
|
+
local pending = redis.call('XPENDING', KEYS[1], ARGV[1], ARGV[2], ARGV[2], 1)
|
|
134
|
+
if #pending == 0 then return 0 end
|
|
135
|
+
local entries = redis.call('XRANGE', KEYS[1], ARGV[2], ARGV[2])
|
|
136
|
+
if #entries == 0 then return redis.error_reply('Pending entry was deleted') end
|
|
137
|
+
redis.call('XADD', KEYS[1], '*', unpack(entries[1][2]))
|
|
138
|
+
return redis.call('XACK', KEYS[1], ARGV[1], ARGV[2])
|
|
139
|
+
""",
|
|
140
|
+
1,
|
|
141
|
+
self.queue,
|
|
142
|
+
self.group,
|
|
143
|
+
self.id,
|
|
144
|
+
)
|
|
145
|
+
except Exception as error:
|
|
146
|
+
self.logger.error(
|
|
147
|
+
'Event atomic requeue failed: queue=%s group=%s consumer=%s id=%s error_type=%s',
|
|
148
|
+
self.queue,
|
|
149
|
+
self.group,
|
|
150
|
+
self.consumer,
|
|
151
|
+
self.id,
|
|
152
|
+
type(error).__name__,
|
|
153
|
+
)
|
|
154
|
+
raise
|
|
155
|
+
self.settled = True
|
|
156
|
+
if isinstance(result, int) and result > 0:
|
|
157
|
+
self.logger.debug(
|
|
158
|
+
'Event requeue and ACK completed: queue=%s group=%s consumer=%s id=%s count=%s',
|
|
159
|
+
self.queue,
|
|
160
|
+
self.group,
|
|
161
|
+
self.consumer,
|
|
162
|
+
self.id,
|
|
163
|
+
result,
|
|
164
|
+
)
|
|
165
|
+
else:
|
|
166
|
+
self.logger.debug(
|
|
167
|
+
'No Event requeue target: queue=%s group=%s consumer=%s id=%s count=%s',
|
|
168
|
+
self.queue,
|
|
169
|
+
self.group,
|
|
170
|
+
self.consumer,
|
|
171
|
+
self.id,
|
|
172
|
+
result,
|
|
173
|
+
)
|
|
174
|
+
else:
|
|
175
|
+
self.logger.debug(
|
|
176
|
+
'Skipping NACK for settled Event: queue=%s group=%s consumer=%s id=%s',
|
|
177
|
+
self.queue,
|
|
178
|
+
self.group,
|
|
179
|
+
self.consumer,
|
|
180
|
+
self.id,
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
def reject(self) -> None:
|
|
184
|
+
self.logger.debug(
|
|
185
|
+
'Event reject requested (currently implemented as ACK): queue=%s group=%s consumer=%s id=%s',
|
|
186
|
+
self.queue,
|
|
187
|
+
self.group,
|
|
188
|
+
self.consumer,
|
|
189
|
+
self.id,
|
|
190
|
+
)
|
|
191
|
+
self.ack()
|