queue-func 0.1.2__py3-none-any.whl → 0.2.1__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.
queue_func/broker/base.py CHANGED
@@ -3,7 +3,8 @@
3
3
  import abc
4
4
  import typing as t
5
5
 
6
- from queue_func.serializer.base import BaseSerializer, Message
6
+ from queue_func.serializer.base import Message
7
+ from queue_func.shared_resource.base import Resource
7
8
 
8
9
  if t.TYPE_CHECKING:
9
10
  from queue_func.config.general import Config
@@ -22,33 +23,12 @@ class BaseBroker(t.Generic[ConfigType, Message], metaclass=abc.ABCMeta):
22
23
  """
23
24
 
24
25
  __slots__ = (
25
- "_config",
26
- "_serializer",
27
26
  "_producer",
28
27
  "_consumer",
29
28
  )
30
29
 
31
- def __init__(self, config: ConfigType):
32
-
33
- self._config = config
34
- self._serializer: type[BaseSerializer[Message]] = self._config.serializer_cls
35
- self._producer = self._create_producer()
36
- self._consumer = self._create_consumer()
37
-
38
- @abc.abstractmethod
39
- def _create_producer(self) -> "BaseProducer[Message]":
40
-
41
- pass
42
-
43
- @abc.abstractmethod
44
- def _create_consumer(self) -> "BaseConsumer[Message]":
45
-
46
- pass
47
-
48
- @property
49
- def serializer(self) -> type[BaseSerializer[Message]]:
50
-
51
- return self._serializer
30
+ _producer: "BaseProducer"
31
+ _consumer: "BaseConsumer"
52
32
 
53
33
  async def publish(
54
34
  self,
@@ -4,6 +4,7 @@ import asyncio
4
4
  import typing as t
5
5
 
6
6
  from queue_func.serializer.base import Message
7
+ from queue_func.shared_resource.queue import Resource
7
8
  from queue_func.broker.base import BaseBroker as _BaseBroker
8
9
  from queue_func.producer.queue import Producer
9
10
  from queue_func.consumer.queue import Consumer
@@ -15,30 +16,19 @@ if t.TYPE_CHECKING:
15
16
  class Broker(_BaseBroker["Config", Message]):
16
17
  """内存队列通道。队列由这里创建,再交给 Producer 和 Consumer。"""
17
18
 
18
- __slots__ = (
19
- "_queue",
20
- )
21
-
22
19
  def __init__(self, config: "Config"):
23
- self._queue: asyncio.Queue[Message] = asyncio.Queue(maxsize=config.maxsize)
24
- super().__init__(config)
25
-
26
- def _create_producer(self) -> Producer[Message]:
27
-
28
- return Producer(
29
- queue=self._queue,
30
- publish_timeout=self._config.publish_timeout,
31
- serializer=self._serializer,
20
+ resource = Resource(
21
+ serializer_cls=config.serializer_cls,
22
+ maxsize=config.maxsize)
23
+ self._producer = Producer(
24
+ resource=resource,
25
+ publish_timeout=config.publish_timeout,
32
26
  )
33
-
34
- def _create_consumer(self) -> Consumer[Message]:
35
-
36
- return Consumer(
37
- queue=self._queue,
38
- consume_timeout=self._config.consume_timeout,
39
- serializer=self._serializer,
40
- coro=self._config.coro,
41
- enable_consumer=self._config.enable_consumer,
42
- pool_executor_cls=self._config.consumer_pool_executor_cls,
43
- pool_maxsize=self._config.consumer_pool_maxsize,
27
+ self._consumer = Consumer(
28
+ resource=resource,
29
+ coro=config.coro,
30
+ enable_consumer=config.enable_consumer,
31
+ consume_timeout=config.consume_timeout,
32
+ pool_executor_cls=config.consumer_pool_executor_cls,
33
+ pool_maxsize=config.consumer_pool_maxsize,
44
34
  )
@@ -0,0 +1,47 @@
1
+ # coding: utf-8
2
+
3
+ import typing as t
4
+
5
+ from queue_func.broker.base import BaseBroker as _BaseBroker
6
+ from queue_func.consumer.rabbit_aio_pika import Consumer
7
+ from queue_func.producer.rabbit_aio_pika import Producer
8
+ from queue_func.shared_resource.rabbit_aio_pika import Resource
9
+
10
+ if t.TYPE_CHECKING:
11
+ from queue_func.config.rabbit_aio_pika 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
+ enable_consumer=config.enable_consumer,
44
+ consume_timeout=config.consume_timeout,
45
+ pool_executor_cls=config.consumer_pool_executor_cls,
46
+ pool_maxsize=config.consumer_pool_maxsize,
47
+ )
@@ -11,18 +11,18 @@ from queue_func.pool_executor.base import BasePoolExecutor
11
11
  from queue_func.pool_executor.general import PoolExecutor
12
12
 
13
13
 
14
- @pyd_dataclass(frozen=True, config=ExtraIgnoreConfig)
14
+ @pyd_dataclass(frozen=True, kw_only=True, config=ExtraIgnoreConfig)
15
15
  class Config:
16
16
 
17
- # consumer
17
+ ## shared resource
18
+ serializer_cls: type[BaseSerializer] = Serializer
19
+
20
+ ## consumer
18
21
  coro: t.Callable[[...], t.Coroutine]
19
22
  enable_consumer: bool = True
20
23
  consume_timeout: float = 2.0
21
24
  consumer_pool_executor_cls: type[BasePoolExecutor] = PoolExecutor
22
25
  consumer_pool_maxsize: int = 200
23
26
 
24
- # broker
25
- serializer_cls: type[BaseSerializer] = Serializer
26
-
27
- # producer
27
+ ## producer
28
28
  publish_timeout: float | None = None
@@ -12,6 +12,6 @@ from queue_func.serializer.raw import Serializer
12
12
  @pyd_dataclass(frozen=True, config=ExtraIgnoreConfig)
13
13
  class Config(_Config):
14
14
 
15
- # broker
15
+ ## shared resource
16
16
  maxsize: int = 200
17
17
  serializer_cls: type[BaseSerializer] = Serializer
@@ -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 queue_func.config.utils import ExtraIgnoreConfig
8
+ from queue_func.config.general import Config as _Config
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
@@ -7,36 +7,43 @@ import contextlib
7
7
  import typing as t
8
8
  from functools import wraps
9
9
 
10
- from queue_func.serializer.base import BaseSerializer, Message
10
+ from queue_func.serializer.base import Message
11
+ from queue_func.shared_resource.base import Resource
11
12
  from queue_func.pool_executor.base import BasePoolExecutor
13
+ from queue_func.utils.hook import OnceHooks
12
14
 
13
15
 
14
- class BaseConsumer(t.Generic[Message], metaclass=abc.ABCMeta):
16
+ class BaseConsumer(t.Generic[Message, Resource], metaclass=abc.ABCMeta):
15
17
 
16
18
  __slots__ = (
17
- "_serializer",
19
+ "_resource",
18
20
  "_coro",
19
21
  "_enable_consumer",
20
22
  "_pool_executor",
21
- "_task",
22
- "_stop_flag",
23
+ "_consume_timeout",
24
+ "_startup",
25
+ "_shutdown",
26
+ "_stopped",
23
27
  "_logger",
24
28
  )
25
29
 
26
30
  def __init__(
27
31
  self,
28
- serializer: type[BaseSerializer[Message]],
32
+ resource: Resource,
29
33
  coro: t.Callable[..., t.Coroutine],
30
34
  enable_consumer: bool,
35
+ consume_timeout: float,
31
36
  pool_executor_cls: type[BasePoolExecutor],
32
37
  pool_maxsize: int):
33
38
 
34
- self._serializer = serializer
39
+ self._resource = resource
35
40
  self._coro = coro
36
41
  self._enable_consumer = enable_consumer
37
42
  self._pool_executor = self._init_pool_executor(pool_executor_cls, pool_maxsize)
38
- self._task: asyncio.Task | None = None
39
- self._stop_flag = False
43
+ self._consume_timeout = consume_timeout
44
+ self._stopped = False
45
+ self._startup = OnceHooks()
46
+ self._shutdown = OnceHooks()
40
47
  self._logger = logging.getLogger(__name__)
41
48
 
42
49
  def _init_pool_executor(
@@ -50,15 +57,15 @@ class BaseConsumer(t.Generic[Message], metaclass=abc.ABCMeta):
50
57
 
51
58
  def _run_until_stopped(
52
59
  self,
53
- coro: t.Callable[..., t.Awaitable],
54
- delay: float = 0.02) -> t.Callable[..., t.Coroutine]:
60
+ fn: t.Callable[..., t.Awaitable],
61
+ delay: float = 0.05) -> t.Callable[..., t.Coroutine]:
55
62
 
56
- @wraps(coro)
63
+ @wraps(fn)
57
64
  async def wrapped(*args, **kwargs) -> None:
58
65
 
59
- while not self._stop_flag:
66
+ while not self._stopped:
60
67
  try:
61
- await coro(*args, **kwargs)
68
+ await fn(*args, **kwargs)
62
69
  except Exception as e:
63
70
  self._logger.exception(e)
64
71
  await asyncio.sleep(delay)
@@ -73,10 +80,10 @@ class BaseConsumer(t.Generic[Message], metaclass=abc.ABCMeta):
73
80
  async def consume(self) -> None:
74
81
 
75
82
  if self._enable_consumer:
76
- self._stop_flag = False
77
- consume_loop = self._run_until_stopped(self._consume)
78
- self._task = asyncio.create_task(consume_loop())
79
- return None
83
+ self._stopped = False
84
+ self._startup.register(self._run_until_stopped(self._consume))
85
+ self._register_shutdown()
86
+ await self._startup.start()
80
87
 
81
88
  @abc.abstractmethod
82
89
  async def _consume(self) -> None: # pragma: no cover
@@ -85,13 +92,22 @@ class BaseConsumer(t.Generic[Message], metaclass=abc.ABCMeta):
85
92
 
86
93
  async def stop(self) -> None:
87
94
 
88
- self._stop_flag = True
95
+ await self._shutdown.start(prepare=self._mark_stopped)
96
+
97
+ def _mark_stopped(self) -> None:
98
+
99
+ self._stopped = True
89
100
 
90
101
  async def join(self) -> None:
91
102
 
92
- if self._task is not None:
93
- with contextlib.suppress(asyncio.CancelledError):
94
- await self._task
103
+ await self._shutdown.wait()
104
+
105
+ async def _join_pool_executor(self):
95
106
 
96
107
  if self._pool_executor is not None:
97
108
  await self._pool_executor.join()
109
+
110
+ def _register_shutdown(self) -> None:
111
+
112
+ self._shutdown.register(self._startup.wait)
113
+ self._shutdown.register(self._join_pool_executor)
@@ -3,53 +3,30 @@
3
3
  import asyncio
4
4
  import typing as t
5
5
 
6
- from queue_func.serializer.base import BaseSerializer, Message
6
+ from queue_func.serializer.base import Message
7
+ from queue_func.shared_resource.queue import Resource
7
8
  from queue_func.pool_executor.base import BasePoolExecutor
8
9
  from queue_func.consumer.base import BaseConsumer as _BaseConsumer
9
10
 
10
11
 
11
- class Consumer(_BaseConsumer[Message]):
12
-
13
- __slots__ = (
14
- "_queue",
15
- "_consume_timeout",
16
- )
17
-
18
- def __init__(
19
- self,
20
- queue: asyncio.Queue[Message],
21
- consume_timeout: float,
22
- serializer: type[BaseSerializer[Message]],
23
- coro: t.Callable[..., t.Coroutine],
24
- enable_consumer: bool,
25
- pool_executor_cls: type[BasePoolExecutor],
26
- pool_maxsize: int):
27
-
28
- super().__init__(
29
- serializer,
30
- coro,
31
- enable_consumer,
32
- pool_executor_cls,
33
- pool_maxsize,
34
- )
35
- self._queue = queue
36
- self._consume_timeout = consume_timeout
12
+ class Consumer(_BaseConsumer[Message, Resource]):
37
13
 
38
14
  async def _consume(self) -> None:
39
15
 
40
- while (not self._stop_flag) or (not self._queue.empty()):
16
+ while (not self._stopped) or (not self._resource.queue.empty()):
41
17
  try:
42
- packed: Message = await asyncio.wait_for(
43
- self._queue.get(),
18
+ message: Message = await asyncio.wait_for(
19
+ self._resource.queue.get(),
44
20
  timeout=self._consume_timeout,
45
21
  )
46
22
  except asyncio.TimeoutError:
47
23
  continue
48
- coro_args, coro_kwargs = self._serializer.unpack(packed)
24
+ coro_args, coro_kwargs = self._resource.serializer.unpack(message)
49
25
  await self._dispatch(coro_args, coro_kwargs)
50
- self._queue.task_done()
26
+ self._resource.queue.task_done()
51
27
 
52
- async def join(self) -> None:
28
+ def _register_shutdown(self) -> None:
53
29
 
54
- await super().join()
55
- await self._queue.join()
30
+ self._shutdown.register(self._startup.wait)
31
+ self._shutdown.register(self._resource.queue.join)
32
+ self._shutdown.register(self._join_pool_executor)
@@ -0,0 +1,99 @@
1
+ # coding: utf-8
2
+
3
+ import asyncio
4
+ import typing as t
5
+
6
+ from queue_func.consumer.base import BaseConsumer as _BaseConsumer
7
+ from queue_func.pool_executor.base import BasePoolExecutor
8
+ from queue_func.shared_resource.rabbit_aio_pika import Resource
9
+
10
+ if t.TYPE_CHECKING:
11
+ from aio_pika.abc import AbstractIncomingMessage, AbstractQueue
12
+
13
+
14
+ class Consumer(_BaseConsumer[bytes, Resource]):
15
+
16
+ __slots__ = (
17
+ "_prefetch_count",
18
+ "_prefetch_size",
19
+ "_global_qos",
20
+ "_channel",
21
+ "_queue",
22
+ "_iterator",
23
+ "_setup_lock",
24
+ )
25
+
26
+ def __init__(
27
+ self,
28
+ resource: Resource,
29
+ prefetch_count: int,
30
+ prefetch_size: int,
31
+ global_qos: bool,
32
+ coro: t.Callable[..., t.Coroutine],
33
+ enable_consumer: bool,
34
+ consume_timeout: float,
35
+ pool_executor_cls: type[BasePoolExecutor],
36
+ pool_maxsize: int):
37
+
38
+ super().__init__(
39
+ resource=resource,
40
+ coro=coro,
41
+ enable_consumer=enable_consumer,
42
+ consume_timeout=consume_timeout,
43
+ pool_executor_cls=pool_executor_cls,
44
+ pool_maxsize=pool_maxsize,
45
+ )
46
+ self._prefetch_count = prefetch_count
47
+ self._prefetch_size = prefetch_size
48
+ self._global_qos = global_qos
49
+ self._channel: "AbstractChannel | None" = None
50
+ self._queue: "AbstractQueue | None" = None
51
+ self._setup_lock = asyncio.Lock()
52
+
53
+ async def _consume(self) -> None:
54
+
55
+ await self._ensure_queue()
56
+ while not self._stopped:
57
+ async with self._queue.iterator(timeout=self._consume_timeout) as queue_iter:
58
+ try:
59
+ message: "AbstractIncomingMessage"
60
+ async for message in queue_iter:
61
+ body: bytes = message.body
62
+ coro_args, coro_kwargs = self._resource.serializer.unpack(body)
63
+ await self._dispatch(coro_args, coro_kwargs)
64
+ await message.ack()
65
+ except asyncio.TimeoutError:
66
+ continue
67
+
68
+ async def _ensure_queue(self) -> None:
69
+
70
+ if self._queue is not None:
71
+ return None
72
+
73
+ async with self._setup_lock:
74
+ if self._queue is not None:
75
+ return None
76
+ channel = await self._resource.get_channel()
77
+ await channel.set_qos(
78
+ prefetch_count=self._prefetch_count,
79
+ prefetch_size=self._prefetch_size,
80
+ global_=self._global_qos,
81
+ )
82
+ queue = await self._resource.declare_queue(channel)
83
+ self._channel, self._queue = channel, queue
84
+
85
+ async def _release(self) -> None:
86
+
87
+ channel = self._channel
88
+ if channel is None:
89
+ return None
90
+ self._channel = None
91
+ self._queue = None
92
+ await self._resource.close_channel(channel)
93
+ await self._resource.try_close_connection()
94
+
95
+ def _register_shutdown(self) -> None:
96
+
97
+ self._shutdown.register(self._startup.wait)
98
+ self._shutdown.register(self._release)
99
+ self._shutdown.register(self._join_pool_executor)
@@ -11,3 +11,4 @@ __all__ = (
11
11
  class Backend(StrEnum):
12
12
 
13
13
  QUEUE = "queue"
14
+ RABBIT_AIO_PIKA = "rabbit_aio_pika"
@@ -3,7 +3,9 @@
3
3
  """Import builder modules so they register with :func:`register_backend`."""
4
4
 
5
5
  from queue_func.factory.builders import queue as queue
6
+ from queue_func.factory.builders import rabbit_aio_pika as rabbit_aio_pika
6
7
 
7
8
  __all__ = (
8
9
  "queue",
10
+ "rabbit_aio_pika",
9
11
  )
@@ -0,0 +1,26 @@
1
+ # coding: utf-8
2
+
3
+ import typing as t
4
+
5
+ from queue_func.factory.backend import Backend
6
+ from queue_func.factory.registry import register_backend
7
+
8
+ if t.TYPE_CHECKING:
9
+ from queue_func.broker.base 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 queue_func.broker.rabbit_aio_pika import Broker
19
+ from queue_func.config.rabbit_aio_pika 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))
@@ -2,6 +2,7 @@
2
2
 
3
3
  import typing as t
4
4
 
5
+ from queue_func.config.rabbit_aio_pika import DeliveryMode, ExchangeName
5
6
  from queue_func.factory import builders as _builders # noqa: F401 — register backends
6
7
  from queue_func.factory.backend import Backend
7
8
  from queue_func.factory.registry import build_broker
@@ -84,6 +85,35 @@ def queue(
84
85
  ...
85
86
 
86
87
 
88
+ @t.overload
89
+ def queue(
90
+ backend: t.Literal[Backend.RABBIT_AIO_PIKA],
91
+ *,
92
+ url: str,
93
+ queue_name: str,
94
+ enable_declare_queue: bool = ...,
95
+ queue_durable: bool = ...,
96
+ queue_exclusive: bool = ...,
97
+ queue_passive: bool = ...,
98
+ queue_auto_delete: bool = ...,
99
+ queue_arguments: dict[str, t.Any] | None = ...,
100
+ serializer_cls: type["BaseSerializer"] = ...,
101
+ enable_consumer: bool = ...,
102
+ consume_timeout: float = ...,
103
+ consumer_pool_executor_cls: type["BasePoolExecutor"] = ...,
104
+ consumer_pool_maxsize: int = ...,
105
+ prefetch_count: int = ...,
106
+ prefetch_size: int = ...,
107
+ global_qos: bool = ...,
108
+ exchange_name: ExchangeName | str = ...,
109
+ routing_key: str = ...,
110
+ delivery_mode: DeliveryMode = ...,
111
+ publish_timeout: float | None = ...,
112
+ ) -> t.Callable[[t.Callable[_P, t.Coroutine[t.Any, t.Any, _R]]], QueueFunc[_P, _R]]:
113
+
114
+ ...
115
+
116
+
87
117
  def queue(
88
118
  backend: Backend,
89
119
  **config_kwargs: t.Any) -> t.Callable[[t.Callable[_P, t.Coroutine[t.Any, t.Any, _R]]], QueueFunc[_P, _R]]:
@@ -3,23 +3,33 @@
3
3
  import abc
4
4
  import typing as t
5
5
 
6
- from queue_func.serializer.base import BaseSerializer, Message
6
+ from queue_func.shared_resource.base import Resource
7
+ from queue_func.serializer.base import Message
8
+ from queue_func.utils.hook import OnceHooks
9
+ from queue_func.utils.waiter import IdleWaiter
7
10
 
8
11
 
9
- class BaseProducer(t.Generic[Message], metaclass=abc.ABCMeta):
12
+ class BaseProducer(t.Generic[Message, Resource], metaclass=abc.ABCMeta):
10
13
 
11
14
  __slots__ = (
12
- "_serializer",
15
+ "_resource",
13
16
  "_publish_timeout",
17
+ "_stopped",
18
+ "_shutdown",
19
+ "_pub_waiter",
14
20
  )
15
21
 
16
22
  def __init__(
17
23
  self,
18
- serializer: type[BaseSerializer[Message]],
24
+ resource: Resource,
19
25
  publish_timeout: float | None):
20
26
 
21
- self._serializer = serializer
27
+ self._resource = resource
22
28
  self._publish_timeout = publish_timeout
29
+ self._stopped = False
30
+ self._shutdown = OnceHooks()
31
+ self._pub_waiter = IdleWaiter()
32
+ self._register_shutdown()
23
33
 
24
34
  async def publish(
25
35
  self,
@@ -28,17 +38,32 @@ class BaseProducer(t.Generic[Message], metaclass=abc.ABCMeta):
28
38
  *,
29
39
  timeout: float | None = None) -> None:
30
40
 
41
+
42
+ if self._stopped:
43
+ raise RuntimeError("publish called on already stopped")
44
+
31
45
  if timeout is None:
32
46
  timeout = self._publish_timeout
33
- packed = self._serializer.pack(coro_args, coro_kwargs)
34
- await self._publish(packed, timeout=timeout)
47
+ packed = self._resource.serializer.pack(coro_args, coro_kwargs)
48
+ async with self._pub_waiter.hold():
49
+ await self._publish(packed, timeout=timeout)
35
50
 
36
51
  @abc.abstractmethod
37
- async def _publish(self, data: Message, *, timeout: float | None) -> None:
52
+ async def _publish(self, msg: Message, *, timeout: float | None) -> None:
38
53
  pass
39
54
 
40
55
  async def stop(self) -> None:
41
- pass
56
+
57
+ await self._shutdown.start(prepare=self._mark_stopped)
58
+
59
+ def _mark_stopped(self) -> None:
60
+
61
+ self._stopped = True
42
62
 
43
63
  async def join(self) -> None:
44
- pass
64
+
65
+ await self._shutdown.wait()
66
+
67
+ def _register_shutdown(self):
68
+
69
+ self._shutdown.register(self._pub_waiter.wait)