pycyphal2 2.0.0.dev0__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.
- pycyphal2/__init__.py +89 -0
- pycyphal2/_api.py +604 -0
- pycyphal2/_hash.py +204 -0
- pycyphal2/_header.py +349 -0
- pycyphal2/_node.py +1472 -0
- pycyphal2/_publisher.py +427 -0
- pycyphal2/_subscriber.py +430 -0
- pycyphal2/_transport.py +92 -0
- pycyphal2/can/__init__.py +43 -0
- pycyphal2/can/_interface.py +131 -0
- pycyphal2/can/_reassembly.py +158 -0
- pycyphal2/can/_transport.py +525 -0
- pycyphal2/can/_wire.py +376 -0
- pycyphal2/can/pythoncan.py +261 -0
- pycyphal2/can/socketcan.py +225 -0
- pycyphal2/py.typed +0 -0
- pycyphal2/udp.py +1000 -0
- pycyphal2-2.0.0.dev0.dist-info/METADATA +58 -0
- pycyphal2-2.0.0.dev0.dist-info/RECORD +22 -0
- pycyphal2-2.0.0.dev0.dist-info/WHEEL +5 -0
- pycyphal2-2.0.0.dev0.dist-info/licenses/LICENSE +20 -0
- pycyphal2-2.0.0.dev0.dist-info/top_level.txt +1 -0
pycyphal2/_publisher.py
ADDED
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import logging
|
|
5
|
+
import math
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
|
|
8
|
+
from ._api import DeliveryError, Instant, LivenessError, Priority, SendError
|
|
9
|
+
from ._api import Publisher, Topic, ResponseStream, Response
|
|
10
|
+
from ._header import MsgBeHeader, MsgRelHeader, RspBeHeader, RspRelHeader
|
|
11
|
+
from ._node import ACK_BASELINE_DEFAULT_TIMEOUT, NodeImpl, PublishTracker, SESSION_LIFETIME, TopicImpl
|
|
12
|
+
from ._transport import TransportArrival
|
|
13
|
+
|
|
14
|
+
_logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
REQUEST_FUTURE_HISTORY = 192
|
|
17
|
+
REQUEST_FUTURE_HISTORY_MASK = (1 << REQUEST_FUTURE_HISTORY) - 1
|
|
18
|
+
ACK_TIMEOUT_MIN = 1e-6
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class ResponseRemoteState:
|
|
23
|
+
seqno_top: int
|
|
24
|
+
seqno_acked: int = 1
|
|
25
|
+
|
|
26
|
+
def accept(self, seqno: int) -> tuple[bool, bool]:
|
|
27
|
+
if seqno > self.seqno_top:
|
|
28
|
+
shift = seqno - self.seqno_top
|
|
29
|
+
self.seqno_acked = (
|
|
30
|
+
1
|
|
31
|
+
if shift >= REQUEST_FUTURE_HISTORY
|
|
32
|
+
else (((self.seqno_acked << shift) & REQUEST_FUTURE_HISTORY_MASK) | 1)
|
|
33
|
+
)
|
|
34
|
+
self.seqno_top = seqno
|
|
35
|
+
return True, True
|
|
36
|
+
dist = self.seqno_top - seqno
|
|
37
|
+
if dist >= REQUEST_FUTURE_HISTORY:
|
|
38
|
+
return False, False
|
|
39
|
+
mask = 1 << dist
|
|
40
|
+
if self.seqno_acked & mask:
|
|
41
|
+
return True, False
|
|
42
|
+
self.seqno_acked |= mask
|
|
43
|
+
return True, True
|
|
44
|
+
|
|
45
|
+
def accepted_earlier(self, seqno: int) -> bool:
|
|
46
|
+
if seqno > self.seqno_top:
|
|
47
|
+
return False
|
|
48
|
+
dist = self.seqno_top - seqno
|
|
49
|
+
return dist < REQUEST_FUTURE_HISTORY and bool(self.seqno_acked & (1 << dist))
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class PublisherImpl(Publisher):
|
|
53
|
+
def __init__(self, node: NodeImpl, topic: TopicImpl) -> None:
|
|
54
|
+
self._node = node
|
|
55
|
+
self._topic = topic
|
|
56
|
+
self._priority = Priority.NOMINAL
|
|
57
|
+
self._ack_timeout_baseline = ACK_BASELINE_DEFAULT_TIMEOUT
|
|
58
|
+
self.closed = False
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def topic(self) -> Topic:
|
|
62
|
+
return self._topic
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def priority(self) -> Priority:
|
|
66
|
+
return self._priority
|
|
67
|
+
|
|
68
|
+
@priority.setter
|
|
69
|
+
def priority(self, priority: Priority) -> None:
|
|
70
|
+
self._priority = priority
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def ack_timeout(self) -> float:
|
|
74
|
+
return self._ack_timeout_baseline * (1 << int(self._priority))
|
|
75
|
+
|
|
76
|
+
@ack_timeout.setter
|
|
77
|
+
def ack_timeout(self, duration: float) -> None:
|
|
78
|
+
duration = float(duration)
|
|
79
|
+
if duration < ACK_TIMEOUT_MIN or not math.isfinite(duration):
|
|
80
|
+
raise ValueError("ACK timeout must be a positive finite duration")
|
|
81
|
+
if duration > SESSION_LIFETIME:
|
|
82
|
+
raise ValueError(f"ACK timeout must be less than session lifetime")
|
|
83
|
+
self._ack_timeout_baseline = duration / (1 << int(self._priority))
|
|
84
|
+
|
|
85
|
+
async def __call__(
|
|
86
|
+
self,
|
|
87
|
+
deadline: Instant,
|
|
88
|
+
message: memoryview | bytes,
|
|
89
|
+
*,
|
|
90
|
+
reliable: bool = False,
|
|
91
|
+
) -> None:
|
|
92
|
+
if self.closed:
|
|
93
|
+
raise SendError("Publisher closed")
|
|
94
|
+
|
|
95
|
+
tag = self._topic.next_tag()
|
|
96
|
+
payload = bytes(message)
|
|
97
|
+
|
|
98
|
+
if not reliable:
|
|
99
|
+
writer = self._topic.ensure_writer()
|
|
100
|
+
await writer(deadline, self._priority, self._serialize_message(tag, payload, reliable=False))
|
|
101
|
+
_logger.debug("Published BE tag=%d topic='%s'", tag, self._topic.name)
|
|
102
|
+
return
|
|
103
|
+
|
|
104
|
+
await self._reliable_publish(deadline, tag, payload)
|
|
105
|
+
|
|
106
|
+
async def request(
|
|
107
|
+
self,
|
|
108
|
+
delivery_deadline: Instant,
|
|
109
|
+
response_timeout: float,
|
|
110
|
+
message: memoryview | bytes,
|
|
111
|
+
) -> ResponseStream:
|
|
112
|
+
if self.closed:
|
|
113
|
+
raise SendError("Publisher closed")
|
|
114
|
+
|
|
115
|
+
tag = self._topic.next_tag()
|
|
116
|
+
payload = bytes(message)
|
|
117
|
+
|
|
118
|
+
# Create response stream before publishing so it's ready to receive.
|
|
119
|
+
stream = ResponseStreamImpl(
|
|
120
|
+
node=self._node,
|
|
121
|
+
topic=self._topic,
|
|
122
|
+
message_tag=tag,
|
|
123
|
+
response_timeout=response_timeout,
|
|
124
|
+
)
|
|
125
|
+
self._topic.request_futures[tag] = stream
|
|
126
|
+
|
|
127
|
+
tracker = self._prepare_reliable_publish_tracker(tag, delivery_deadline.ns, payload)
|
|
128
|
+
try:
|
|
129
|
+
initial_window = await self._reliable_publish_start(delivery_deadline, tag, payload, tracker)
|
|
130
|
+
except asyncio.CancelledError:
|
|
131
|
+
tracker.compromised = True
|
|
132
|
+
self._topic.request_futures.pop(tag, None)
|
|
133
|
+
self._release_reliable_publish_tracker(tag, tracker)
|
|
134
|
+
raise
|
|
135
|
+
except BaseException:
|
|
136
|
+
self._topic.request_futures.pop(tag, None)
|
|
137
|
+
self._release_reliable_publish_tracker(tag, tracker)
|
|
138
|
+
raise
|
|
139
|
+
|
|
140
|
+
task = self._node.loop.create_task(
|
|
141
|
+
self._request_publish(delivery_deadline, tag, payload, stream, tracker, initial_window)
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
def on_done(done_task: asyncio.Task[None]) -> None:
|
|
145
|
+
if done_task.cancelled() and self._topic.publish_futures.get(tag) is tracker:
|
|
146
|
+
tracker.compromised = True
|
|
147
|
+
self._release_reliable_publish_tracker(tag, tracker)
|
|
148
|
+
|
|
149
|
+
task.add_done_callback(on_done)
|
|
150
|
+
stream.set_publish_task(task)
|
|
151
|
+
return stream
|
|
152
|
+
|
|
153
|
+
async def _request_publish(
|
|
154
|
+
self,
|
|
155
|
+
deadline: Instant,
|
|
156
|
+
tag: int,
|
|
157
|
+
payload: bytes,
|
|
158
|
+
stream: ResponseStreamImpl,
|
|
159
|
+
tracker: PublishTracker,
|
|
160
|
+
initial_window: tuple[int, bool],
|
|
161
|
+
) -> None:
|
|
162
|
+
try:
|
|
163
|
+
await self._reliable_publish_continue(deadline, tag, payload, tracker, initial_window)
|
|
164
|
+
except asyncio.CancelledError:
|
|
165
|
+
tracker.compromised = True
|
|
166
|
+
raise
|
|
167
|
+
except BaseException as ex:
|
|
168
|
+
stream.on_publish_error(ex)
|
|
169
|
+
finally:
|
|
170
|
+
self._release_reliable_publish_tracker(tag, tracker)
|
|
171
|
+
|
|
172
|
+
@staticmethod
|
|
173
|
+
def _ack_is_last_attempt(current_ack_deadline_ns: int, current_ack_timeout: float, total_deadline_ns: int) -> bool:
|
|
174
|
+
next_ack_timeout_ns = round(current_ack_timeout * 2 * 1e9)
|
|
175
|
+
remaining_budget_ns = total_deadline_ns - current_ack_deadline_ns
|
|
176
|
+
return remaining_budget_ns < next_ack_timeout_ns
|
|
177
|
+
|
|
178
|
+
@staticmethod
|
|
179
|
+
def _ack_window_is_compromised(deadline_ns: int, current_ack_timeout: float) -> bool:
|
|
180
|
+
return Instant.now().ns >= (deadline_ns - round(current_ack_timeout * 1e9))
|
|
181
|
+
|
|
182
|
+
def _serialize_message(self, tag: int, payload: bytes, *, reliable: bool) -> bytes:
|
|
183
|
+
lage = self._topic.lage(Instant.now().s)
|
|
184
|
+
hdr = (MsgRelHeader if reliable else MsgBeHeader)(
|
|
185
|
+
topic_log_age=lage,
|
|
186
|
+
topic_evictions=self._topic.evictions,
|
|
187
|
+
topic_hash=self._topic.hash,
|
|
188
|
+
tag=tag,
|
|
189
|
+
)
|
|
190
|
+
return hdr.serialize() + payload
|
|
191
|
+
|
|
192
|
+
@staticmethod
|
|
193
|
+
def _reliable_publish_window(deadline_ns: int, ack_timeout: float) -> tuple[int, bool] | None:
|
|
194
|
+
now_ns = Instant.now().ns
|
|
195
|
+
if now_ns >= deadline_ns:
|
|
196
|
+
return None
|
|
197
|
+
ack_deadline_ns = min(deadline_ns, now_ns + round(ack_timeout * 1e9))
|
|
198
|
+
return ack_deadline_ns, PublisherImpl._ack_is_last_attempt(ack_deadline_ns, ack_timeout, deadline_ns)
|
|
199
|
+
|
|
200
|
+
def _prepare_reliable_publish_tracker(self, tag: int, deadline_ns: int, payload: bytes) -> PublishTracker:
|
|
201
|
+
tracker = self._node.prepare_publish_tracker(self._topic, tag, deadline_ns, payload)
|
|
202
|
+
tracker.ack_timeout = self.ack_timeout
|
|
203
|
+
self._topic.publish_futures[tag] = tracker
|
|
204
|
+
return tracker
|
|
205
|
+
|
|
206
|
+
def _release_reliable_publish_tracker(self, tag: int, tracker: PublishTracker) -> None:
|
|
207
|
+
self._topic.publish_futures.pop(tag, None)
|
|
208
|
+
self._node.publish_tracker_release(self._topic, tracker)
|
|
209
|
+
|
|
210
|
+
async def _send_reliable_publish(
|
|
211
|
+
self,
|
|
212
|
+
deadline: Instant,
|
|
213
|
+
tag: int,
|
|
214
|
+
payload: bytes,
|
|
215
|
+
tracker: PublishTracker,
|
|
216
|
+
*,
|
|
217
|
+
first_attempt: bool,
|
|
218
|
+
) -> None:
|
|
219
|
+
data = self._serialize_message(tag, payload, reliable=True)
|
|
220
|
+
if (not first_attempt) and (len(tracker.remaining) == 1):
|
|
221
|
+
remote_id = next(iter(tracker.remaining))
|
|
222
|
+
await self._node.transport.unicast(deadline, self._priority, remote_id, data)
|
|
223
|
+
else:
|
|
224
|
+
writer = self._topic.ensure_writer()
|
|
225
|
+
await writer(deadline, self._priority, data)
|
|
226
|
+
|
|
227
|
+
async def _reliable_publish_start(
|
|
228
|
+
self,
|
|
229
|
+
deadline: Instant,
|
|
230
|
+
tag: int,
|
|
231
|
+
payload: bytes,
|
|
232
|
+
tracker: PublishTracker,
|
|
233
|
+
) -> tuple[int, bool]:
|
|
234
|
+
initial_window = self._reliable_publish_window(deadline.ns, tracker.ack_timeout)
|
|
235
|
+
if initial_window is None:
|
|
236
|
+
raise DeliveryError("Reliable publish not acknowledged before deadline")
|
|
237
|
+
ack_deadline_ns, _ = initial_window
|
|
238
|
+
tracker.ack_event.clear()
|
|
239
|
+
try:
|
|
240
|
+
await self._send_reliable_publish(Instant(ns=ack_deadline_ns), tag, payload, tracker, first_attempt=True)
|
|
241
|
+
except SendError:
|
|
242
|
+
tracker.compromised = True
|
|
243
|
+
raise
|
|
244
|
+
except OSError as ex:
|
|
245
|
+
tracker.compromised = True
|
|
246
|
+
raise SendError("Reliable publish initial send failed") from ex
|
|
247
|
+
return initial_window
|
|
248
|
+
|
|
249
|
+
async def _reliable_publish_continue(
|
|
250
|
+
self,
|
|
251
|
+
deadline: Instant,
|
|
252
|
+
tag: int,
|
|
253
|
+
payload: bytes,
|
|
254
|
+
tracker: PublishTracker,
|
|
255
|
+
initial_window: tuple[int, bool],
|
|
256
|
+
) -> None:
|
|
257
|
+
ack_deadline_ns, last_attempt = initial_window
|
|
258
|
+
while True:
|
|
259
|
+
if tracker.acknowledged and not tracker.remaining:
|
|
260
|
+
_logger.debug("Reliable publish ACKed tag=%d topic='%s'", tag, self._topic.name)
|
|
261
|
+
return
|
|
262
|
+
|
|
263
|
+
wait_until_ns = deadline.ns if last_attempt else ack_deadline_ns
|
|
264
|
+
wait_timeout = max(0.0, (wait_until_ns - Instant.now().ns) * 1e-9)
|
|
265
|
+
if wait_timeout > 0:
|
|
266
|
+
try:
|
|
267
|
+
await asyncio.wait_for(tracker.ack_event.wait(), timeout=wait_timeout)
|
|
268
|
+
except asyncio.TimeoutError:
|
|
269
|
+
pass
|
|
270
|
+
|
|
271
|
+
if (not last_attempt) and self._ack_window_is_compromised(deadline.ns, tracker.ack_timeout):
|
|
272
|
+
tracker.compromised = True
|
|
273
|
+
|
|
274
|
+
if tracker.acknowledged and not tracker.remaining:
|
|
275
|
+
_logger.debug("Reliable publish ACKed tag=%d topic='%s'", tag, self._topic.name)
|
|
276
|
+
return
|
|
277
|
+
if last_attempt:
|
|
278
|
+
break
|
|
279
|
+
tracker.ack_timeout *= 2
|
|
280
|
+
next_window = self._reliable_publish_window(deadline.ns, tracker.ack_timeout)
|
|
281
|
+
if next_window is None:
|
|
282
|
+
break
|
|
283
|
+
ack_deadline_ns, last_attempt = next_window
|
|
284
|
+
tracker.ack_event.clear()
|
|
285
|
+
try:
|
|
286
|
+
await self._send_reliable_publish(
|
|
287
|
+
Instant(ns=ack_deadline_ns), tag, payload, tracker, first_attempt=False
|
|
288
|
+
)
|
|
289
|
+
except (SendError, OSError):
|
|
290
|
+
tracker.compromised = True
|
|
291
|
+
|
|
292
|
+
raise DeliveryError("Reliable publish not acknowledged before deadline")
|
|
293
|
+
|
|
294
|
+
async def _reliable_publish(self, deadline: Instant, tag: int, payload: bytes) -> None:
|
|
295
|
+
tracker = self._prepare_reliable_publish_tracker(tag, deadline.ns, payload)
|
|
296
|
+
try:
|
|
297
|
+
initial_window = await self._reliable_publish_start(deadline, tag, payload, tracker)
|
|
298
|
+
await self._reliable_publish_continue(deadline, tag, payload, tracker, initial_window)
|
|
299
|
+
except asyncio.CancelledError:
|
|
300
|
+
tracker.compromised = True
|
|
301
|
+
raise
|
|
302
|
+
finally:
|
|
303
|
+
self._release_reliable_publish_tracker(tag, tracker)
|
|
304
|
+
|
|
305
|
+
def close(self) -> None:
|
|
306
|
+
if self.closed:
|
|
307
|
+
return
|
|
308
|
+
self.closed = True
|
|
309
|
+
self._topic.pub_count -= 1
|
|
310
|
+
self._topic.sync_implicit()
|
|
311
|
+
_logger.info("Publisher closed for '%s'", self._topic.name)
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
# =====================================================================================================================
|
|
315
|
+
# Response Stream
|
|
316
|
+
# =====================================================================================================================
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
class ResponseStreamImpl(ResponseStream):
|
|
320
|
+
def __init__(
|
|
321
|
+
self,
|
|
322
|
+
node: NodeImpl,
|
|
323
|
+
topic: TopicImpl,
|
|
324
|
+
message_tag: int,
|
|
325
|
+
response_timeout: float,
|
|
326
|
+
) -> None:
|
|
327
|
+
self._node = node
|
|
328
|
+
self._topic = topic
|
|
329
|
+
self._message_tag = message_tag
|
|
330
|
+
self._response_timeout = response_timeout
|
|
331
|
+
self.queue: asyncio.Queue[Response | BaseException] = asyncio.Queue()
|
|
332
|
+
self.closed = False
|
|
333
|
+
self._reliable_remote_by_id: dict[int, ResponseRemoteState] = {}
|
|
334
|
+
self._publish_task: asyncio.Task[None] | None = None
|
|
335
|
+
self._cleanup_handle: asyncio.TimerHandle | None = None
|
|
336
|
+
|
|
337
|
+
def __aiter__(self) -> ResponseStreamImpl:
|
|
338
|
+
return self
|
|
339
|
+
|
|
340
|
+
async def __anext__(self) -> Response:
|
|
341
|
+
if self.closed:
|
|
342
|
+
raise StopAsyncIteration
|
|
343
|
+
try:
|
|
344
|
+
item = await asyncio.wait_for(self.queue.get(), timeout=self._response_timeout)
|
|
345
|
+
except asyncio.TimeoutError:
|
|
346
|
+
raise LivenessError("Response timeout")
|
|
347
|
+
if isinstance(item, StopAsyncIteration):
|
|
348
|
+
raise item
|
|
349
|
+
if isinstance(item, BaseException):
|
|
350
|
+
raise item
|
|
351
|
+
return item
|
|
352
|
+
|
|
353
|
+
def set_publish_task(self, task: asyncio.Task[None]) -> None:
|
|
354
|
+
self._publish_task = task
|
|
355
|
+
|
|
356
|
+
def on_publish_error(self, ex: BaseException) -> None:
|
|
357
|
+
if self.closed or isinstance(ex, asyncio.CancelledError):
|
|
358
|
+
return
|
|
359
|
+
self.queue.put_nowait(ex)
|
|
360
|
+
|
|
361
|
+
def _remove_from_topic(self) -> None:
|
|
362
|
+
if self._cleanup_handle is not None:
|
|
363
|
+
self._cleanup_handle.cancel()
|
|
364
|
+
self._cleanup_handle = None
|
|
365
|
+
if self._topic.request_futures.get(self._message_tag) is self:
|
|
366
|
+
del self._topic.request_futures[self._message_tag]
|
|
367
|
+
|
|
368
|
+
def _schedule_cleanup(self) -> None:
|
|
369
|
+
if self._cleanup_handle is not None:
|
|
370
|
+
return
|
|
371
|
+
|
|
372
|
+
def cleanup() -> None:
|
|
373
|
+
self._cleanup_handle = None
|
|
374
|
+
self._remove_from_topic()
|
|
375
|
+
|
|
376
|
+
self._cleanup_handle = self._node.loop.call_later(SESSION_LIFETIME / 2, cleanup)
|
|
377
|
+
|
|
378
|
+
def on_response(
|
|
379
|
+
self,
|
|
380
|
+
arrival: TransportArrival,
|
|
381
|
+
hdr: RspBeHeader | RspRelHeader,
|
|
382
|
+
payload: bytes,
|
|
383
|
+
) -> bool:
|
|
384
|
+
"""Called by the node when a response arrives matching our message_tag."""
|
|
385
|
+
reliable = isinstance(hdr, RspRelHeader)
|
|
386
|
+
if self.closed:
|
|
387
|
+
if not reliable:
|
|
388
|
+
return False
|
|
389
|
+
remote = self._reliable_remote_by_id.get(arrival.remote_id)
|
|
390
|
+
return (remote is not None) and remote.accepted_earlier(hdr.seqno)
|
|
391
|
+
|
|
392
|
+
if reliable:
|
|
393
|
+
remote = self._reliable_remote_by_id.get(arrival.remote_id)
|
|
394
|
+
if remote is None:
|
|
395
|
+
remote = ResponseRemoteState(seqno_top=hdr.seqno)
|
|
396
|
+
self._reliable_remote_by_id[arrival.remote_id] = remote
|
|
397
|
+
unique = True
|
|
398
|
+
else:
|
|
399
|
+
accepted, unique = remote.accept(hdr.seqno)
|
|
400
|
+
if not accepted:
|
|
401
|
+
return False
|
|
402
|
+
if not unique:
|
|
403
|
+
_logger.debug("RSP dedup drop remote=%016x seqno=%d", arrival.remote_id, hdr.seqno)
|
|
404
|
+
return True
|
|
405
|
+
|
|
406
|
+
response = Response(
|
|
407
|
+
timestamp=arrival.timestamp,
|
|
408
|
+
remote_id=arrival.remote_id,
|
|
409
|
+
seqno=hdr.seqno,
|
|
410
|
+
message=payload,
|
|
411
|
+
)
|
|
412
|
+
self.queue.put_nowait(response)
|
|
413
|
+
return True
|
|
414
|
+
|
|
415
|
+
def close(self) -> None:
|
|
416
|
+
if self.closed:
|
|
417
|
+
return
|
|
418
|
+
self.closed = True
|
|
419
|
+
if self._publish_task is not None:
|
|
420
|
+
self._publish_task.cancel()
|
|
421
|
+
self._publish_task = None
|
|
422
|
+
if self._reliable_remote_by_id:
|
|
423
|
+
self._schedule_cleanup()
|
|
424
|
+
else:
|
|
425
|
+
self._remove_from_topic()
|
|
426
|
+
self.queue.put_nowait(StopAsyncIteration())
|
|
427
|
+
_logger.debug("Response stream closed for tag=%d", self._message_tag)
|