async-task-kit 0.1.2__tar.gz → 0.1.14__tar.gz

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.
Files changed (22) hide show
  1. {async_task_kit-0.1.2 → async_task_kit-0.1.14}/PKG-INFO +49 -1
  2. {async_task_kit-0.1.2 → async_task_kit-0.1.14}/README.md +48 -0
  3. {async_task_kit-0.1.2 → async_task_kit-0.1.14}/async_task_kit/consumer/coroutine.py +6 -3
  4. {async_task_kit-0.1.2 → async_task_kit-0.1.14}/async_task_kit/consumer/process.py +1 -1
  5. {async_task_kit-0.1.2 → async_task_kit-0.1.14}/async_task_kit/consumer/thread.py +1 -1
  6. {async_task_kit-0.1.2 → async_task_kit-0.1.14}/async_task_kit/core/rabbitmq.py +83 -28
  7. {async_task_kit-0.1.2 → async_task_kit-0.1.14}/async_task_kit.egg-info/PKG-INFO +49 -1
  8. {async_task_kit-0.1.2 → async_task_kit-0.1.14}/pyproject.toml +1 -1
  9. {async_task_kit-0.1.2 → async_task_kit-0.1.14}/LICENSE +0 -0
  10. {async_task_kit-0.1.2 → async_task_kit-0.1.14}/async_task_kit/__init__.py +0 -0
  11. {async_task_kit-0.1.2 → async_task_kit-0.1.14}/async_task_kit/consumer/__init__.py +0 -0
  12. {async_task_kit-0.1.2 → async_task_kit-0.1.14}/async_task_kit/consumer/base.py +0 -0
  13. {async_task_kit-0.1.2 → async_task_kit-0.1.14}/async_task_kit/core/__init__.py +0 -0
  14. {async_task_kit-0.1.2 → async_task_kit-0.1.14}/async_task_kit/core/processor.py +0 -0
  15. {async_task_kit-0.1.2 → async_task_kit-0.1.14}/async_task_kit/utils/__init__.py +0 -0
  16. {async_task_kit-0.1.2 → async_task_kit-0.1.14}/async_task_kit/utils/env_loader.py +0 -0
  17. {async_task_kit-0.1.2 → async_task_kit-0.1.14}/async_task_kit/utils/logger.py +0 -0
  18. {async_task_kit-0.1.2 → async_task_kit-0.1.14}/async_task_kit.egg-info/SOURCES.txt +0 -0
  19. {async_task_kit-0.1.2 → async_task_kit-0.1.14}/async_task_kit.egg-info/dependency_links.txt +0 -0
  20. {async_task_kit-0.1.2 → async_task_kit-0.1.14}/async_task_kit.egg-info/requires.txt +0 -0
  21. {async_task_kit-0.1.2 → async_task_kit-0.1.14}/async_task_kit.egg-info/top_level.txt +0 -0
  22. {async_task_kit-0.1.2 → async_task_kit-0.1.14}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: async-task-kit
3
- Version: 0.1.2
3
+ Version: 0.1.14
4
4
  Summary: A powerful async task processing kit based on RabbitMQ with Coroutine, Thread, and Process support.
5
5
  Author-email: realwrtoff <realwrtoff@gmail.com>
6
6
  Classifier: Programming Language :: Python :: 3
@@ -175,6 +175,54 @@ if __name__ == "__main__":
175
175
  asyncio.run(publish())
176
176
  ```
177
177
 
178
+ ### 5. RabbitMQ Client API
179
+
180
+ `RabbitMQ` 提供连接池、`push` / `pop`、延迟重试队列,以及 `queue_length` 查询队列深度。
181
+
182
+ #### `queue_length`
183
+
184
+ ```python
185
+ from async_task_kit import RabbitMQ
186
+
187
+ rmq = RabbitMQ("amqp://guest:guest@localhost/")
188
+ await rmq.init()
189
+
190
+ length = await rmq.queue_length("my_demo_queue")
191
+ if length == RabbitMQ.QUEUE_LENGTH_UNAVAILABLE:
192
+ # -1:连不上或队列不存在,稍后重试
193
+ ...
194
+ elif length == 0:
195
+ # 队列存在且为空
196
+ ...
197
+ else:
198
+ # 当前消息数
199
+ ...
200
+
201
+ await rmq.close()
202
+ ```
203
+
204
+ | 返回值 | 含义 |
205
+ |--------|------|
206
+ | `>= 0` | 队列真实消息数(`0` = 空队列) |
207
+ | `-1` (`QUEUE_LENGTH_UNAVAILABLE`) | 连接失败、队列不存在或其它异常,应重试 |
208
+
209
+ 实现要点:
210
+
211
+ - 使用 **`passive=True`** 声明:只查询已存在的队列,**不会**自动创建空队列。
212
+ - 断连时会关闭旧连接池并自动重建(与 `push` / `pop` 一致)。
213
+
214
+ #### `pop` 的 `durable` 参数
215
+
216
+ `pop` 在取消息前会 `declare_queue`。`push` 默认以 **`durable=True`** 创建队列(重启后队列仍在),因此 `pop` 默认同样传 `durable=True`,保证声明参数与已有队列一致,避免 RabbitMQ 返回 `PRECONDITION_FAILED`。
217
+
218
+ 若你的队列是非持久化的,调用时需显式传入:
219
+
220
+ ```python
221
+ await rmq.pop("my_queue", durable=False)
222
+ ```
223
+
224
+ `queue_length` 使用 passive 模式,无需传 `durable`。
225
+
178
226
  ## License
179
227
 
180
228
  MIT
@@ -159,6 +159,54 @@ if __name__ == "__main__":
159
159
  asyncio.run(publish())
160
160
  ```
161
161
 
162
+ ### 5. RabbitMQ Client API
163
+
164
+ `RabbitMQ` 提供连接池、`push` / `pop`、延迟重试队列,以及 `queue_length` 查询队列深度。
165
+
166
+ #### `queue_length`
167
+
168
+ ```python
169
+ from async_task_kit import RabbitMQ
170
+
171
+ rmq = RabbitMQ("amqp://guest:guest@localhost/")
172
+ await rmq.init()
173
+
174
+ length = await rmq.queue_length("my_demo_queue")
175
+ if length == RabbitMQ.QUEUE_LENGTH_UNAVAILABLE:
176
+ # -1:连不上或队列不存在,稍后重试
177
+ ...
178
+ elif length == 0:
179
+ # 队列存在且为空
180
+ ...
181
+ else:
182
+ # 当前消息数
183
+ ...
184
+
185
+ await rmq.close()
186
+ ```
187
+
188
+ | 返回值 | 含义 |
189
+ |--------|------|
190
+ | `>= 0` | 队列真实消息数(`0` = 空队列) |
191
+ | `-1` (`QUEUE_LENGTH_UNAVAILABLE`) | 连接失败、队列不存在或其它异常,应重试 |
192
+
193
+ 实现要点:
194
+
195
+ - 使用 **`passive=True`** 声明:只查询已存在的队列,**不会**自动创建空队列。
196
+ - 断连时会关闭旧连接池并自动重建(与 `push` / `pop` 一致)。
197
+
198
+ #### `pop` 的 `durable` 参数
199
+
200
+ `pop` 在取消息前会 `declare_queue`。`push` 默认以 **`durable=True`** 创建队列(重启后队列仍在),因此 `pop` 默认同样传 `durable=True`,保证声明参数与已有队列一致,避免 RabbitMQ 返回 `PRECONDITION_FAILED`。
201
+
202
+ 若你的队列是非持久化的,调用时需显式传入:
203
+
204
+ ```python
205
+ await rmq.pop("my_queue", durable=False)
206
+ ```
207
+
208
+ `queue_length` 使用 passive 模式,无需传 `durable`。
209
+
162
210
  ## License
163
211
 
164
212
  MIT
@@ -18,6 +18,7 @@ class CoroutineConsumer(BaseConsumer):
18
18
  concurrency: int = 1,
19
19
  max_retry: int = 3,
20
20
  retry_delay: int = 30,
21
+ rmq: RabbitMQ | None = None,
21
22
  ):
22
23
  self.amqp_url = amqp_url
23
24
  self.queue_name = queue_name
@@ -26,7 +27,8 @@ class CoroutineConsumer(BaseConsumer):
26
27
  self.max_retry = max_retry
27
28
  self.retry_delay = retry_delay
28
29
 
29
- self._rmq = RabbitMQ(amqp_url)
30
+ self._owns_rmq = rmq is None
31
+ self._rmq = rmq or RabbitMQ(amqp_url)
30
32
  self._stop_event = asyncio.Event()
31
33
 
32
34
  async def start(self):
@@ -93,5 +95,6 @@ class CoroutineConsumer(BaseConsumer):
93
95
 
94
96
  async def stop(self):
95
97
  self._stop_event.set()
96
- await self._rmq.close()
97
- logger.info(f"[CoroutineConsumer] stopped | queue={self.queue_name}")
98
+ if self._owns_rmq:
99
+ await self._rmq.close()
100
+ logger.info(f"[CoroutineConsumer] stopped | queue={self.queue_name}")
@@ -104,4 +104,4 @@ class ProcessConsumer(BaseConsumer):
104
104
 
105
105
  async def stop(self):
106
106
  self._stop_event.set()
107
- logger.info(f"[ProcessConsumer] stopped | queue={self.queue_name}")
107
+ logger.info(f"[ProcessConsumer] stopped | queue={self.queue_name}")
@@ -104,4 +104,4 @@ class ThreadConsumer(BaseConsumer):
104
104
 
105
105
  async def stop(self):
106
106
  self._stop_event.set()
107
- logger.info(f"[ThreadConsumer] stopped | queue={self.queue_name}")
107
+ logger.info(f"[ThreadConsumer] stopped | queue={self.queue_name}")
@@ -12,6 +12,7 @@ from aio_pika.exceptions import (
12
12
  ChannelClosed,
13
13
  AMQPError
14
14
  )
15
+ from aiormq.exceptions import ChannelInvalidStateError
15
16
  from aio_pika.pool import Pool
16
17
 
17
18
  logger = logging.getLogger(__name__)
@@ -29,7 +30,11 @@ class BasicQueue(ABC):
29
30
  @abstractmethod
30
31
  async def pop(self, queue_name: str,** kwargs: Any) -> Any:
31
32
  raise NotImplementedError
32
-
33
+
34
+ @abstractmethod
35
+ async def queue_length(self, queue_name: str) -> int:
36
+ raise NotImplementedError
37
+
33
38
  @abstractmethod
34
39
  async def ack(self, message: Any) -> None:
35
40
  raise NotImplementedError
@@ -50,6 +55,7 @@ class RabbitMQ(BasicQueue):
50
55
  DEFAULT_DEAD_LETTER_DAYS = 7
51
56
  DEFAULT_DELAY_SECONDS = 30
52
57
  DEFAULT_POP_TIMEOUT = 1.0
58
+ QUEUE_LENGTH_UNAVAILABLE = -1
53
59
 
54
60
  def __init__(
55
61
  self,
@@ -58,6 +64,7 @@ class RabbitMQ(BasicQueue):
58
64
  channel_size: int = DEFAULT_CHANNEL_SIZE,
59
65
  heartbeat: int = 60,
60
66
  max_retry_count: int = DEFAULT_MAX_RETRY_COUNT,
67
+
61
68
  ):
62
69
  self.rabbit_url = rabbit_url
63
70
  self.pool_size = pool_size
@@ -92,6 +99,23 @@ class RabbitMQ(BasicQueue):
92
99
  logger.error(f"[RabbitMQ] 初始化失败: {e}", exc_info=True)
93
100
  raise
94
101
 
102
+ async def _invalidate_pools(self) -> None:
103
+ """断连或 close 时关闭 pool,下次 init() 会重建。"""
104
+ async with self._init_lock:
105
+ self._ready.clear()
106
+ channel_pool = self._channel_pool
107
+ connection_pool = self._connection_pool
108
+ self._channel_pool = None
109
+ self._connection_pool = None
110
+
111
+ for pool, label in ((channel_pool, "channel"), (connection_pool, "connection")):
112
+ if pool is None:
113
+ continue
114
+ try:
115
+ await pool.close()
116
+ except Exception as e:
117
+ logger.warning(f"[RabbitMQ] 关闭旧 {label} pool 失败: {e}")
118
+
95
119
  async def _get_connection(self) -> aio_pika.abc.AbstractRobustConnection:
96
120
  try:
97
121
  return await aio_pika.connect_robust(
@@ -118,41 +142,80 @@ class RabbitMQ(BasicQueue):
118
142
  count = int(kwargs.get("count", 0))
119
143
  durable = kwargs.get("durable", True)
120
144
 
121
- async with self._channel_pool.acquire() as channel:
122
- if 0 < count < self.max_retry_count:
123
- rk = await self._setup_delay_queue(channel, queue_name,** kwargs)
124
- elif count >= self.max_retry_count:
125
- rk = await self._setup_dead_letter_queue(channel, queue_name, **kwargs)
126
- else:
127
- rk = await self._setup_normal_queue(channel, queue_name, durable)
128
-
129
- msg = self._create_message(data, count)
130
- await channel.default_exchange.publish(msg, routing_key=rk)
131
- logger.debug(f"[PUSH] queue={queue_name} count={count}")
132
- return msg
145
+ try:
146
+ async with self._channel_pool.acquire() as channel:
147
+ if 0 < count < self.max_retry_count:
148
+ rk = await self._setup_delay_queue(channel, queue_name,** kwargs)
149
+ elif count >= self.max_retry_count:
150
+ rk = await self._setup_dead_letter_queue(channel, queue_name, **kwargs)
151
+ else:
152
+ rk = await self._setup_normal_queue(channel, queue_name, durable)
153
+
154
+ msg = self._create_message(data, count)
155
+ await channel.default_exchange.publish(msg, routing_key=rk)
156
+ logger.debug(f"[PUSH] queue={queue_name} count={count}")
157
+ return msg
158
+ except (ConnectionClosed, ChannelClosed):
159
+ logger.warning("[PUSH] 连接断开,准备重连")
160
+ await self._invalidate_pools()
161
+ return None
162
+ except ChannelInvalidStateError:
163
+ logger.warning("[PUSH] 信道 RPC 异常,准备重建 pool")
164
+ await self._invalidate_pools()
165
+ return None
166
+ except Exception as e:
167
+ logger.error(f"[PUSH] 异常 queue={queue_name}: {e}", exc_info=True)
168
+ return None
133
169
 
134
170
  async def pop(self, queue_name: str,** kwargs: Any) -> Optional[aio_pika.abc.AbstractIncomingMessage]:
135
171
  if self._closed:
136
172
  return None
137
173
 
138
174
  await self.init()
175
+ # durable 须与 push 声明队列时一致(默认 True),否则 re-declare 可能 PRECONDITION_FAILED
139
176
  durable = kwargs.get("durable", True)
140
177
 
141
178
  try:
142
179
  async with self._channel_pool.acquire() as channel:
143
180
  queue = await channel.declare_queue(queue_name, durable=durable)
144
- return await asyncio.wait_for(
145
- queue.get(), timeout=self.DEFAULT_POP_TIMEOUT
181
+ # 使用 aio_pika 自带 timeout,勿用 asyncio.wait_for 取消 get(会关闭 channel)
182
+ return await queue.get(
183
+ timeout=self.DEFAULT_POP_TIMEOUT,
184
+ fail=False,
146
185
  )
147
186
  except (QueueEmpty, asyncio.TimeoutError):
148
187
  return None
149
- except (ConnectionClosed, ChannelClosed):
150
- logger.warning("[POP] 连接断开,准备重连")
151
- self._ready.clear()
188
+ except (ConnectionClosed, ChannelClosed, ChannelInvalidStateError):
189
+ logger.warning("[POP] 连接/信道异常,准备重建 pool")
190
+ await self._invalidate_pools()
152
191
  return None
153
192
  except Exception as e:
154
193
  logger.error(f"[POP] 异常 queue={queue_name}: {e}", exc_info=True)
155
194
  return None
195
+
196
+ async def queue_length(self, queue_name: str) -> int:
197
+ """Return queue message count, or QUEUE_LENGTH_UNAVAILABLE (-1) on failure."""
198
+ if self._closed:
199
+ return self.QUEUE_LENGTH_UNAVAILABLE
200
+
201
+ try:
202
+ await self.init()
203
+ async with self._channel_pool.acquire() as channel:
204
+ # passive:只查询已存在队列,不创建;队列不存在则 ChannelClosed → -1
205
+ queue = await channel.declare_queue(queue_name, passive=True)
206
+ count = queue.declaration_result.message_count
207
+ return count if count is not None else 0
208
+ except (ConnectionClosed, ChannelClosed):
209
+ logger.warning("[QUEUE_LENGTH] 连接断开,准备重连")
210
+ await self._invalidate_pools()
211
+ return self.QUEUE_LENGTH_UNAVAILABLE
212
+ except ChannelInvalidStateError:
213
+ logger.warning("[QUEUE_LENGTH] 信道 RPC 异常,准备重建 pool")
214
+ await self._invalidate_pools()
215
+ return self.QUEUE_LENGTH_UNAVAILABLE
216
+ except Exception as e:
217
+ logger.error(f"[QUEUE_LENGTH] 异常 queue={queue_name}: {e}", exc_info=True)
218
+ return self.QUEUE_LENGTH_UNAVAILABLE
156
219
 
157
220
  async def ack(self, message: aio_pika.abc.AbstractIncomingMessage) -> None:
158
221
  try:
@@ -217,13 +280,5 @@ class RabbitMQ(BasicQueue):
217
280
  if self._closed:
218
281
  return
219
282
  self._closed = True
220
- self._ready.clear()
221
-
222
- try:
223
- if self._channel_pool:
224
- await self._channel_pool.close()
225
- if self._connection_pool:
226
- await self._connection_pool.close()
227
- logger.info("[RabbitMQ] 已关闭")
228
- except Exception as e:
229
- logger.error(f"[关闭失败] {e}")
283
+ await self._invalidate_pools()
284
+ logger.info("[RabbitMQ] 已关闭")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: async-task-kit
3
- Version: 0.1.2
3
+ Version: 0.1.14
4
4
  Summary: A powerful async task processing kit based on RabbitMQ with Coroutine, Thread, and Process support.
5
5
  Author-email: realwrtoff <realwrtoff@gmail.com>
6
6
  Classifier: Programming Language :: Python :: 3
@@ -175,6 +175,54 @@ if __name__ == "__main__":
175
175
  asyncio.run(publish())
176
176
  ```
177
177
 
178
+ ### 5. RabbitMQ Client API
179
+
180
+ `RabbitMQ` 提供连接池、`push` / `pop`、延迟重试队列,以及 `queue_length` 查询队列深度。
181
+
182
+ #### `queue_length`
183
+
184
+ ```python
185
+ from async_task_kit import RabbitMQ
186
+
187
+ rmq = RabbitMQ("amqp://guest:guest@localhost/")
188
+ await rmq.init()
189
+
190
+ length = await rmq.queue_length("my_demo_queue")
191
+ if length == RabbitMQ.QUEUE_LENGTH_UNAVAILABLE:
192
+ # -1:连不上或队列不存在,稍后重试
193
+ ...
194
+ elif length == 0:
195
+ # 队列存在且为空
196
+ ...
197
+ else:
198
+ # 当前消息数
199
+ ...
200
+
201
+ await rmq.close()
202
+ ```
203
+
204
+ | 返回值 | 含义 |
205
+ |--------|------|
206
+ | `>= 0` | 队列真实消息数(`0` = 空队列) |
207
+ | `-1` (`QUEUE_LENGTH_UNAVAILABLE`) | 连接失败、队列不存在或其它异常,应重试 |
208
+
209
+ 实现要点:
210
+
211
+ - 使用 **`passive=True`** 声明:只查询已存在的队列,**不会**自动创建空队列。
212
+ - 断连时会关闭旧连接池并自动重建(与 `push` / `pop` 一致)。
213
+
214
+ #### `pop` 的 `durable` 参数
215
+
216
+ `pop` 在取消息前会 `declare_queue`。`push` 默认以 **`durable=True`** 创建队列(重启后队列仍在),因此 `pop` 默认同样传 `durable=True`,保证声明参数与已有队列一致,避免 RabbitMQ 返回 `PRECONDITION_FAILED`。
217
+
218
+ 若你的队列是非持久化的,调用时需显式传入:
219
+
220
+ ```python
221
+ await rmq.pop("my_queue", durable=False)
222
+ ```
223
+
224
+ `queue_length` 使用 passive 模式,无需传 `durable`。
225
+
178
226
  ## License
179
227
 
180
228
  MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "async-task-kit"
7
- version = "0.1.2"
7
+ version = "0.1.14"
8
8
  description = "A powerful async task processing kit based on RabbitMQ with Coroutine, Thread, and Process support."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
File without changes