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)
SyncHandler: TypeAlias = Callable[[acemq_amqp.Message], acemq_amqp.Ack]
class SyncPublisher:
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.

SyncPublisher( loop: acemq_amqp.sync._LoopThread, publisher: acemq_amqp.Publisher)
108    def __init__(self, loop: _LoopThread, publisher: Publisher) -> None:
109        self._loop = loop
110        self._publisher = publisher
def send( self, payload: Any, *, envelope: acemq_amqp.Envelope | None = None, routing_key: str | None = None) -> acemq_amqp.PublishResult:
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
def send_all( self, payloads: Iterable[typing.Any]) -> list[acemq_amqp.PublishResult]:
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
class SyncConsumer:
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.

SyncConsumer( connection: SyncConnection, consumer: acemq_amqp.Consumer, workers: concurrent.futures.thread.ThreadPoolExecutor)
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
queue: str
156    @property
157    def queue(self) -> str:
158        """The queue this consumer reads."""
159        return self._consumer.queue

The queue this consumer reads.

def close(self, *, timeout: float | None = 20.0) -> bool:
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; None waits for as long as they take :returns: True if every handler finished, False if the time ran out first and some were abandoned — the consumer is stopped either way
class SyncConnection:
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.

SyncConnection( loop: acemq_amqp.sync._LoopThread, connection: acemq_amqp.Connection)
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] = []
connection: acemq_amqp.Connection
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.

def declare(self, topology: acemq_amqp.Topology) -> None:
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.

def publisher( self, exchange: str = '', routing_key: str = '', *, codec: acemq_amqp.Codec | None = None, persistent: bool = True, mandatory: bool = False) -> SyncPublisher:
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.

def consume( self, queue: str, handler: Callable[[acemq_amqp.Message], acemq_amqp.Ack], *, codec: acemq_amqp.Codec | None = None, retry: acemq_amqp.RetryPolicy | None = None, prefetch: int | None = None, concurrency: int = 1, tag: str = '', args: Mapping[str, typing.Any] | None = None, declare: bool = True) -> SyncConsumer:
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}.parked and the rungs before subscribing. On by default; see acemq_amqp.Connection.consume() :returns: the running consumer
def queue_exists(self, queue: str) -> bool:
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.

def message_count(self, queue: str) -> int:
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.

def delete_queue(self, queue: str) -> None:
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.

def close(self, *, timeout: float | None = 20.0) -> bool:
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; None waits for as long as they take :returns: True if every handler finished, False if the time ran out first. Closing again returns True
def connect( url: str, *, codec: acemq_amqp.Codec | None = None, origin: str | None = None, retry: acemq_amqp.RetryPolicy | None = None, prefetch: int = 20, max_outstanding_publishes: int = 1000, confirm_timeout: datetime.timedelta = datetime.timedelta(seconds=10), security: acemq_amqp.Security | None = None, **transport_options: Any) -> SyncConnection:
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