acemq_amqp.sync
The same library, for programs that are not running an event loop.
Plenty of Python is not asyncio and has no reason to become asyncio because it sends a message. This is a blocking API for those programs::
with sync.connect("amqp://localhost/") as mq:
mq.publisher(routing_key="orders").send({"id": 7})
It is a facade rather than a second implementation. A loop runs on a thread of
its own and every call here is handed to it, so the envelope rules, the codec
negotiation and the retry engine are the ones in
acemq_amqp.connection — there is one set of retry arithmetic in this
library, and it is not this file's.
Handlers run on a worker thread rather than on the loop. A blocking handler called from the loop would stop every other consumer on the connection, and a blocking handler is exactly what somebody using this API has: that is why they are here.
1# Copyright 2026 AceMQ. 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# https://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14 15"""The same library, for programs that are not running an event loop. 16 17Plenty of Python is not asyncio and has no reason to become asyncio because it 18sends a message. This is a blocking API for those programs:: 19 20 with sync.connect("amqp://localhost/") as mq: 21 mq.publisher(routing_key="orders").send({"id": 7}) 22 23It is a facade rather than a second implementation. A loop runs on a thread of 24its own and every call here is handed to it, so the envelope rules, the codec 25negotiation and the retry engine are the ones in 26:mod:`acemq_amqp.connection` — there is one set of retry arithmetic in this 27library, and it is not this file's. 28 29Handlers run on a worker thread rather than on the loop. A blocking handler 30called from the loop would stop every other consumer on the connection, and a 31blocking handler is exactly what somebody using this API has: that is why they 32are here. 33""" 34 35from __future__ import annotations 36 37import asyncio 38import threading 39from collections.abc import Callable, Coroutine, Iterable, Mapping 40from concurrent.futures import ThreadPoolExecutor 41from datetime import timedelta 42from typing import Any, TypeAlias, TypeVar 43 44from .ack import Ack 45from .codec import Codec 46from .connection import ( 47 DEFAULT_CONFIRM_TIMEOUT, 48 DEFAULT_DRAIN_TIMEOUT, 49 DEFAULT_MAX_OUTSTANDING_PUBLISHES, 50 DEFAULT_PREFETCH, 51 Connection, 52 Consumer, 53 Message, 54 Publisher, 55) 56from .connection import connect as _connect_async 57from .envelope import Envelope 58from .retry import RetryPolicy 59from .security import Security 60from .topology import Topology 61from .transport import PublishResult 62 63T = TypeVar("T") 64 65#: What a blocking handler is: a message in, a decision out. 66SyncHandler: TypeAlias = Callable[[Message], Ack] 67 68 69class _LoopThread: 70 """An event loop on a thread, and the way to get work onto it. 71 72 One per connection rather than one per process, so two connections cannot 73 starve each other and closing one does not take the other's loop with it. 74 """ 75 76 def __init__(self, name: str) -> None: 77 self._loop = asyncio.new_event_loop() 78 self._ready = threading.Event() 79 self._thread = threading.Thread(target=self._run, name=name, daemon=True) 80 self._thread.start() 81 self._ready.wait() 82 83 def _run(self) -> None: 84 asyncio.set_event_loop(self._loop) 85 self._loop.call_soon(self._ready.set) 86 self._loop.run_forever() 87 88 def run(self, work: Coroutine[Any, Any, T]) -> T: 89 """Runs a coroutine on the loop and waits for its answer here.""" 90 return asyncio.run_coroutine_threadsafe(work, self._loop).result() 91 92 def stop(self) -> None: 93 """Stops the loop and waits for its thread. 94 95 Joined rather than left to the daemon flag, so that a connection closed 96 in a test is really gone by the time the next test starts rather than 97 still holding a socket. 98 """ 99 self._loop.call_soon_threadsafe(self._loop.stop) 100 self._thread.join() 101 self._loop.close() 102 103 104class SyncPublisher: 105 """A :class:`~acemq_amqp.Publisher` you can call without awaiting.""" 106 107 def __init__(self, loop: _LoopThread, publisher: Publisher) -> None: 108 self._loop = loop 109 self._publisher = publisher 110 111 def send( 112 self, 113 payload: Any, 114 *, 115 envelope: Envelope | None = None, 116 routing_key: str | None = None, 117 ) -> PublishResult: 118 """Publishes one message and waits for the broker to answer. 119 120 :param payload: what to send 121 :param envelope: metadata to send instead of a fresh one 122 :param routing_key: what to publish under, overriding the publisher's 123 :returns: what the broker said 124 """ 125 return self._loop.run( 126 self._publisher.send(payload, envelope=envelope, routing_key=routing_key) 127 ) 128 129 def send_all(self, payloads: Iterable[Any]) -> list[PublishResult]: 130 """Publishes a batch and waits for every confirm. 131 132 Every message goes out before any confirm is awaited, which is the 133 whole point of it: a loop calling :meth:`send` pays a broker round trip 134 per message. See :meth:`acemq_amqp.Publisher.send_all` for what a 135 partial failure reports and why it is not atomic. 136 137 :param payloads: what to send, in order 138 :returns: what the broker said about each, in the order the payloads 139 were given 140 :raises PublishError: when any message was not confirmed 141 """ 142 return self._loop.run(self._publisher.send_all(payloads)) 143 144 145class SyncConsumer: 146 """A running subscription. Close it to stop.""" 147 148 def __init__( 149 self, connection: SyncConnection, consumer: Consumer, workers: ThreadPoolExecutor 150 ) -> None: 151 self._connection = connection 152 self._consumer = consumer 153 self._workers = workers 154 155 @property 156 def queue(self) -> str: 157 """The queue this consumer reads.""" 158 return self._consumer.queue 159 160 def close(self, *, timeout: float | None = DEFAULT_DRAIN_TIMEOUT) -> bool: 161 """Stops the consumer and waits, for up to ``timeout``, for the handlers 162 already running. 163 164 Delivery stops first, and a message that had been delivered but not 165 started is given back to the broker. A handler that finishes in time is 166 settled as usual — see :meth:`acemq_amqp.Consumer.close`. 167 168 **A handler still running at the deadline is abandoned, not stopped.** 169 Python cannot cancel a thread, so it keeps running until the handler 170 returns of its own accord, and whatever it returns is thrown away. Its 171 message is left unsettled — never acknowledged, never rejected, never 172 dead-lettered — so the broker hands it out again once the channel goes: 173 a handler that was cut off may have done its work, and the message may 174 be handled twice, which is the at-least-once promise and not a loss. 175 The settlement belongs to the consumer, not to the thread, so a thread 176 that finishes late has nothing to acknowledge with and cannot settle a 177 message on a channel that has closed or been replaced. 178 179 The abandoned thread is a normal, non-daemon thread: the interpreter 180 waits for it at exit. A handler that can hang should carry its own 181 timeout. 182 183 :param timeout: seconds to wait for every running handler together; 184 ``None`` waits for as long as they take 185 :returns: ``True`` if every handler finished, ``False`` if the time ran 186 out first and some were abandoned — the consumer is stopped either way 187 """ 188 try: 189 return self._connection._loop.run(self._consumer.close(timeout=timeout)) 190 finally: 191 # Not waited for: a thread still running here is one the deadline 192 # has already given up on. 193 self._workers.shutdown(wait=False, cancel_futures=True) 194 195 def __enter__(self) -> SyncConsumer: 196 return self 197 198 def __exit__(self, *_: object) -> None: 199 self.close() 200 201 202class SyncConnection: 203 """A connection to a broker, without an event loop in sight. 204 205 Everything on it blocks until the broker has answered, which is the point: 206 a program that is not running a loop should not have to start one to send a 207 message. 208 """ 209 210 def __init__(self, loop: _LoopThread, connection: Connection) -> None: 211 self._loop = loop 212 self._connection = connection 213 self._closed = False 214 self._workers: list[ThreadPoolExecutor] = [] 215 216 @property 217 def connection(self) -> Connection: 218 """The asyncio connection underneath, for code that has a loop after all.""" 219 return self._connection 220 221 def declare(self, topology: Topology) -> None: 222 """Applies a topology to this connection's broker.""" 223 self._loop.run(self._connection.declare(topology)) 224 225 def publisher( 226 self, 227 exchange: str = "", 228 routing_key: str = "", 229 *, 230 codec: Codec | None = None, 231 persistent: bool = True, 232 mandatory: bool = False, 233 ) -> SyncPublisher: 234 """Builds a publisher for an exchange and routing key.""" 235 return SyncPublisher( 236 self._loop, 237 self._connection.publisher( 238 exchange, 239 routing_key, 240 codec=codec, 241 persistent=persistent, 242 mandatory=mandatory, 243 ), 244 ) 245 246 def consume( 247 self, 248 queue: str, 249 handler: SyncHandler, 250 *, 251 codec: Codec | None = None, 252 retry: RetryPolicy | None = None, 253 prefetch: int | None = None, 254 concurrency: int = 1, 255 tag: str = "", 256 args: Mapping[str, Any] | None = None, 257 declare: bool = True, 258 ) -> SyncConsumer: 259 """Reads messages from a queue until the returned consumer is closed. 260 261 The handler is called on a worker thread, not on the loop, so it may 262 block for as long as it needs to without stopping anything else. There 263 are ``concurrency`` threads and ``concurrency`` messages in flight, so 264 the number means the same thing here as it does on the async API. 265 266 :param queue: what to read 267 :param handler: what to do with a message 268 :param codec: a codec other than this connection's 269 :param retry: a policy other than this connection's 270 :param prefetch: how many unacknowledged messages to hold 271 :param concurrency: how many messages to work on at once 272 :param tag: what to call this consumer to the broker 273 :param args: broker-specific consumer arguments 274 :param declare: declare ``{queue}.dlq``, ``{queue}.parked`` and the 275 rungs before subscribing. On by default; see 276 :meth:`acemq_amqp.Connection.consume` 277 :returns: the running consumer 278 """ 279 workers = ThreadPoolExecutor( 280 max_workers=concurrency, thread_name_prefix=f"acemq-{queue}" 281 ) 282 self._workers.append(workers) 283 284 async def run_on_a_thread(message: Message) -> Ack: 285 loop = asyncio.get_running_loop() 286 return await loop.run_in_executor(workers, handler, message) 287 288 consumer = self._loop.run( 289 self._connection.consume( 290 queue, 291 run_on_a_thread, 292 codec=codec, 293 retry=retry, 294 prefetch=prefetch, 295 concurrency=concurrency, 296 tag=tag, 297 args=args, 298 declare=declare, 299 ) 300 ) 301 return SyncConsumer(self, consumer, workers) 302 303 def queue_exists(self, queue: str) -> bool: 304 """Whether a queue is on the broker, creating nothing.""" 305 return self._loop.run(self._connection.queue_exists(queue)) 306 307 def message_count(self, queue: str) -> int: 308 """How many messages are waiting on a queue.""" 309 return self._loop.run(self._connection.message_count(queue)) 310 311 def delete_queue(self, queue: str) -> None: 312 """Removes a queue and every message still on it.""" 313 self._loop.run(self._connection.delete_queue(queue)) 314 315 def close(self, *, timeout: float | None = DEFAULT_DRAIN_TIMEOUT) -> bool: 316 """Stops every consumer, releases the connection and stops the loop. 317 318 Every consumer's running handlers share one deadline, ``timeout``. A 319 handler still running at it is abandoned exactly as 320 :meth:`SyncConsumer.close` describes: its thread keeps running, its 321 result is discarded and its message is left unsettled for the broker 322 to redeliver. 323 324 :param timeout: seconds to wait for the running handlers; ``None`` 325 waits for as long as they take 326 :returns: ``True`` if every handler finished, ``False`` if the time ran 327 out first. Closing again returns ``True`` 328 """ 329 if self._closed: 330 return True 331 self._closed = True 332 try: 333 return self._loop.run(self._connection.close(timeout=timeout)) 334 finally: 335 self._loop.stop() 336 for workers in self._workers: 337 workers.shutdown(wait=False, cancel_futures=True) 338 339 def __enter__(self) -> SyncConnection: 340 return self 341 342 def __exit__(self, *_: object) -> None: 343 self.close() 344 345 346def connect( 347 url: str, 348 *, 349 codec: Codec | None = None, 350 origin: str | None = None, 351 retry: RetryPolicy | None = None, 352 prefetch: int = DEFAULT_PREFETCH, 353 max_outstanding_publishes: int = DEFAULT_MAX_OUTSTANDING_PUBLISHES, 354 confirm_timeout: timedelta = DEFAULT_CONFIRM_TIMEOUT, 355 security: Security | None = None, 356 **transport_options: Any, 357) -> SyncConnection: 358 """Opens a connection to a broker, blocking until it is open. 359 360 Takes the same arguments as :func:`acemq_amqp.connect` and returns the 361 blocking shape of the same thing — including ``security``, because a program 362 that is not running an event loop has exactly the same broker to reach:: 363 364 with sync.connect( 365 "amqps://broker.internal:5671/", 366 security=Security( 367 certificate_authority="certs/ca.crt", 368 credentials=credentials_from_environment(), 369 ), 370 ) as mq: 371 ... 372 373 :param url: where the broker is 374 :param codec: what publishers and consumers use unless they say otherwise 375 :param origin: what to stamp on published messages 376 :param retry: what consumers use unless they say otherwise 377 :param prefetch: how many unacknowledged messages a consumer holds 378 :param max_outstanding_publishes: how many publishes may be waiting for the 379 broker at once, a thousand by default. See 380 :class:`acemq_amqp.Connection` 381 :param confirm_timeout: how long a publish waits for room before raising 382 :param security: how to verify the broker and who to log in as 383 :param transport_options: passed to the transport 384 :returns: the connection 385 """ 386 loop = _LoopThread(name="acemq-loop") 387 try: 388 connection = loop.run( 389 _connect_async( 390 url, 391 codec=codec, 392 origin=origin, 393 retry=retry, 394 prefetch=prefetch, 395 max_outstanding_publishes=max_outstanding_publishes, 396 confirm_timeout=confirm_timeout, 397 security=security, 398 **transport_options, 399 ) 400 ) 401 except BaseException: 402 # The loop outlives a failed connect otherwise, and a thread nobody has 403 # a reference to is a thread nobody can stop. 404 loop.stop() 405 raise 406 return SyncConnection(loop, connection)
105class SyncPublisher: 106 """A :class:`~acemq_amqp.Publisher` you can call without awaiting.""" 107 108 def __init__(self, loop: _LoopThread, publisher: Publisher) -> None: 109 self._loop = loop 110 self._publisher = publisher 111 112 def send( 113 self, 114 payload: Any, 115 *, 116 envelope: Envelope | None = None, 117 routing_key: str | None = None, 118 ) -> PublishResult: 119 """Publishes one message and waits for the broker to answer. 120 121 :param payload: what to send 122 :param envelope: metadata to send instead of a fresh one 123 :param routing_key: what to publish under, overriding the publisher's 124 :returns: what the broker said 125 """ 126 return self._loop.run( 127 self._publisher.send(payload, envelope=envelope, routing_key=routing_key) 128 ) 129 130 def send_all(self, payloads: Iterable[Any]) -> list[PublishResult]: 131 """Publishes a batch and waits for every confirm. 132 133 Every message goes out before any confirm is awaited, which is the 134 whole point of it: a loop calling :meth:`send` pays a broker round trip 135 per message. See :meth:`acemq_amqp.Publisher.send_all` for what a 136 partial failure reports and why it is not atomic. 137 138 :param payloads: what to send, in order 139 :returns: what the broker said about each, in the order the payloads 140 were given 141 :raises PublishError: when any message was not confirmed 142 """ 143 return self._loop.run(self._publisher.send_all(payloads))
A ~acemq_amqp.Publisher you can call without awaiting.
112 def send( 113 self, 114 payload: Any, 115 *, 116 envelope: Envelope | None = None, 117 routing_key: str | None = None, 118 ) -> PublishResult: 119 """Publishes one message and waits for the broker to answer. 120 121 :param payload: what to send 122 :param envelope: metadata to send instead of a fresh one 123 :param routing_key: what to publish under, overriding the publisher's 124 :returns: what the broker said 125 """ 126 return self._loop.run( 127 self._publisher.send(payload, envelope=envelope, routing_key=routing_key) 128 )
Publishes one message and waits for the broker to answer.
Parameters
- payload: what to send
- envelope: metadata to send instead of a fresh one
- routing_key: what to publish under, overriding the publisher's :returns: what the broker said
130 def send_all(self, payloads: Iterable[Any]) -> list[PublishResult]: 131 """Publishes a batch and waits for every confirm. 132 133 Every message goes out before any confirm is awaited, which is the 134 whole point of it: a loop calling :meth:`send` pays a broker round trip 135 per message. See :meth:`acemq_amqp.Publisher.send_all` for what a 136 partial failure reports and why it is not atomic. 137 138 :param payloads: what to send, in order 139 :returns: what the broker said about each, in the order the payloads 140 were given 141 :raises PublishError: when any message was not confirmed 142 """ 143 return self._loop.run(self._publisher.send_all(payloads))
Publishes a batch and waits for every confirm.
Every message goes out before any confirm is awaited, which is the
whole point of it: a loop calling send() pays a broker round trip
per message. See acemq_amqp.Publisher.send_all() for what a
partial failure reports and why it is not atomic.
Parameters
- payloads: what to send, in order :returns: what the broker said about each, in the order the payloads were given
Raises
- PublishError: when any message was not confirmed
146class SyncConsumer: 147 """A running subscription. Close it to stop.""" 148 149 def __init__( 150 self, connection: SyncConnection, consumer: Consumer, workers: ThreadPoolExecutor 151 ) -> None: 152 self._connection = connection 153 self._consumer = consumer 154 self._workers = workers 155 156 @property 157 def queue(self) -> str: 158 """The queue this consumer reads.""" 159 return self._consumer.queue 160 161 def close(self, *, timeout: float | None = DEFAULT_DRAIN_TIMEOUT) -> bool: 162 """Stops the consumer and waits, for up to ``timeout``, for the handlers 163 already running. 164 165 Delivery stops first, and a message that had been delivered but not 166 started is given back to the broker. A handler that finishes in time is 167 settled as usual — see :meth:`acemq_amqp.Consumer.close`. 168 169 **A handler still running at the deadline is abandoned, not stopped.** 170 Python cannot cancel a thread, so it keeps running until the handler 171 returns of its own accord, and whatever it returns is thrown away. Its 172 message is left unsettled — never acknowledged, never rejected, never 173 dead-lettered — so the broker hands it out again once the channel goes: 174 a handler that was cut off may have done its work, and the message may 175 be handled twice, which is the at-least-once promise and not a loss. 176 The settlement belongs to the consumer, not to the thread, so a thread 177 that finishes late has nothing to acknowledge with and cannot settle a 178 message on a channel that has closed or been replaced. 179 180 The abandoned thread is a normal, non-daemon thread: the interpreter 181 waits for it at exit. A handler that can hang should carry its own 182 timeout. 183 184 :param timeout: seconds to wait for every running handler together; 185 ``None`` waits for as long as they take 186 :returns: ``True`` if every handler finished, ``False`` if the time ran 187 out first and some were abandoned — the consumer is stopped either way 188 """ 189 try: 190 return self._connection._loop.run(self._consumer.close(timeout=timeout)) 191 finally: 192 # Not waited for: a thread still running here is one the deadline 193 # has already given up on. 194 self._workers.shutdown(wait=False, cancel_futures=True) 195 196 def __enter__(self) -> SyncConsumer: 197 return self 198 199 def __exit__(self, *_: object) -> None: 200 self.close()
A running subscription. Close it to stop.
156 @property 157 def queue(self) -> str: 158 """The queue this consumer reads.""" 159 return self._consumer.queue
The queue this consumer reads.
161 def close(self, *, timeout: float | None = DEFAULT_DRAIN_TIMEOUT) -> bool: 162 """Stops the consumer and waits, for up to ``timeout``, for the handlers 163 already running. 164 165 Delivery stops first, and a message that had been delivered but not 166 started is given back to the broker. A handler that finishes in time is 167 settled as usual — see :meth:`acemq_amqp.Consumer.close`. 168 169 **A handler still running at the deadline is abandoned, not stopped.** 170 Python cannot cancel a thread, so it keeps running until the handler 171 returns of its own accord, and whatever it returns is thrown away. Its 172 message is left unsettled — never acknowledged, never rejected, never 173 dead-lettered — so the broker hands it out again once the channel goes: 174 a handler that was cut off may have done its work, and the message may 175 be handled twice, which is the at-least-once promise and not a loss. 176 The settlement belongs to the consumer, not to the thread, so a thread 177 that finishes late has nothing to acknowledge with and cannot settle a 178 message on a channel that has closed or been replaced. 179 180 The abandoned thread is a normal, non-daemon thread: the interpreter 181 waits for it at exit. A handler that can hang should carry its own 182 timeout. 183 184 :param timeout: seconds to wait for every running handler together; 185 ``None`` waits for as long as they take 186 :returns: ``True`` if every handler finished, ``False`` if the time ran 187 out first and some were abandoned — the consumer is stopped either way 188 """ 189 try: 190 return self._connection._loop.run(self._consumer.close(timeout=timeout)) 191 finally: 192 # Not waited for: a thread still running here is one the deadline 193 # has already given up on. 194 self._workers.shutdown(wait=False, cancel_futures=True)
Stops the consumer and waits, for up to timeout, for the handlers
already running.
Delivery stops first, and a message that had been delivered but not
started is given back to the broker. A handler that finishes in time is
settled as usual — see acemq_amqp.Consumer.close().
A handler still running at the deadline is abandoned, not stopped. Python cannot cancel a thread, so it keeps running until the handler returns of its own accord, and whatever it returns is thrown away. Its message is left unsettled — never acknowledged, never rejected, never dead-lettered — so the broker hands it out again once the channel goes: a handler that was cut off may have done its work, and the message may be handled twice, which is the at-least-once promise and not a loss. The settlement belongs to the consumer, not to the thread, so a thread that finishes late has nothing to acknowledge with and cannot settle a message on a channel that has closed or been replaced.
The abandoned thread is a normal, non-daemon thread: the interpreter waits for it at exit. A handler that can hang should carry its own timeout.
Parameters
- timeout: seconds to wait for every running handler together;
Nonewaits for as long as they take :returns:Trueif every handler finished,Falseif the time ran out first and some were abandoned — the consumer is stopped either way
203class SyncConnection: 204 """A connection to a broker, without an event loop in sight. 205 206 Everything on it blocks until the broker has answered, which is the point: 207 a program that is not running a loop should not have to start one to send a 208 message. 209 """ 210 211 def __init__(self, loop: _LoopThread, connection: Connection) -> None: 212 self._loop = loop 213 self._connection = connection 214 self._closed = False 215 self._workers: list[ThreadPoolExecutor] = [] 216 217 @property 218 def connection(self) -> Connection: 219 """The asyncio connection underneath, for code that has a loop after all.""" 220 return self._connection 221 222 def declare(self, topology: Topology) -> None: 223 """Applies a topology to this connection's broker.""" 224 self._loop.run(self._connection.declare(topology)) 225 226 def publisher( 227 self, 228 exchange: str = "", 229 routing_key: str = "", 230 *, 231 codec: Codec | None = None, 232 persistent: bool = True, 233 mandatory: bool = False, 234 ) -> SyncPublisher: 235 """Builds a publisher for an exchange and routing key.""" 236 return SyncPublisher( 237 self._loop, 238 self._connection.publisher( 239 exchange, 240 routing_key, 241 codec=codec, 242 persistent=persistent, 243 mandatory=mandatory, 244 ), 245 ) 246 247 def consume( 248 self, 249 queue: str, 250 handler: SyncHandler, 251 *, 252 codec: Codec | None = None, 253 retry: RetryPolicy | None = None, 254 prefetch: int | None = None, 255 concurrency: int = 1, 256 tag: str = "", 257 args: Mapping[str, Any] | None = None, 258 declare: bool = True, 259 ) -> SyncConsumer: 260 """Reads messages from a queue until the returned consumer is closed. 261 262 The handler is called on a worker thread, not on the loop, so it may 263 block for as long as it needs to without stopping anything else. There 264 are ``concurrency`` threads and ``concurrency`` messages in flight, so 265 the number means the same thing here as it does on the async API. 266 267 :param queue: what to read 268 :param handler: what to do with a message 269 :param codec: a codec other than this connection's 270 :param retry: a policy other than this connection's 271 :param prefetch: how many unacknowledged messages to hold 272 :param concurrency: how many messages to work on at once 273 :param tag: what to call this consumer to the broker 274 :param args: broker-specific consumer arguments 275 :param declare: declare ``{queue}.dlq``, ``{queue}.parked`` and the 276 rungs before subscribing. On by default; see 277 :meth:`acemq_amqp.Connection.consume` 278 :returns: the running consumer 279 """ 280 workers = ThreadPoolExecutor( 281 max_workers=concurrency, thread_name_prefix=f"acemq-{queue}" 282 ) 283 self._workers.append(workers) 284 285 async def run_on_a_thread(message: Message) -> Ack: 286 loop = asyncio.get_running_loop() 287 return await loop.run_in_executor(workers, handler, message) 288 289 consumer = self._loop.run( 290 self._connection.consume( 291 queue, 292 run_on_a_thread, 293 codec=codec, 294 retry=retry, 295 prefetch=prefetch, 296 concurrency=concurrency, 297 tag=tag, 298 args=args, 299 declare=declare, 300 ) 301 ) 302 return SyncConsumer(self, consumer, workers) 303 304 def queue_exists(self, queue: str) -> bool: 305 """Whether a queue is on the broker, creating nothing.""" 306 return self._loop.run(self._connection.queue_exists(queue)) 307 308 def message_count(self, queue: str) -> int: 309 """How many messages are waiting on a queue.""" 310 return self._loop.run(self._connection.message_count(queue)) 311 312 def delete_queue(self, queue: str) -> None: 313 """Removes a queue and every message still on it.""" 314 self._loop.run(self._connection.delete_queue(queue)) 315 316 def close(self, *, timeout: float | None = DEFAULT_DRAIN_TIMEOUT) -> bool: 317 """Stops every consumer, releases the connection and stops the loop. 318 319 Every consumer's running handlers share one deadline, ``timeout``. A 320 handler still running at it is abandoned exactly as 321 :meth:`SyncConsumer.close` describes: its thread keeps running, its 322 result is discarded and its message is left unsettled for the broker 323 to redeliver. 324 325 :param timeout: seconds to wait for the running handlers; ``None`` 326 waits for as long as they take 327 :returns: ``True`` if every handler finished, ``False`` if the time ran 328 out first. Closing again returns ``True`` 329 """ 330 if self._closed: 331 return True 332 self._closed = True 333 try: 334 return self._loop.run(self._connection.close(timeout=timeout)) 335 finally: 336 self._loop.stop() 337 for workers in self._workers: 338 workers.shutdown(wait=False, cancel_futures=True) 339 340 def __enter__(self) -> SyncConnection: 341 return self 342 343 def __exit__(self, *_: object) -> None: 344 self.close()
A connection to a broker, without an event loop in sight.
Everything on it blocks until the broker has answered, which is the point: a program that is not running a loop should not have to start one to send a message.
217 @property 218 def connection(self) -> Connection: 219 """The asyncio connection underneath, for code that has a loop after all.""" 220 return self._connection
The asyncio connection underneath, for code that has a loop after all.
222 def declare(self, topology: Topology) -> None: 223 """Applies a topology to this connection's broker.""" 224 self._loop.run(self._connection.declare(topology))
Applies a topology to this connection's broker.
226 def publisher( 227 self, 228 exchange: str = "", 229 routing_key: str = "", 230 *, 231 codec: Codec | None = None, 232 persistent: bool = True, 233 mandatory: bool = False, 234 ) -> SyncPublisher: 235 """Builds a publisher for an exchange and routing key.""" 236 return SyncPublisher( 237 self._loop, 238 self._connection.publisher( 239 exchange, 240 routing_key, 241 codec=codec, 242 persistent=persistent, 243 mandatory=mandatory, 244 ), 245 )
Builds a publisher for an exchange and routing key.
247 def consume( 248 self, 249 queue: str, 250 handler: SyncHandler, 251 *, 252 codec: Codec | None = None, 253 retry: RetryPolicy | None = None, 254 prefetch: int | None = None, 255 concurrency: int = 1, 256 tag: str = "", 257 args: Mapping[str, Any] | None = None, 258 declare: bool = True, 259 ) -> SyncConsumer: 260 """Reads messages from a queue until the returned consumer is closed. 261 262 The handler is called on a worker thread, not on the loop, so it may 263 block for as long as it needs to without stopping anything else. There 264 are ``concurrency`` threads and ``concurrency`` messages in flight, so 265 the number means the same thing here as it does on the async API. 266 267 :param queue: what to read 268 :param handler: what to do with a message 269 :param codec: a codec other than this connection's 270 :param retry: a policy other than this connection's 271 :param prefetch: how many unacknowledged messages to hold 272 :param concurrency: how many messages to work on at once 273 :param tag: what to call this consumer to the broker 274 :param args: broker-specific consumer arguments 275 :param declare: declare ``{queue}.dlq``, ``{queue}.parked`` and the 276 rungs before subscribing. On by default; see 277 :meth:`acemq_amqp.Connection.consume` 278 :returns: the running consumer 279 """ 280 workers = ThreadPoolExecutor( 281 max_workers=concurrency, thread_name_prefix=f"acemq-{queue}" 282 ) 283 self._workers.append(workers) 284 285 async def run_on_a_thread(message: Message) -> Ack: 286 loop = asyncio.get_running_loop() 287 return await loop.run_in_executor(workers, handler, message) 288 289 consumer = self._loop.run( 290 self._connection.consume( 291 queue, 292 run_on_a_thread, 293 codec=codec, 294 retry=retry, 295 prefetch=prefetch, 296 concurrency=concurrency, 297 tag=tag, 298 args=args, 299 declare=declare, 300 ) 301 ) 302 return SyncConsumer(self, consumer, workers)
Reads messages from a queue until the returned consumer is closed.
The handler is called on a worker thread, not on the loop, so it may
block for as long as it needs to without stopping anything else. There
are concurrency threads and concurrency messages in flight, so
the number means the same thing here as it does on the async API.
Parameters
- queue: what to read
- handler: what to do with a message
- codec: a codec other than this connection's
- retry: a policy other than this connection's
- prefetch: how many unacknowledged messages to hold
- concurrency: how many messages to work on at once
- tag: what to call this consumer to the broker
- args: broker-specific consumer arguments
- declare: declare
{queue}.dlq,{queue}.parkedand the rungs before subscribing. On by default; seeacemq_amqp.Connection.consume():returns: the running consumer
304 def queue_exists(self, queue: str) -> bool: 305 """Whether a queue is on the broker, creating nothing.""" 306 return self._loop.run(self._connection.queue_exists(queue))
Whether a queue is on the broker, creating nothing.
308 def message_count(self, queue: str) -> int: 309 """How many messages are waiting on a queue.""" 310 return self._loop.run(self._connection.message_count(queue))
How many messages are waiting on a queue.
312 def delete_queue(self, queue: str) -> None: 313 """Removes a queue and every message still on it.""" 314 self._loop.run(self._connection.delete_queue(queue))
Removes a queue and every message still on it.
316 def close(self, *, timeout: float | None = DEFAULT_DRAIN_TIMEOUT) -> bool: 317 """Stops every consumer, releases the connection and stops the loop. 318 319 Every consumer's running handlers share one deadline, ``timeout``. A 320 handler still running at it is abandoned exactly as 321 :meth:`SyncConsumer.close` describes: its thread keeps running, its 322 result is discarded and its message is left unsettled for the broker 323 to redeliver. 324 325 :param timeout: seconds to wait for the running handlers; ``None`` 326 waits for as long as they take 327 :returns: ``True`` if every handler finished, ``False`` if the time ran 328 out first. Closing again returns ``True`` 329 """ 330 if self._closed: 331 return True 332 self._closed = True 333 try: 334 return self._loop.run(self._connection.close(timeout=timeout)) 335 finally: 336 self._loop.stop() 337 for workers in self._workers: 338 workers.shutdown(wait=False, cancel_futures=True)
Stops every consumer, releases the connection and stops the loop.
Every consumer's running handlers share one deadline, timeout. A
handler still running at it is abandoned exactly as
SyncConsumer.close() describes: its thread keeps running, its
result is discarded and its message is left unsettled for the broker
to redeliver.
Parameters
- timeout: seconds to wait for the running handlers;
Nonewaits for as long as they take :returns:Trueif every handler finished,Falseif the time ran out first. Closing again returnsTrue
347def connect( 348 url: str, 349 *, 350 codec: Codec | None = None, 351 origin: str | None = None, 352 retry: RetryPolicy | None = None, 353 prefetch: int = DEFAULT_PREFETCH, 354 max_outstanding_publishes: int = DEFAULT_MAX_OUTSTANDING_PUBLISHES, 355 confirm_timeout: timedelta = DEFAULT_CONFIRM_TIMEOUT, 356 security: Security | None = None, 357 **transport_options: Any, 358) -> SyncConnection: 359 """Opens a connection to a broker, blocking until it is open. 360 361 Takes the same arguments as :func:`acemq_amqp.connect` and returns the 362 blocking shape of the same thing — including ``security``, because a program 363 that is not running an event loop has exactly the same broker to reach:: 364 365 with sync.connect( 366 "amqps://broker.internal:5671/", 367 security=Security( 368 certificate_authority="certs/ca.crt", 369 credentials=credentials_from_environment(), 370 ), 371 ) as mq: 372 ... 373 374 :param url: where the broker is 375 :param codec: what publishers and consumers use unless they say otherwise 376 :param origin: what to stamp on published messages 377 :param retry: what consumers use unless they say otherwise 378 :param prefetch: how many unacknowledged messages a consumer holds 379 :param max_outstanding_publishes: how many publishes may be waiting for the 380 broker at once, a thousand by default. See 381 :class:`acemq_amqp.Connection` 382 :param confirm_timeout: how long a publish waits for room before raising 383 :param security: how to verify the broker and who to log in as 384 :param transport_options: passed to the transport 385 :returns: the connection 386 """ 387 loop = _LoopThread(name="acemq-loop") 388 try: 389 connection = loop.run( 390 _connect_async( 391 url, 392 codec=codec, 393 origin=origin, 394 retry=retry, 395 prefetch=prefetch, 396 max_outstanding_publishes=max_outstanding_publishes, 397 confirm_timeout=confirm_timeout, 398 security=security, 399 **transport_options, 400 ) 401 ) 402 except BaseException: 403 # The loop outlives a failed connect otherwise, and a thread nobody has 404 # a reference to is a thread nobody can stop. 405 loop.stop() 406 raise 407 return SyncConnection(loop, connection)
Opens a connection to a broker, blocking until it is open.
Takes the same arguments as acemq_amqp.connect() and returns the
blocking shape of the same thing — including security, because a program
that is not running an event loop has exactly the same broker to reach::
with sync.connect(
"amqps://broker.internal:5671/",
security=Security(
certificate_authority="certs/ca.crt",
credentials=credentials_from_environment(),
),
) as mq:
...
Parameters
- url: where the broker is
- codec: what publishers and consumers use unless they say otherwise
- origin: what to stamp on published messages
- retry: what consumers use unless they say otherwise
- prefetch: how many unacknowledged messages a consumer holds
- max_outstanding_publishes: how many publishes may be waiting for the
broker at once, a thousand by default. See
acemq_amqp.Connection - confirm_timeout: how long a publish waits for room before raising
- security: how to verify the broker and who to log in as
- transport_options: passed to the transport :returns: the connection