Repository navigation
Expand file tree
/
Copy pathtest_redrive.py
More file actions
226 lines (162 loc) · 7.09 KB
/
Copy pathtest_redrive.py
File metadata and controls
226 lines (162 loc) · 7.09 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
"""Tests for the optional DLQ redrive tooling (ADR-0026)."""
import pytest
from babelqueue import dead_letter
from babelqueue.codec import EnvelopeCodec
from babelqueue.redrive import redrive
from babelqueue.transport import InMemoryTransport
def _dead_letter(transport, dlq, urn, original_queue, data=None):
env = EnvelopeCodec.make(urn, data or {}, queue=original_queue)
annotated = dead_letter.annotate(env, "failed", original_queue, 3, error="boom")
transport.publish(dlq, EnvelopeCodec.encode(annotated))
return annotated
def _drain(transport, queue):
out = []
while True:
message = transport.pop(queue, 0)
if message is None:
break
out.append(EnvelopeCodec.decode(message.body))
transport.ack(message)
return out
def test_redrive_to_source():
t = InMemoryTransport()
orig = _dead_letter(t, "orders.dlq", "urn:babel:orders:created", "orders", {"order_id": 1})
result = redrive(t, "orders.dlq")
assert result.redriven == 1 and result.skipped == 0
assert result.items[0].bypassed is False # bypass is off by default
got = _drain(t, "orders")
assert len(got) == 1
assert "dead_letter" not in got[0]
assert got[0]["attempts"] == 0
assert got[0]["trace_id"] == orig["trace_id"]
assert got[0]["data"] == {"order_id": 1}
assert EnvelopeCodec.urn(got[0]) == "urn:babel:orders:created"
assert _drain(t, "orders.dlq") == []
def test_redrive_to_sandbox():
t = InMemoryTransport()
_dead_letter(t, "orders.dlq", "urn:babel:orders:created", "orders")
result = redrive(t, "orders.dlq", to_queue="sandbox")
assert result.redriven == 1
assert _drain(t, "orders") == []
assert len(_drain(t, "sandbox")) == 1
def test_redrive_dry_run():
t = InMemoryTransport()
_dead_letter(t, "orders.dlq", "urn:babel:orders:created", "orders")
result = redrive(t, "orders.dlq", dry_run=True)
assert result.redriven == 0 and result.skipped == 1
assert result.items[0].to == "orders"
assert result.items[0].redriven is False
assert _drain(t, "orders") == []
dlq = _drain(t, "orders.dlq")
assert len(dlq) == 1 and "dead_letter" in dlq[0]
def test_redrive_select():
t = InMemoryTransport()
_dead_letter(t, "dlq", "urn:babel:orders:created", "orders")
_dead_letter(t, "dlq", "urn:babel:emails:welcome", "emails")
result = redrive(t, "dlq", select=lambda e: EnvelopeCodec.urn(e) == "urn:babel:orders:created")
assert result.redriven == 1 and result.skipped == 1
assert len(_drain(t, "orders")) == 1
assert _drain(t, "emails") == []
assert len(_drain(t, "dlq")) == 1 # the unselected one is restored
def test_redrive_max():
t = InMemoryTransport()
for _ in range(3):
_dead_letter(t, "dlq", "urn:babel:orders:created", "orders")
result = redrive(t, "dlq", max=2)
assert result.redriven == 2
assert len(_drain(t, "dlq")) == 1 # Max respected
def test_redrive_no_dead_letter_falls_back_to_meta_queue():
t = InMemoryTransport()
# a plain (never dead-lettered) envelope on the DLQ — redrive falls back to meta.queue
env = EnvelopeCodec.make("urn:babel:orders:created", {}, queue="orders")
t.publish("dlq", EnvelopeCodec.encode(env))
result = redrive(t, "dlq")
assert result.redriven == 1
assert len(_drain(t, "orders")) == 1
class _FailOnTarget(InMemoryTransport):
"""An in-memory transport that refuses to publish to one queue."""
def __init__(self, fail_queue):
super().__init__()
self._fail_queue = fail_queue
def publish(self, queue, body):
if queue == self._fail_queue:
raise RuntimeError("publish refused")
super().publish(queue, body)
def test_redrive_publish_failure_restores():
t = _FailOnTarget("orders")
_dead_letter(t, "dlq", "urn:babel:orders:created", "orders")
with pytest.raises(RuntimeError):
redrive(t, "dlq")
assert len(_drain(t, "dlq")) == 1 # restored to the DLQ, not lost
assert _drain(t, "orders") == [] # nothing reached the source queue
def test_redrive_undecodable_restored():
t = InMemoryTransport()
t.publish("dlq", "not-json{{{")
result = redrive(t, "dlq")
assert result.redriven == 0 and result.skipped == 1
message = t.pop("dlq", 0)
assert message is not None and message.body == "not-json{{{"
class _RoutedDLQ(InMemoryTransport):
"""Stamps a route URN on every popped message and records publish_routed calls."""
def __init__(self, route_urn=""):
super().__init__()
self.route_urn = route_urn
self.routed = []
def pop(self, queue, timeout=0.0):
message = super().pop(queue, timeout)
if message is not None:
message.route_urn = self.route_urn
return message
def publish_routed(self, queue, body, route_urn):
self.routed.append((queue, route_urn))
super().publish(queue, body)
def test_redrive_keeps_the_route_urn():
route = "urn:babel:orders:refunded"
t = _RoutedDLQ(route)
_dead_letter(t, "dlq", "urn:babel:orders:created", "orders")
redrive(t, "dlq")
assert t.routed == [("orders", route)]
t2 = _RoutedDLQ(route) # a dry-run restore to the DLQ keeps the route too
_dead_letter(t2, "dlq", "urn:babel:orders:created", "orders")
redrive(t2, "dlq", dry_run=True)
assert t2.routed == [("dlq", route)]
t3 = _RoutedDLQ() # no route -> plain publish
_dead_letter(t3, "dlq", "urn:babel:orders:created", "orders")
redrive(t3, "dlq")
assert t3.routed == []
assert len(_drain(t3, "orders")) == 1
class _KeyedDLQ(InMemoryTransport):
"""Stamps a key on every popped message and records the key of every settle write."""
def __init__(self, key):
super().__init__()
self.key = key
self.settled = []
def pop(self, queue, timeout=0.0):
message = super().pop(queue, timeout)
if message is not None:
message.key = self.key
return message
def publish_settle(self, queue, body, route_urn="", key=None):
self.settled.append((queue, key))
super().publish(queue, body)
@pytest.mark.parametrize("key", [b"order-1042", b"", None])
def test_redrive_carries_the_record_key(key):
t = _KeyedDLQ(key)
_dead_letter(t, "dlq", "urn:babel:orders:created", "orders")
result = redrive(t, "dlq")
assert result.redriven == 1
assert t.settled == [("orders", key)] # b"" stays b"", None stays None
t2 = _KeyedDLQ(key) # a dry-run restore to the DLQ keeps it too
_dead_letter(t2, "dlq", "urn:babel:orders:created", "orders")
redrive(t2, "dlq", dry_run=True)
assert t2.settled == [("dlq", key)]
def test_redrive_on_a_non_keyed_transport_is_unchanged():
t = InMemoryTransport()
_dead_letter(t, "dlq", "urn:babel:orders:created", "orders")
assert redrive(t, "dlq").redriven == 1
assert len(_drain(t, "orders")) == 1
def test_redrive_publisher_is_exported_from_the_package_root():
import babelqueue
from babelqueue.transport import RedrivePublisher
assert babelqueue.RedrivePublisher is RedrivePublisher
assert "RedrivePublisher" in babelqueue.__all__