py-messor 0.2.4__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.
- messor/__init__.py +7 -0
- messor/backends/__init__.py +1 -0
- messor/backends/queue/__init__.py +1 -0
- messor/backends/queue/broker.py +35 -0
- messor/backends/queue/build.py +20 -0
- messor/backends/queue/config.py +17 -0
- messor/backends/queue/consumer.py +47 -0
- messor/backends/queue/producer.py +28 -0
- messor/backends/queue/resource.py +23 -0
- messor/backends/rabbit_aio_pika/__init__.py +1 -0
- messor/backends/rabbit_aio_pika/broker.py +46 -0
- messor/backends/rabbit_aio_pika/build.py +26 -0
- messor/backends/rabbit_aio_pika/config.py +55 -0
- messor/backends/rabbit_aio_pika/consumer.py +77 -0
- messor/backends/rabbit_aio_pika/producer.py +68 -0
- messor/backends/rabbit_aio_pika/resource.py +111 -0
- messor/core/__init__.py +1 -0
- messor/core/_inflight.py +74 -0
- messor/core/broker.py +63 -0
- messor/core/config.py +35 -0
- messor/core/consumer.py +121 -0
- messor/core/producer.py +90 -0
- messor/core/resource.py +25 -0
- messor/errors.py +7 -0
- messor/factory/__init__.py +11 -0
- messor/factory/app.py +135 -0
- messor/factory/backend.py +14 -0
- messor/factory/deco.py +48 -0
- messor/factory/registry.py +52 -0
- messor/pool_executor/__init__.py +1 -0
- messor/pool_executor/base.py +31 -0
- messor/pool_executor/general.py +42 -0
- messor/serializer/__init__.py +1 -0
- messor/serializer/base.py +27 -0
- messor/serializer/orjson_ser.py +27 -0
- messor/serializer/raw.py +24 -0
- messor/task/__init__.py +1 -0
- messor/task/base.py +28 -0
- messor/task/thread.py +26 -0
- messor/utils/__init__.py +1 -0
- messor/utils/deco.py +36 -0
- messor/utils/hook.py +121 -0
- messor/utils/lazy.py +46 -0
- messor/utils/sem.py +49 -0
- messor/utils/timer.py +34 -0
- messor/utils/waiter.py +35 -0
- py_messor-0.2.4.dist-info/METADATA +311 -0
- py_messor-0.2.4.dist-info/RECORD +50 -0
- py_messor-0.2.4.dist-info/WHEEL +4 -0
- py_messor-0.2.4.dist-info/licenses/LICENSE +201 -0
messor/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# coding: utf-8
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# coding: utf-8
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# coding: utf-8
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import typing as t
|
|
5
|
+
|
|
6
|
+
from messor.serializer.base import Message
|
|
7
|
+
from messor.backends.queue.resource import Resource
|
|
8
|
+
from messor.core.broker import BaseBroker as _BaseBroker
|
|
9
|
+
from messor.backends.queue.producer import Producer
|
|
10
|
+
from messor.backends.queue.consumer import Consumer
|
|
11
|
+
|
|
12
|
+
if t.TYPE_CHECKING:
|
|
13
|
+
from messor.backends.queue.config import Config
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Broker(_BaseBroker["Config", Message]):
|
|
17
|
+
"""内存队列通道。队列由这里创建,再交给 Producer 和 Consumer。"""
|
|
18
|
+
|
|
19
|
+
IS_MEMORY = True
|
|
20
|
+
|
|
21
|
+
def __init__(self, config: "Config"):
|
|
22
|
+
resource = Resource(
|
|
23
|
+
serializer_cls=config.serializer_cls,
|
|
24
|
+
maxsize=config.maxsize)
|
|
25
|
+
self._producer = Producer(
|
|
26
|
+
resource=resource,
|
|
27
|
+
publish_timeout=config.publish_timeout,
|
|
28
|
+
)
|
|
29
|
+
self._consumer = Consumer(
|
|
30
|
+
resource=resource,
|
|
31
|
+
coro=config.coro,
|
|
32
|
+
consume_timeout=config.consume_timeout,
|
|
33
|
+
pool_executor_cls=config.consumer_pool_executor_cls,
|
|
34
|
+
pool_maxsize=config.consumer_pool_maxsize,
|
|
35
|
+
)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# coding: utf-8
|
|
2
|
+
|
|
3
|
+
import typing as t
|
|
4
|
+
|
|
5
|
+
from messor.factory.backend import Backend
|
|
6
|
+
from messor.factory.registry import register_backend
|
|
7
|
+
|
|
8
|
+
if t.TYPE_CHECKING:
|
|
9
|
+
from messor.core.broker import BaseBroker
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@register_backend(Backend.QUEUE)
|
|
13
|
+
def build_queue_broker(
|
|
14
|
+
coro: t.Callable[..., t.Coroutine],
|
|
15
|
+
**kwargs: t.Any) -> "BaseBroker[t.Any, t.Any]":
|
|
16
|
+
|
|
17
|
+
from messor.backends.queue.broker import Broker
|
|
18
|
+
from messor.backends.queue.config import Config
|
|
19
|
+
|
|
20
|
+
return Broker(Config(coro=coro, **kwargs))
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# coding: utf-8
|
|
2
|
+
|
|
3
|
+
import typing as t
|
|
4
|
+
from pydantic.dataclasses import dataclass as pyd_dataclass
|
|
5
|
+
|
|
6
|
+
from messor.core.config import Config as _Config
|
|
7
|
+
from messor.core.config import ExtraIgnoreConfig
|
|
8
|
+
from messor.serializer.base import BaseSerializer
|
|
9
|
+
from messor.serializer.raw import Serializer
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@pyd_dataclass(frozen=True, config=ExtraIgnoreConfig)
|
|
13
|
+
class Config(_Config):
|
|
14
|
+
|
|
15
|
+
## shared resource
|
|
16
|
+
maxsize: int = 200
|
|
17
|
+
serializer_cls: type[BaseSerializer] = Serializer
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# coding: utf-8
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
|
|
5
|
+
from messor.serializer.base import Message
|
|
6
|
+
from messor.backends.queue.resource import Resource
|
|
7
|
+
from messor.core._inflight import dec
|
|
8
|
+
from messor.core.consumer import BaseConsumer as _BaseConsumer
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Consumer(_BaseConsumer[Message, Resource]):
|
|
12
|
+
|
|
13
|
+
__slots__ = ()
|
|
14
|
+
|
|
15
|
+
def __init__(self, *args, **kwargs):
|
|
16
|
+
|
|
17
|
+
super().__init__(*args, **kwargs)
|
|
18
|
+
# coro 执行完成后 -1
|
|
19
|
+
self._on_coro_done.register(dec)
|
|
20
|
+
|
|
21
|
+
async def _consume_once(self) -> Message | None:
|
|
22
|
+
|
|
23
|
+
queue = self._resource.queue
|
|
24
|
+
try:
|
|
25
|
+
message: Message = await asyncio.wait_for(
|
|
26
|
+
queue.get(),
|
|
27
|
+
timeout=self._consume_timeout,
|
|
28
|
+
)
|
|
29
|
+
except asyncio.TimeoutError:
|
|
30
|
+
return None
|
|
31
|
+
else:
|
|
32
|
+
await self._dispatch(message)
|
|
33
|
+
queue.task_done()
|
|
34
|
+
return message
|
|
35
|
+
|
|
36
|
+
async def _drain(self) -> None:
|
|
37
|
+
|
|
38
|
+
queue = self._resource.queue
|
|
39
|
+
while not queue.empty():
|
|
40
|
+
message: Message = queue.get_nowait()
|
|
41
|
+
await self._dispatch(message)
|
|
42
|
+
queue.task_done()
|
|
43
|
+
|
|
44
|
+
async def _on_shutdown(self) -> None:
|
|
45
|
+
|
|
46
|
+
await self._resource.queue.join()
|
|
47
|
+
await self._pool_executor.obj.join()
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# coding: utf-8
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
|
|
5
|
+
from messor.errors import PublishTimeoutError
|
|
6
|
+
from messor.serializer.base import Message
|
|
7
|
+
from messor.core._inflight import inc
|
|
8
|
+
from messor.core.producer import BaseProducer
|
|
9
|
+
from messor.backends.queue.resource import Resource
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Producer(BaseProducer[Message, Resource]):
|
|
13
|
+
|
|
14
|
+
def __init__(self, *args, **kwargs):
|
|
15
|
+
|
|
16
|
+
super().__init__(*args, **kwargs)
|
|
17
|
+
# push 成功后 +1
|
|
18
|
+
self._on_publish_success.register(inc)
|
|
19
|
+
|
|
20
|
+
async def _publish(self, msg: Message, *, timeout: float | None) -> None:
|
|
21
|
+
|
|
22
|
+
if timeout:
|
|
23
|
+
try:
|
|
24
|
+
await asyncio.wait_for(self._resource.queue.put(msg), timeout)
|
|
25
|
+
except asyncio.TimeoutError as exc:
|
|
26
|
+
raise PublishTimeoutError("publish timed out") from exc
|
|
27
|
+
return None
|
|
28
|
+
await self._resource.queue.put(msg)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# coding: utf-8
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
|
|
5
|
+
from messor.serializer.base import Message, BaseSerializer
|
|
6
|
+
from messor.core.resource import BaseResource as _Base
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Resource(_Base[Message]):
|
|
10
|
+
|
|
11
|
+
__slots__ = (
|
|
12
|
+
"_queue",
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
def __init__(self, serializer_cls: type[BaseSerializer], maxsize: int) -> None:
|
|
16
|
+
|
|
17
|
+
super().__init__(serializer_cls)
|
|
18
|
+
self._queue: asyncio.Queue[Message] = asyncio.Queue(maxsize=maxsize)
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def queue(self) -> asyncio.Queue[Message]:
|
|
22
|
+
|
|
23
|
+
return self._queue
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# coding: utf-8
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# coding: utf-8
|
|
2
|
+
|
|
3
|
+
import typing as t
|
|
4
|
+
|
|
5
|
+
from messor.core.broker import BaseBroker as _BaseBroker
|
|
6
|
+
from messor.backends.rabbit_aio_pika.consumer import Consumer
|
|
7
|
+
from messor.backends.rabbit_aio_pika.producer import Producer
|
|
8
|
+
from messor.backends.rabbit_aio_pika.resource import Resource
|
|
9
|
+
|
|
10
|
+
if t.TYPE_CHECKING:
|
|
11
|
+
from messor.backends.rabbit_aio_pika.config import Config
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Broker(_BaseBroker["Config", bytes]):
|
|
15
|
+
"""RabbitMQ 通道。一条连接上分开发布和消费,再交给 Producer 和 Consumer。"""
|
|
16
|
+
|
|
17
|
+
def __init__(self, config: "Config"):
|
|
18
|
+
|
|
19
|
+
resource = Resource(
|
|
20
|
+
serializer_cls=config.serializer_cls,
|
|
21
|
+
url=config.url,
|
|
22
|
+
queue_name=config.queue_name,
|
|
23
|
+
enable_declare_queue=config.enable_declare_queue,
|
|
24
|
+
queue_durable=config.queue_durable,
|
|
25
|
+
queue_exclusive=config.queue_exclusive,
|
|
26
|
+
queue_passive=config.queue_passive,
|
|
27
|
+
queue_auto_delete=config.queue_auto_delete,
|
|
28
|
+
queue_arguments=config.queue_arguments,
|
|
29
|
+
)
|
|
30
|
+
self._producer = Producer(
|
|
31
|
+
resource=resource,
|
|
32
|
+
publish_timeout=config.publish_timeout,
|
|
33
|
+
exchange_name=config.exchange_name,
|
|
34
|
+
routing_key=config.routing_key,
|
|
35
|
+
delivery_mode=config.delivery_mode,
|
|
36
|
+
)
|
|
37
|
+
self._consumer = Consumer(
|
|
38
|
+
resource=resource,
|
|
39
|
+
prefetch_count=config.prefetch_count,
|
|
40
|
+
prefetch_size=config.prefetch_size,
|
|
41
|
+
global_qos=config.global_qos,
|
|
42
|
+
coro=config.coro,
|
|
43
|
+
consume_timeout=config.consume_timeout,
|
|
44
|
+
pool_executor_cls=config.consumer_pool_executor_cls,
|
|
45
|
+
pool_maxsize=config.consumer_pool_maxsize,
|
|
46
|
+
)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# coding: utf-8
|
|
2
|
+
|
|
3
|
+
import typing as t
|
|
4
|
+
|
|
5
|
+
from messor.factory.backend import Backend
|
|
6
|
+
from messor.factory.registry import register_backend
|
|
7
|
+
|
|
8
|
+
if t.TYPE_CHECKING:
|
|
9
|
+
from messor.core.broker import BaseBroker
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@register_backend(Backend.RABBIT_AIO_PIKA)
|
|
13
|
+
def build_rabbit_aio_pika_broker(
|
|
14
|
+
coro: t.Callable[..., t.Coroutine],
|
|
15
|
+
**kwargs: t.Any) -> "BaseBroker[t.Any, t.Any]":
|
|
16
|
+
|
|
17
|
+
try:
|
|
18
|
+
from messor.backends.rabbit_aio_pika.broker import Broker
|
|
19
|
+
from messor.backends.rabbit_aio_pika.config import Config
|
|
20
|
+
except ImportError as exc:
|
|
21
|
+
raise ImportError(
|
|
22
|
+
"Backend.RABBIT_AIO_PIKA requires aio-pika. "
|
|
23
|
+
"Install it with the rabbit extra.",
|
|
24
|
+
) from exc
|
|
25
|
+
|
|
26
|
+
return Broker(Config(coro=coro, **kwargs))
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# coding: utf-8
|
|
2
|
+
|
|
3
|
+
from enum import unique, StrEnum, IntEnum
|
|
4
|
+
|
|
5
|
+
from pydantic.dataclasses import dataclass as pyd_dataclass
|
|
6
|
+
|
|
7
|
+
from messor.core.config import Config as _Config
|
|
8
|
+
from messor.core.config import ExtraIgnoreConfig
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@unique
|
|
12
|
+
class ExchangeName(StrEnum):
|
|
13
|
+
|
|
14
|
+
DEFAULT = ""
|
|
15
|
+
DIRECT = "amq.direct"
|
|
16
|
+
TOPIC = "amq.topic"
|
|
17
|
+
FANOUT = "amq.fanout"
|
|
18
|
+
HEADERS = "amq.headers"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@unique
|
|
22
|
+
class DeliveryMode(IntEnum):
|
|
23
|
+
|
|
24
|
+
NOT_PERSISTENT = 1
|
|
25
|
+
PERSISTENT = 2
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@pyd_dataclass(frozen=True, config=ExtraIgnoreConfig, kw_only=True)
|
|
29
|
+
class Config(_Config):
|
|
30
|
+
|
|
31
|
+
## shared resource
|
|
32
|
+
# amqp://user:password@host/vhost?heartbeat=60&x=y
|
|
33
|
+
url: str
|
|
34
|
+
queue_name: str
|
|
35
|
+
enable_declare_queue: bool = False
|
|
36
|
+
# broker 重启后队列仍在;消息是否保留看 delivery_mode
|
|
37
|
+
queue_durable: bool = False
|
|
38
|
+
# 仅当前连接可使用,连接断开后队列删除
|
|
39
|
+
queue_exclusive: bool = False
|
|
40
|
+
# 不创建队列,只确认同名队列已存在,不存在则失败
|
|
41
|
+
queue_passive: bool = False
|
|
42
|
+
# 最后一个消费者取消或断开后删除队列
|
|
43
|
+
queue_auto_delete: bool = False
|
|
44
|
+
# 声明时的附加参数,例如 x-message-ttl、x-dead-letter-exchange
|
|
45
|
+
queue_arguments: dict | None = None
|
|
46
|
+
|
|
47
|
+
## producer
|
|
48
|
+
exchange_name: ExchangeName | str = ExchangeName.DEFAULT
|
|
49
|
+
routing_key: str = ""
|
|
50
|
+
delivery_mode: DeliveryMode = DeliveryMode.PERSISTENT
|
|
51
|
+
|
|
52
|
+
## consumer
|
|
53
|
+
prefetch_count: int = 0
|
|
54
|
+
prefetch_size: int = 0
|
|
55
|
+
global_qos: bool = False
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# coding: utf-8
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import typing as t
|
|
5
|
+
from functools import partial
|
|
6
|
+
|
|
7
|
+
from messor.core.consumer import BaseConsumer as _BaseConsumer
|
|
8
|
+
from messor.pool_executor.base import BasePoolExecutor
|
|
9
|
+
from messor.backends.rabbit_aio_pika.resource import Resource
|
|
10
|
+
|
|
11
|
+
if t.TYPE_CHECKING:
|
|
12
|
+
from aio_pika.abc import AbstractIncomingMessage, AbstractQueue
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Consumer(_BaseConsumer[bytes, Resource]):
|
|
16
|
+
|
|
17
|
+
__slots__ = (
|
|
18
|
+
"_prefetch_count",
|
|
19
|
+
"_prefetch_size",
|
|
20
|
+
"_global_qos",
|
|
21
|
+
# "_channel",
|
|
22
|
+
"_queue",
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
resource: Resource,
|
|
28
|
+
prefetch_count: int,
|
|
29
|
+
prefetch_size: int,
|
|
30
|
+
global_qos: bool,
|
|
31
|
+
coro: t.Callable[..., t.Coroutine],
|
|
32
|
+
consume_timeout: float,
|
|
33
|
+
pool_executor_cls: type[BasePoolExecutor],
|
|
34
|
+
pool_maxsize: int):
|
|
35
|
+
|
|
36
|
+
super().__init__(
|
|
37
|
+
resource=resource,
|
|
38
|
+
coro=coro,
|
|
39
|
+
consume_timeout=consume_timeout,
|
|
40
|
+
pool_executor_cls=pool_executor_cls,
|
|
41
|
+
pool_maxsize=pool_maxsize,
|
|
42
|
+
)
|
|
43
|
+
self._prefetch_count = prefetch_count
|
|
44
|
+
self._prefetch_size = prefetch_size
|
|
45
|
+
self._global_qos = global_qos
|
|
46
|
+
# self._channel: "AbstractChannel | None" = None
|
|
47
|
+
self._queue: "AbstractQueue | None" = None
|
|
48
|
+
|
|
49
|
+
async def _consume_once(self) -> None:
|
|
50
|
+
|
|
51
|
+
queue = await self._ensure_queue()
|
|
52
|
+
async with queue.iterator(timeout=self._consume_timeout) as queue_iter:
|
|
53
|
+
message: "AbstractIncomingMessage"
|
|
54
|
+
try:
|
|
55
|
+
async for message in queue_iter:
|
|
56
|
+
body: bytes = message.body
|
|
57
|
+
await self._dispatch(body)
|
|
58
|
+
await message.ack()
|
|
59
|
+
except asyncio.TimeoutError:
|
|
60
|
+
pass
|
|
61
|
+
|
|
62
|
+
async def _ensure_queue(self) -> "AbstractQueue":
|
|
63
|
+
|
|
64
|
+
if self._queue is not None:
|
|
65
|
+
return self._queue
|
|
66
|
+
|
|
67
|
+
self._on_cleanup.register(self._resource.try_close_connection)
|
|
68
|
+
channel = await self._resource.get_channel()
|
|
69
|
+
self._on_cleanup.register(partial(self._resource.close_channel, channel))
|
|
70
|
+
await channel.set_qos(
|
|
71
|
+
prefetch_count=self._prefetch_count,
|
|
72
|
+
prefetch_size=self._prefetch_size,
|
|
73
|
+
global_=self._global_qos,
|
|
74
|
+
)
|
|
75
|
+
queue = await self._resource.declare_queue(channel)
|
|
76
|
+
self._queue = queue
|
|
77
|
+
return queue
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# coding: utf-8
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import typing as t
|
|
5
|
+
from functools import partial
|
|
6
|
+
|
|
7
|
+
import aio_pika
|
|
8
|
+
|
|
9
|
+
from messor.backends.rabbit_aio_pika.config import DeliveryMode, ExchangeName
|
|
10
|
+
from messor.errors import PublishTimeoutError
|
|
11
|
+
from messor.core.producer import BaseProducer
|
|
12
|
+
from messor.backends.rabbit_aio_pika.resource import Resource
|
|
13
|
+
|
|
14
|
+
if t.TYPE_CHECKING:
|
|
15
|
+
from aio_pika.abc import AbstractExchange
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Producer(BaseProducer[bytes, Resource]):
|
|
19
|
+
|
|
20
|
+
__slots__ = (
|
|
21
|
+
"_exchange_name",
|
|
22
|
+
"_exchange_type",
|
|
23
|
+
"_routing_key",
|
|
24
|
+
"_delivery_mode",
|
|
25
|
+
"_exchange",
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
resource: Resource,
|
|
31
|
+
publish_timeout: float | None,
|
|
32
|
+
exchange_name: ExchangeName | str,
|
|
33
|
+
routing_key: str,
|
|
34
|
+
delivery_mode: DeliveryMode):
|
|
35
|
+
super().__init__(resource, publish_timeout)
|
|
36
|
+
self._exchange_name = exchange_name
|
|
37
|
+
self._exchange: "AbstractExchange | None" = None
|
|
38
|
+
self._routing_key = routing_key
|
|
39
|
+
self._delivery_mode = delivery_mode
|
|
40
|
+
|
|
41
|
+
async def _publish(self, msg: bytes, *, timeout: float | None) -> None:
|
|
42
|
+
|
|
43
|
+
routing_key = self._routing_key or self._resource.queue_name
|
|
44
|
+
message = aio_pika.Message(msg, delivery_mode=self._delivery_mode)
|
|
45
|
+
|
|
46
|
+
await self._ensure_exchange()
|
|
47
|
+
try:
|
|
48
|
+
await self._exchange.publish(message, routing_key=routing_key, timeout=timeout)
|
|
49
|
+
except TimeoutError as exc:
|
|
50
|
+
raise PublishTimeoutError("publish timed out") from exc
|
|
51
|
+
|
|
52
|
+
async def _ensure_exchange(self) -> "AbstractExchange":
|
|
53
|
+
|
|
54
|
+
if self._exchange is not None:
|
|
55
|
+
return self._exchange
|
|
56
|
+
|
|
57
|
+
async with self._lock:
|
|
58
|
+
if self._exchange is None:
|
|
59
|
+
self._on_cleanup.register(self._resource.try_close_connection)
|
|
60
|
+
channel = await self._resource.get_channel()
|
|
61
|
+
self._on_cleanup.register(partial(self._resource.close_channel, channel))
|
|
62
|
+
await self._resource.declare_queue(channel)
|
|
63
|
+
if self._exchange_name == ExchangeName.DEFAULT:
|
|
64
|
+
exchange = channel.default_exchange
|
|
65
|
+
else:
|
|
66
|
+
exchange = await channel.get_exchange(self._exchange_name)
|
|
67
|
+
self._exchange = exchange
|
|
68
|
+
return exchange
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# coding: utf-8
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import typing as t
|
|
5
|
+
|
|
6
|
+
import aio_pika
|
|
7
|
+
|
|
8
|
+
from messor.utils.deco import before
|
|
9
|
+
from messor.core.resource import BaseResource as _Base
|
|
10
|
+
from messor.serializer.base import Message, BaseSerializer
|
|
11
|
+
|
|
12
|
+
if t.TYPE_CHECKING:
|
|
13
|
+
from aio_pika.abc import AbstractConnection, AbstractChannel, AbstractQueue
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Resource(_Base[Message]):
|
|
17
|
+
|
|
18
|
+
__slots__ = (
|
|
19
|
+
"_queue_name",
|
|
20
|
+
"_url",
|
|
21
|
+
"_conn",
|
|
22
|
+
"_lock",
|
|
23
|
+
"_active_channel_num",
|
|
24
|
+
"_enable_declare_queue",
|
|
25
|
+
"_queue_durable",
|
|
26
|
+
"_queue_exclusive",
|
|
27
|
+
"_queue_passive",
|
|
28
|
+
"_queue_auto_delete",
|
|
29
|
+
"_queue_arguments",
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
def __init__(
|
|
33
|
+
self,
|
|
34
|
+
serializer_cls: type[BaseSerializer],
|
|
35
|
+
url: str,
|
|
36
|
+
queue_name: str,
|
|
37
|
+
enable_declare_queue: bool,
|
|
38
|
+
queue_durable: bool,
|
|
39
|
+
queue_exclusive: bool,
|
|
40
|
+
queue_passive: bool,
|
|
41
|
+
queue_auto_delete: bool,
|
|
42
|
+
queue_arguments: dict[str, t.Any] | None):
|
|
43
|
+
|
|
44
|
+
super().__init__(serializer_cls)
|
|
45
|
+
self._queue_name = queue_name
|
|
46
|
+
self._url = url
|
|
47
|
+
self._conn: "AbstractConnection | None" = None
|
|
48
|
+
self._lock = asyncio.Lock()
|
|
49
|
+
self._active_channel_num = 0
|
|
50
|
+
self._enable_declare_queue = enable_declare_queue
|
|
51
|
+
self._queue_durable = queue_durable
|
|
52
|
+
self._queue_exclusive = queue_exclusive
|
|
53
|
+
self._queue_passive = queue_passive
|
|
54
|
+
self._queue_auto_delete = queue_auto_delete
|
|
55
|
+
self._queue_arguments = queue_arguments
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def queue_name(self) -> str:
|
|
59
|
+
|
|
60
|
+
return self._queue_name
|
|
61
|
+
|
|
62
|
+
async def declare_queue(self, channel: "AbstractChannel") -> "AbstractQueue":
|
|
63
|
+
|
|
64
|
+
if self._enable_declare_queue:
|
|
65
|
+
return await channel.declare_queue(
|
|
66
|
+
self._queue_name,
|
|
67
|
+
durable=self._queue_durable,
|
|
68
|
+
exclusive=self._queue_exclusive,
|
|
69
|
+
passive=self._queue_passive,
|
|
70
|
+
auto_delete=self._queue_auto_delete,
|
|
71
|
+
arguments=self._queue_arguments,
|
|
72
|
+
)
|
|
73
|
+
return await channel.declare_queue(self._queue_name, passive=True)
|
|
74
|
+
|
|
75
|
+
async def _ensure_conn(self) -> None:
|
|
76
|
+
|
|
77
|
+
if self._conn is not None:
|
|
78
|
+
return None
|
|
79
|
+
|
|
80
|
+
async with self._lock:
|
|
81
|
+
if self._conn is None:
|
|
82
|
+
self._conn = await aio_pika.connect_robust(self._url)
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def _required_conn(self) -> "AbstractConnection":
|
|
86
|
+
|
|
87
|
+
if self._conn is None:
|
|
88
|
+
raise RuntimeError("RabbitMQ connection not established")
|
|
89
|
+
return self._conn
|
|
90
|
+
|
|
91
|
+
@before(_ensure_conn)
|
|
92
|
+
async def get_channel(self) -> "AbstractChannel":
|
|
93
|
+
|
|
94
|
+
async with self._lock:
|
|
95
|
+
channel = await self._required_conn.channel()
|
|
96
|
+
self._active_channel_num += 1
|
|
97
|
+
return channel
|
|
98
|
+
|
|
99
|
+
async def close_channel(self, channel: "AbstractChannel") -> None:
|
|
100
|
+
|
|
101
|
+
async with self._lock:
|
|
102
|
+
await channel.close()
|
|
103
|
+
self._active_channel_num -= 1
|
|
104
|
+
|
|
105
|
+
async def try_close_connection(self) -> None:
|
|
106
|
+
|
|
107
|
+
async with self._lock:
|
|
108
|
+
if self._active_channel_num > 0 or self._conn is None:
|
|
109
|
+
return None
|
|
110
|
+
await self._required_conn.close()
|
|
111
|
+
self._conn = None
|
messor/core/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# coding: utf-8
|
messor/core/_inflight.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# coding: utf-8
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
|
|
5
|
+
__all__ = (
|
|
6
|
+
"inc",
|
|
7
|
+
"dec",
|
|
8
|
+
"wait_drained",
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class _Inflight:
|
|
13
|
+
"""进程级全局在途消息计数(内存队列专用)。
|
|
14
|
+
|
|
15
|
+
push 成功后 +1,coro 执行完成后 -1;
|
|
16
|
+
计数归 0 表示无在途消息,可通过 wait_drained 等待该状态。
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
__slots__ = (
|
|
20
|
+
"_count",
|
|
21
|
+
"_event",
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
def __init__(self) -> None:
|
|
25
|
+
|
|
26
|
+
self._count = 0
|
|
27
|
+
self._event = asyncio.Event()
|
|
28
|
+
self._event.set()
|
|
29
|
+
|
|
30
|
+
def inc(self, n: int = 1) -> None:
|
|
31
|
+
|
|
32
|
+
self._count += n
|
|
33
|
+
# 有在途消息,阻塞 wait_drained
|
|
34
|
+
self._event.clear()
|
|
35
|
+
|
|
36
|
+
def dec(self, n: int = 1) -> None:
|
|
37
|
+
|
|
38
|
+
self._count = max(0, self._count - n)
|
|
39
|
+
if self._count == 0:
|
|
40
|
+
# 无在途消息,放行 wait_drained
|
|
41
|
+
self._event.set()
|
|
42
|
+
|
|
43
|
+
async def wait_drained(self) -> None:
|
|
44
|
+
|
|
45
|
+
while self._count > 0:
|
|
46
|
+
await self._event.wait()
|
|
47
|
+
|
|
48
|
+
def reset(self) -> None:
|
|
49
|
+
"""清空计数(测试隔离用,不对外暴露)。"""
|
|
50
|
+
|
|
51
|
+
self._count = 0
|
|
52
|
+
self._event.set()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# 全局单例:内存队列 producer/consumer 共享,不对外暴露
|
|
56
|
+
_INFLIGHT = _Inflight()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def inc(n: int = 1) -> None:
|
|
60
|
+
"""push 成功后 +n。"""
|
|
61
|
+
|
|
62
|
+
_INFLIGHT.inc(n)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def dec(n: int = 1) -> None:
|
|
66
|
+
"""coro 执行完成后 -n(默认 -1,可传批量值)。"""
|
|
67
|
+
|
|
68
|
+
_INFLIGHT.dec(n)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
async def wait_drained() -> None:
|
|
72
|
+
"""等待在途消息全部处理完成。"""
|
|
73
|
+
|
|
74
|
+
await _INFLIGHT.wait_drained()
|