agentconnect-cli 0.1.0a1__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.
- agentconnect/__init__.py +3 -0
- agentconnect/a2a_adapter/__init__.py +6 -0
- agentconnect/a2a_adapter/adapter.py +565 -0
- agentconnect/a2a_adapter/cards.py +149 -0
- agentconnect/a2a_adapter/convert.py +157 -0
- agentconnect/a2a_adapter/facade.py +560 -0
- agentconnect/a2a_adapter/seen.py +49 -0
- agentconnect/a2a_adapter/serve.py +67 -0
- agentconnect/adapter.py +319 -0
- agentconnect/adapters.py +52 -0
- agentconnect/apiclient.py +51 -0
- agentconnect/backoff.py +42 -0
- agentconnect/callback.py +314 -0
- agentconnect/cli.py +2324 -0
- agentconnect/cli_json.py +470 -0
- agentconnect/cli_support.py +444 -0
- agentconnect/clock.py +161 -0
- agentconnect/config.py +1213 -0
- agentconnect/daemon/__init__.py +41 -0
- agentconnect/deliveries.py +295 -0
- agentconnect/directory_client.py +255 -0
- agentconnect/failpoints.py +99 -0
- agentconnect/identity.py +111 -0
- agentconnect/isolation.py +345 -0
- agentconnect/keystore.py +279 -0
- agentconnect/limits.py +457 -0
- agentconnect/log.py +143 -0
- agentconnect/mcp/__init__.py +30 -0
- agentconnect/mcp/backend.py +405 -0
- agentconnect/mcp/server.py +382 -0
- agentconnect/mcp/session.py +373 -0
- agentconnect/mcp/tools.py +583 -0
- agentconnect/ownerfile.py +238 -0
- agentconnect/payloads.py +109 -0
- agentconnect/policy.py +1066 -0
- agentconnect/provision.py +239 -0
- agentconnect/py.typed +0 -0
- agentconnect/relay.py +405 -0
- agentconnect/runtime/__init__.py +46 -0
- agentconnect/runtime/agent.py +225 -0
- agentconnect/runtime/api.py +2043 -0
- agentconnect/runtime/engine.py +5359 -0
- agentconnect/runtime/errors.py +47 -0
- agentconnect/runtime/floor.py +649 -0
- agentconnect/runtime/groups.py +1351 -0
- agentconnect/runtime/knocks.py +1422 -0
- agentconnect/runtime/ledger.py +94 -0
- agentconnect/runtime/listing.py +1307 -0
- agentconnect/runtime/registry.py +53 -0
- agentconnect/runtime/runtime.py +158 -0
- agentconnect/runtime/sas.py +1002 -0
- agentconnect/sas_emoji.py +83 -0
- agentconnect/schemas.py +381 -0
- agentconnect/shapes.py +56 -0
- agentconnect/sockets.py +86 -0
- agentconnect/sse.py +234 -0
- agentconnect/store.py +3282 -0
- agentconnect/tasks.py +35 -0
- agentconnect/tokens.py +63 -0
- agentconnect/webhook.py +199 -0
- agentconnect_cli-0.1.0a1.dist-info/METADATA +103 -0
- agentconnect_cli-0.1.0a1.dist-info/RECORD +64 -0
- agentconnect_cli-0.1.0a1.dist-info/WHEEL +4 -0
- agentconnect_cli-0.1.0a1.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
"""One MCP session's runtime: host it, or attach to the one that runs.
|
|
2
|
+
|
|
3
|
+
ADR-005, Decision 2: a session that finds the data directory free hosts the
|
|
4
|
+
runtime (:class:`~agentconnect.mcp.backend.HostBackend`); one that finds it
|
|
5
|
+
locked attaches to the holder (:class:`~agentconnect.mcp.backend.AttachBackend`).
|
|
6
|
+
|
|
7
|
+
When the holder goes away, the session decides from the lock as it is now,
|
|
8
|
+
on every reconnect: held, it attaches to whoever holds it; free, it takes
|
|
9
|
+
the directory over unless its last holder was a daemon or an embedded agent
|
|
10
|
+
(those are supervised by a host of their own, which brings them back, and a
|
|
11
|
+
session holding the lock would keep them from starting). ``flock`` lets one
|
|
12
|
+
session win; the new owner finishes what the old one left in its journal
|
|
13
|
+
(ADR-004). A reconnect that fails leaves no backend: the next call tries
|
|
14
|
+
again.
|
|
15
|
+
|
|
16
|
+
Acknowledgement (at least once, never lost): a wait's batch stays leased
|
|
17
|
+
until the session has written the tool result to the client
|
|
18
|
+
(:meth:`Session.result_written`); only then is it acknowledged. A result
|
|
19
|
+
that was never written (the call was cancelled, the client stopped reading)
|
|
20
|
+
acknowledges nothing, and the batch is offered again when its lease runs
|
|
21
|
+
out. An ack the owner refuses or never answers is retried on the next wait,
|
|
22
|
+
events call or close, and until it went through the batch's messages are
|
|
23
|
+
not treated as repeats: offered again, they are returned again.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import asyncio
|
|
29
|
+
import contextvars
|
|
30
|
+
from collections import OrderedDict
|
|
31
|
+
from collections.abc import Sequence
|
|
32
|
+
from dataclasses import dataclass, field
|
|
33
|
+
from typing import Any, Literal
|
|
34
|
+
|
|
35
|
+
import httpx
|
|
36
|
+
|
|
37
|
+
from agentconnect import log
|
|
38
|
+
from agentconnect.deliveries import LEASE_SECS, delivery_key
|
|
39
|
+
from agentconnect.keystore import DataDirAccessError, DataDirLock
|
|
40
|
+
from agentconnect.mcp.backend import (
|
|
41
|
+
AttachBackend,
|
|
42
|
+
Backend,
|
|
43
|
+
HostBackend,
|
|
44
|
+
OwnerGoneError,
|
|
45
|
+
Pulled,
|
|
46
|
+
ToolError,
|
|
47
|
+
)
|
|
48
|
+
from agentconnect.runtime import Clock, Config, DataDirLockedError, Keystore, Options
|
|
49
|
+
|
|
50
|
+
LOG = log.get("agentconnect.mcp")
|
|
51
|
+
|
|
52
|
+
REMEMBER = 4096 # keys of acknowledged messages kept in memory
|
|
53
|
+
UNWRITTEN_KEEP = 64 # batches whose result isn't written yet, at most
|
|
54
|
+
UNACKED_KEEP = 8 # batches whose ack failed, kept for a retry, at most
|
|
55
|
+
MAX_WAITS = 2 # agentconnect_wait calls running at once in one session
|
|
56
|
+
SUPERVISED = frozenset({"daemon", "embedded"}) # holders a session never takes over
|
|
57
|
+
# Calls that are safe to repeat when the owner went away mid-request.
|
|
58
|
+
REPEATABLE = frozenset(
|
|
59
|
+
{"groups", "group_info", "events", "knock_inspect", "knocks", "listing_status", "take"}
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
# The MCP request a tool call serves (set by the server around each call).
|
|
63
|
+
current_request: contextvars.ContextVar[object | None] = contextvars.ContextVar(
|
|
64
|
+
"agentconnect_mcp_request", default=None
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass
|
|
69
|
+
class _Batch:
|
|
70
|
+
lease: str
|
|
71
|
+
ids: list[str]
|
|
72
|
+
keys: list[str]
|
|
73
|
+
msg_ids: list[str] = field(default_factory=list) # what its result returns
|
|
74
|
+
taken_at: float = 0.0 # Unix seconds on the session's clock
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class Session:
|
|
78
|
+
"""The agent-scope operations of one MCP session, over whichever backend
|
|
79
|
+
serves it now."""
|
|
80
|
+
|
|
81
|
+
def __init__(
|
|
82
|
+
self,
|
|
83
|
+
config: Config,
|
|
84
|
+
clock: Clock,
|
|
85
|
+
*,
|
|
86
|
+
keystore: Keystore | None = None,
|
|
87
|
+
options: Options | None = None,
|
|
88
|
+
transport: httpx.AsyncBaseTransport | None = None,
|
|
89
|
+
) -> None:
|
|
90
|
+
self.config, self._clock = config, clock
|
|
91
|
+
self._keystore, self._options, self._transport = keystore, options, transport
|
|
92
|
+
self.backend: Backend | None = None
|
|
93
|
+
self._opened = False
|
|
94
|
+
self._switch = asyncio.Lock()
|
|
95
|
+
self._waits = 0
|
|
96
|
+
self._acked: OrderedDict[str, None] = OrderedDict() # keys acknowledged
|
|
97
|
+
self._unwritten: OrderedDict[object, _Batch] = OrderedDict()
|
|
98
|
+
self._unacked: list[_Batch] = [] # written, but the ack didn't go through
|
|
99
|
+
self._acking: set[asyncio.Task[None]] = set()
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def mode(self) -> Literal["host", "attach"]:
|
|
103
|
+
return "attach" if self.backend is None else self.backend.mode
|
|
104
|
+
|
|
105
|
+
async def open(self) -> None:
|
|
106
|
+
"""Host the runtime if the directory is free, else attach. Without a
|
|
107
|
+
relay this session can't host: it attaches, or fails if nothing
|
|
108
|
+
holds the directory."""
|
|
109
|
+
self.backend = await self._connect(startup=True)
|
|
110
|
+
self._opened = True
|
|
111
|
+
LOG.info("mcp_session", mode=self.backend.mode)
|
|
112
|
+
|
|
113
|
+
async def _connect(self, *, startup: bool = False) -> Backend:
|
|
114
|
+
"""A backend for the directory as it is now (see the module doc)."""
|
|
115
|
+
try:
|
|
116
|
+
held, holder = DataDirLock.probe(self.config.data_dir)
|
|
117
|
+
except DataDirAccessError as e:
|
|
118
|
+
raise ToolError("data_dir_not_accessible", str(e), 403) from None
|
|
119
|
+
if not held:
|
|
120
|
+
if not startup and holder in SUPERVISED:
|
|
121
|
+
msg = (
|
|
122
|
+
f"the {holder} runtime that owns {self.config.data_dir} isn't running; "
|
|
123
|
+
"start it again and retry"
|
|
124
|
+
)
|
|
125
|
+
raise ToolError("not_running", msg, 503)
|
|
126
|
+
if not self.config.relay_url:
|
|
127
|
+
msg = (
|
|
128
|
+
f"nothing runs on {self.config.data_dir} and no relay is configured: "
|
|
129
|
+
"pass --relay (or set AC_RELAY_URL) so this server can run it"
|
|
130
|
+
)
|
|
131
|
+
raise ToolError("bad_config", msg)
|
|
132
|
+
try:
|
|
133
|
+
return await HostBackend.start(
|
|
134
|
+
self.config, self._clock, keystore=self._keystore, options=self._options
|
|
135
|
+
)
|
|
136
|
+
except DataDirLockedError as e:
|
|
137
|
+
holder = e.holder # another session won the lock meanwhile
|
|
138
|
+
return AttachBackend(self.config, holder, self._transport)
|
|
139
|
+
|
|
140
|
+
def _connected(self) -> Backend | None:
|
|
141
|
+
return self.backend
|
|
142
|
+
|
|
143
|
+
async def _backend(self) -> Backend:
|
|
144
|
+
"""The backend, connecting again first if the last reconnect failed."""
|
|
145
|
+
if self.backend is not None:
|
|
146
|
+
return self.backend
|
|
147
|
+
if not self._opened:
|
|
148
|
+
msg = "the session isn't open"
|
|
149
|
+
raise ToolError("not_running", msg, 503)
|
|
150
|
+
async with self._switch:
|
|
151
|
+
current = self._connected() # another call may have connected meanwhile
|
|
152
|
+
if current is None:
|
|
153
|
+
current = await self._connect()
|
|
154
|
+
self.backend = current
|
|
155
|
+
LOG.info("mcp_reconnected", mode=current.mode)
|
|
156
|
+
return current
|
|
157
|
+
|
|
158
|
+
async def _recover(self, failed: Backend) -> None:
|
|
159
|
+
"""The holder went away: drop the failed backend, then connect to the
|
|
160
|
+
directory as it is now. If that fails too, the next call retries."""
|
|
161
|
+
async with self._switch:
|
|
162
|
+
if self.backend is not failed:
|
|
163
|
+
return # another call already switched
|
|
164
|
+
self.backend = None
|
|
165
|
+
await failed.close()
|
|
166
|
+
self.backend = await self._connect()
|
|
167
|
+
LOG.info("mcp_reconnected", mode=self.backend.mode)
|
|
168
|
+
|
|
169
|
+
async def _call(self, op: str, *args: Any, **kwargs: Any) -> Any:
|
|
170
|
+
backend = await self._backend()
|
|
171
|
+
try:
|
|
172
|
+
return await getattr(backend, op)(*args, **kwargs)
|
|
173
|
+
except OwnerGoneError as e:
|
|
174
|
+
if e.sent and op not in REPEATABLE:
|
|
175
|
+
await self._recover(backend)
|
|
176
|
+
msg = (
|
|
177
|
+
"the runtime went away during this call: it may or may not have "
|
|
178
|
+
"happened; check (agentconnect_groups, agentconnect_events) before "
|
|
179
|
+
"trying again"
|
|
180
|
+
)
|
|
181
|
+
raise ToolError("interrupted", msg, 503) from None
|
|
182
|
+
await self._recover(backend)
|
|
183
|
+
try:
|
|
184
|
+
return await getattr(await self._backend(), op)(*args, **kwargs)
|
|
185
|
+
except OwnerGoneError:
|
|
186
|
+
msg = (
|
|
187
|
+
f"the runtime that owns {self.config.data_dir} isn't answering; "
|
|
188
|
+
"start it again (or end the other session) and retry"
|
|
189
|
+
)
|
|
190
|
+
raise ToolError("not_running", msg, 503) from None
|
|
191
|
+
|
|
192
|
+
async def create_group(self, name: str, purpose: str) -> dict[str, Any]:
|
|
193
|
+
return await self._call("create_group", name, purpose)
|
|
194
|
+
|
|
195
|
+
async def create_invite(self, group: str, max_uses: int, ttl_secs: int) -> dict[str, Any]:
|
|
196
|
+
return await self._call("create_invite", group, max_uses, ttl_secs)
|
|
197
|
+
|
|
198
|
+
async def accept(self, url: str, display_name: str | None) -> dict[str, Any]:
|
|
199
|
+
return await self._call("accept", url, display_name)
|
|
200
|
+
|
|
201
|
+
async def send(
|
|
202
|
+
self, group: str, text: str, to: Sequence[str], reply_to: Sequence[str] | None
|
|
203
|
+
) -> dict[str, Any]:
|
|
204
|
+
return await self._call("send", group, text, to, reply_to)
|
|
205
|
+
|
|
206
|
+
async def groups(self) -> dict[str, Any]:
|
|
207
|
+
return await self._call("groups")
|
|
208
|
+
|
|
209
|
+
async def group_info(self, group: str) -> dict[str, Any]:
|
|
210
|
+
return await self._call("group_info", group)
|
|
211
|
+
|
|
212
|
+
async def leave(self, group: str) -> dict[str, Any]:
|
|
213
|
+
return await self._call("leave", group)
|
|
214
|
+
|
|
215
|
+
async def events(self, after: int, group: str | None) -> dict[str, Any]:
|
|
216
|
+
await self.retry_acks()
|
|
217
|
+
return await self._call("events", after, group)
|
|
218
|
+
|
|
219
|
+
async def knock_inspect(self, listing: str, domain_check: bool | None) -> dict[str, Any]:
|
|
220
|
+
return await self._call("knock_inspect", listing, domain_check)
|
|
221
|
+
|
|
222
|
+
async def knock(
|
|
223
|
+
self,
|
|
224
|
+
listing: str,
|
|
225
|
+
*,
|
|
226
|
+
listing_sha256: str | None,
|
|
227
|
+
display_name: str | None,
|
|
228
|
+
note: str | None,
|
|
229
|
+
domain_check: bool | None,
|
|
230
|
+
) -> dict[str, Any]:
|
|
231
|
+
return await self._call(
|
|
232
|
+
"knock",
|
|
233
|
+
listing,
|
|
234
|
+
listing_sha256=listing_sha256,
|
|
235
|
+
display_name=display_name,
|
|
236
|
+
note=note,
|
|
237
|
+
domain_check=domain_check,
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
async def knocks(self) -> dict[str, Any]:
|
|
241
|
+
return await self._call("knocks")
|
|
242
|
+
|
|
243
|
+
async def listing_status(self) -> dict[str, Any]:
|
|
244
|
+
return await self._call("listing_status")
|
|
245
|
+
|
|
246
|
+
async def take(
|
|
247
|
+
self, group: str | None, addressed_only: bool, wait_secs: float
|
|
248
|
+
) -> tuple[list[Pulled], bool]:
|
|
249
|
+
"""A wait's leased batch. At most :data:`MAX_WAITS` run at once per
|
|
250
|
+
session (another is refused with ``busy``). Messages whose ack went
|
|
251
|
+
through already are acknowledged again quietly and left out. The
|
|
252
|
+
batch is acknowledged once its result is written (for the MCP request
|
|
253
|
+
in :data:`current_request`; without one, at once)."""
|
|
254
|
+
if self._waits >= MAX_WAITS:
|
|
255
|
+
msg = f"{MAX_WAITS} agentconnect_wait calls run already in this session"
|
|
256
|
+
raise ToolError("busy", msg, 429)
|
|
257
|
+
self._waits += 1
|
|
258
|
+
try:
|
|
259
|
+
await self.retry_acks()
|
|
260
|
+
pulls = await self._call("take", group, addressed_only, wait_secs)
|
|
261
|
+
finally:
|
|
262
|
+
self._waits -= 1
|
|
263
|
+
fresh: list[Pulled] = []
|
|
264
|
+
repeats: list[Pulled] = []
|
|
265
|
+
for item in pulls.items:
|
|
266
|
+
key = delivery_key(item.payload)
|
|
267
|
+
(repeats if key in self._acked else fresh).append(item)
|
|
268
|
+
now = self._now()
|
|
269
|
+
if repeats:
|
|
270
|
+
await self._ack(_Batch(pulls.lease, [r.delivery_id for r in repeats], [], [], now))
|
|
271
|
+
if fresh:
|
|
272
|
+
batch = _Batch(
|
|
273
|
+
pulls.lease,
|
|
274
|
+
[f.delivery_id for f in fresh],
|
|
275
|
+
[delivery_key(f.payload) for f in fresh],
|
|
276
|
+
[str(f.payload.get("msg_id")) for f in fresh],
|
|
277
|
+
now,
|
|
278
|
+
)
|
|
279
|
+
request = current_request.get()
|
|
280
|
+
if request is None:
|
|
281
|
+
await self._ack(batch)
|
|
282
|
+
else:
|
|
283
|
+
# A batch still kept under this id never had its result
|
|
284
|
+
# written (the client reused the id): it goes back now.
|
|
285
|
+
await self.abandon(request)
|
|
286
|
+
self._unwritten[request] = batch
|
|
287
|
+
while len(self._unwritten) > UNWRITTEN_KEEP:
|
|
288
|
+
self._unwritten.popitem(last=False) # its lease runs out
|
|
289
|
+
return fresh, bool(pulls.more)
|
|
290
|
+
|
|
291
|
+
def awaiting_write(self) -> bool:
|
|
292
|
+
"""Whether some batch waits for its result to be written."""
|
|
293
|
+
return bool(self._unwritten)
|
|
294
|
+
|
|
295
|
+
def _now(self) -> float:
|
|
296
|
+
return self._clock.now().timestamp()
|
|
297
|
+
|
|
298
|
+
async def result_written(self, request: object, msg_ids: Sequence[str]) -> None:
|
|
299
|
+
"""A result for ``request`` returning ``msg_ids`` reached the client's
|
|
300
|
+
pipe: acknowledge the batch kept under that id, if that result is the
|
|
301
|
+
one that returned it. Another answer with the same id (a client that
|
|
302
|
+
reused it) acknowledges nothing."""
|
|
303
|
+
batch = self._unwritten.get(request)
|
|
304
|
+
if batch is not None and list(msg_ids) == batch.msg_ids:
|
|
305
|
+
del self._unwritten[request]
|
|
306
|
+
await self._ack(batch)
|
|
307
|
+
|
|
308
|
+
def result_written_soon(self, request: object, msg_ids: Sequence[str]) -> None:
|
|
309
|
+
""":meth:`result_written` in a task of its own (from the writer)."""
|
|
310
|
+
if request not in self._unwritten:
|
|
311
|
+
return
|
|
312
|
+
task = asyncio.ensure_future(self.result_written(request, msg_ids))
|
|
313
|
+
self._acking.add(task)
|
|
314
|
+
task.add_done_callback(self._acking.discard)
|
|
315
|
+
|
|
316
|
+
async def abandon(self, request: object) -> None:
|
|
317
|
+
"""The batch kept under ``request`` won't be written (the call was
|
|
318
|
+
cancelled, or the client reused the id): release its lease, so its
|
|
319
|
+
messages are offered again at once."""
|
|
320
|
+
batch = self._unwritten.pop(request, None)
|
|
321
|
+
if batch is None:
|
|
322
|
+
return
|
|
323
|
+
try:
|
|
324
|
+
backend = await self._backend()
|
|
325
|
+
await backend.release(batch.lease, batch.ids)
|
|
326
|
+
except (OwnerGoneError, ToolError) as e:
|
|
327
|
+
LOG.info("mcp_release_failed", code=getattr(e, "code", "gone"))
|
|
328
|
+
|
|
329
|
+
async def _ack(self, batch: _Batch) -> bool:
|
|
330
|
+
"""Acknowledge ``batch``; if the owner refuses or doesn't answer, keep
|
|
331
|
+
it for :meth:`retry_acks` (the newest :data:`UNACKED_KEEP`). Keys
|
|
332
|
+
count as acknowledged only after the owner confirmed them."""
|
|
333
|
+
try:
|
|
334
|
+
backend = await self._backend()
|
|
335
|
+
done = set(await backend.ack(batch.lease, batch.ids))
|
|
336
|
+
except (OwnerGoneError, ToolError) as e:
|
|
337
|
+
LOG.info("mcp_ack_failed", code=getattr(e, "code", "gone"), count=len(batch.ids))
|
|
338
|
+
self._unacked.append(batch)
|
|
339
|
+
del self._unacked[:-UNACKED_KEEP]
|
|
340
|
+
return False
|
|
341
|
+
for delivery_id, key in zip(batch.ids, batch.keys, strict=False):
|
|
342
|
+
if delivery_id in done:
|
|
343
|
+
self._acked[key] = None
|
|
344
|
+
self._acked.move_to_end(key)
|
|
345
|
+
while len(self._acked) > REMEMBER:
|
|
346
|
+
self._acked.popitem(last=False)
|
|
347
|
+
return True
|
|
348
|
+
|
|
349
|
+
async def retry_acks(self) -> None:
|
|
350
|
+
"""Try again the acks that didn't go through, oldest first, until one
|
|
351
|
+
fails again. A batch older than its lease is dropped instead: its
|
|
352
|
+
messages are offered again anyway."""
|
|
353
|
+
fresh_since = self._now() - LEASE_SECS
|
|
354
|
+
pending = [b for b in self._unacked if b.taken_at >= fresh_since]
|
|
355
|
+
self._unacked = []
|
|
356
|
+
for n, batch in enumerate(pending):
|
|
357
|
+
if not await self._ack(batch):
|
|
358
|
+
self._unacked = [*self._unacked[:-1], *pending[n + 1 :], batch][-UNACKED_KEEP:]
|
|
359
|
+
return
|
|
360
|
+
|
|
361
|
+
async def close(self) -> None:
|
|
362
|
+
"""Finish the acks (written results only), then stop the runtime
|
|
363
|
+
(host) or the client (attach)."""
|
|
364
|
+
if self._acking:
|
|
365
|
+
await asyncio.gather(*self._acking, return_exceptions=True)
|
|
366
|
+
if self.backend is not None:
|
|
367
|
+
for request in list(self._unwritten):
|
|
368
|
+
await self.abandon(request)
|
|
369
|
+
await self.retry_acks()
|
|
370
|
+
backend, self.backend = self.backend, None
|
|
371
|
+
self._opened = False
|
|
372
|
+
if backend is not None:
|
|
373
|
+
await backend.close()
|