ergon-framework-python 0.1.6__tar.gz → 0.1.8__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.
- {ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/PKG-INFO +1 -1
- {ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/pyproject.toml +1 -1
- {ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/__init__.py +1 -1
- {ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/connector/rabbitmq/async_service.py +163 -65
- {ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/task/runner.py +39 -16
- {ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon_framework_python.egg-info/PKG-INFO +1 -1
- {ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/tests/test_smoke.py +1 -1
- {ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/LICENSE +0 -0
- {ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/README.md +0 -0
- {ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/setup.cfg +0 -0
- {ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/bootstrap/src/__project__/__init__.py +0 -0
- {ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/bootstrap/src/__project__/_observability/docker-compose.telemetry.yml +0 -0
- {ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/bootstrap/src/__project__/_observability/grafana.yaml +0 -0
- {ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/bootstrap/src/__project__/_observability/loki.yaml +0 -0
- {ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/bootstrap/src/__project__/_observability/otel-collector-config.yaml +0 -0
- {ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/bootstrap/src/__project__/_observability/prometheus.yaml +0 -0
- {ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/bootstrap/src/__project__/_observability/tempo.yaml +0 -0
- {ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/bootstrap/src/__project__/connectors/__init__.py +0 -0
- {ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/bootstrap/src/__project__/main.py +0 -0
- {ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/bootstrap/src/__project__/tasks/__init__.py +0 -0
- {ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/bootstrap/src/__project__/tasks/constants.py +0 -0
- {ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/bootstrap/src/__project__/tasks/example_task/__init__.py +0 -0
- {ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/bootstrap/src/__project__/tasks/example_task/config.py +0 -0
- {ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/bootstrap/src/__project__/tasks/example_task/exceptions.py +0 -0
- {ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/bootstrap/src/__project__/tasks/example_task/helpers.py +0 -0
- {ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/bootstrap/src/__project__/tasks/example_task/schemas.py +0 -0
- {ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/bootstrap/src/__project__/tasks/example_task/task.py +0 -0
- {ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/bootstrap/src/__project__/tasks/exceptions.py +0 -0
- {ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/bootstrap/src/__project__/tasks/helpers.py +0 -0
- {ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/bootstrap/src/__project__/tasks/schemas.py +0 -0
- {ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/bootstrap/src/__project__/tasks/settings.py +0 -0
- {ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/cli.py +0 -0
- {ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/connector/__init__.py +0 -0
- {ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/connector/connector.py +0 -0
- {ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/connector/ergon_platform/__init__.py +0 -0
- {ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/connector/ergon_platform/_client.py +0 -0
- {ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/connector/ergon_platform/_operations.py +0 -0
- {ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/connector/ergon_platform/async_connector.py +0 -0
- {ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/connector/ergon_platform/connector.py +0 -0
- {ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/connector/ergon_platform/models.py +0 -0
- {ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/connector/ergon_platform/utils.py +0 -0
- {ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/connector/excel/__init__.py +0 -0
- {ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/connector/excel/connector.py +0 -0
- {ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/connector/excel/models.py +0 -0
- {ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/connector/excel/service.py +0 -0
- {ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/connector/nylas/__init__.py +0 -0
- {ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/connector/nylas/async_auth_service.py +0 -0
- {ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/connector/nylas/async_connector.py +0 -0
- {ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/connector/nylas/async_service.py +0 -0
- {ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/connector/nylas/auth_service.py +0 -0
- {ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/connector/nylas/connector.py +0 -0
- {ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/connector/nylas/models.py +0 -0
- {ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/connector/nylas/service.py +0 -0
- {ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/connector/nylas/utils.py +0 -0
- {ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/connector/pipefy/__init__.py +0 -0
- {ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/connector/pipefy/async_connector.py +0 -0
- {ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/connector/pipefy/async_service.py +0 -0
- {ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/connector/pipefy/connector.py +0 -0
- {ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/connector/pipefy/models.py +0 -0
- {ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/connector/pipefy/service.py +0 -0
- {ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/connector/pipefy/version.py +0 -0
- {ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/connector/postgres/__init__.py +0 -0
- {ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/connector/postgres/async_connector.py +0 -0
- {ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/connector/postgres/async_service.py +0 -0
- {ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/connector/postgres/models.py +0 -0
- {ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/connector/rabbitmq/__init__.py +0 -0
- {ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/connector/rabbitmq/async_connector.py +0 -0
- {ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/connector/rabbitmq/connector.py +0 -0
- {ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/connector/rabbitmq/helper.py +0 -0
- {ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/connector/rabbitmq/models.py +0 -0
- {ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/connector/rabbitmq/service.py +0 -0
- {ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/connector/sqs/__init__.py +0 -0
- {ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/connector/sqs/async_connector.py +0 -0
- {ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/connector/sqs/async_service.py +0 -0
- {ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/connector/sqs/connector.py +0 -0
- {ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/connector/sqs/models.py +0 -0
- {ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/connector/sqs/service.py +0 -0
- {ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/connector/transaction.py +0 -0
- {ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/py.typed +0 -0
- {ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/service/__init__.py +0 -0
- {ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/service/service.py +0 -0
- {ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/task/__init__.py +0 -0
- {ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/task/base.py +0 -0
- {ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/task/exceptions.py +0 -0
- {ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/task/helpers.py +0 -0
- {ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/task/liveness.py +0 -0
- {ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/task/manager.py +0 -0
- {ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/task/mixins/__init__.py +0 -0
- {ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/task/mixins/consumer.py +0 -0
- {ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/task/mixins/metrics.py +0 -0
- {ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/task/mixins/producer.py +0 -0
- {ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/task/policies.py +0 -0
- {ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/task/utils.py +0 -0
- {ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/telemetry/__init__.py +0 -0
- {ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/telemetry/_resource.py +0 -0
- {ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/telemetry/logging.py +0 -0
- {ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/telemetry/metrics.py +0 -0
- {ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/telemetry/tracing.py +0 -0
- {ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/utils/__init__.py +0 -0
- {ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/utils/env.py +0 -0
- {ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon_framework_python.egg-info/SOURCES.txt +0 -0
- {ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon_framework_python.egg-info/dependency_links.txt +0 -0
- {ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon_framework_python.egg-info/entry_points.txt +0 -0
- {ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon_framework_python.egg-info/requires.txt +0 -0
- {ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon_framework_python.egg-info/top_level.txt +0 -0
|
@@ -4,7 +4,6 @@ import logging
|
|
|
4
4
|
import ssl as ssl_module
|
|
5
5
|
import time
|
|
6
6
|
from collections import deque
|
|
7
|
-
from contextlib import AsyncExitStack
|
|
8
7
|
from typing import Any, AsyncContextManager, Callable, Dict, List, Optional, cast
|
|
9
8
|
|
|
10
9
|
import aio_pika
|
|
@@ -24,15 +23,36 @@ from .models import AsyncRabbitmqClient, AsyncRabbitmqConsumerConfig, AsyncRabbi
|
|
|
24
23
|
logger = logging.getLogger(__name__)
|
|
25
24
|
|
|
26
25
|
|
|
27
|
-
#
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
-
|
|
26
|
+
# Invalid channel state always means the active transport disappeared. Broader
|
|
27
|
+
# AMQP connection hierarchies also contain permanent authentication/protocol
|
|
28
|
+
# errors, so consume-time recovery uses the classifier below instead.
|
|
29
|
+
_CHANNEL_INVALID_EXCEPTIONS: tuple[type[BaseException], ...] = (
|
|
30
|
+
aio_pika.exceptions.ChannelInvalidStateError,
|
|
31
|
+
aiormq.exceptions.ChannelInvalidStateError,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _is_recoverable_broker_interruption(exc: BaseException) -> bool:
|
|
36
|
+
if isinstance(exc, _CHANNEL_INVALID_EXCEPTIONS):
|
|
37
|
+
return True
|
|
38
|
+
if type(exc) is aiormq.exceptions.AMQPConnectionError:
|
|
39
|
+
return True
|
|
40
|
+
if type(exc) is aiormq.exceptions.ConnectionClosed:
|
|
41
|
+
reply_code = exc.args[0] if exc.args else None
|
|
42
|
+
return reply_code == 320
|
|
43
|
+
if type(exc) is aiormq.exceptions.ConnectionInternalError:
|
|
44
|
+
return True
|
|
45
|
+
return False
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# Settlement can additionally discover a channel-level failure after delivery.
|
|
49
|
+
# Surface those as DeadChannelError so the consumer mixin does not attempt a
|
|
50
|
+
# second settlement on the same unusable channel.
|
|
31
51
|
_DEAD_CHANNEL_EXCEPTIONS: tuple[type[BaseException], ...] = (
|
|
52
|
+
aio_pika.exceptions.AMQPConnectionError,
|
|
53
|
+
*_CHANNEL_INVALID_EXCEPTIONS,
|
|
32
54
|
aio_pika.exceptions.MessageProcessError,
|
|
33
55
|
aio_pika.exceptions.ChannelClosed,
|
|
34
|
-
aio_pika.exceptions.ChannelInvalidStateError,
|
|
35
|
-
aiormq.exceptions.ChannelInvalidStateError,
|
|
36
56
|
)
|
|
37
57
|
|
|
38
58
|
# Same as above plus ``TimeoutError`` (raised by the async timeout context when an
|
|
@@ -135,11 +155,33 @@ class AsyncRabbitMQService:
|
|
|
135
155
|
|
|
136
156
|
connection = self._connection
|
|
137
157
|
self._connection = None
|
|
138
|
-
|
|
139
|
-
|
|
158
|
+
await self._close_connection_safely(connection, reason)
|
|
159
|
+
|
|
160
|
+
async def _close_connection_safely(
|
|
161
|
+
self,
|
|
162
|
+
connection: Optional[AbstractRobustConnection],
|
|
163
|
+
reason: str,
|
|
164
|
+
) -> None:
|
|
165
|
+
"""Stop transport and robust-reconnect ownership within a fixed deadline.
|
|
166
|
+
|
|
167
|
+
``RobustConnection.is_closed`` only describes its current transport. Its
|
|
168
|
+
background reconnection task can still be alive after the broker closes
|
|
169
|
+
that transport, so ``close()`` must always run for an owned connection.
|
|
170
|
+
"""
|
|
171
|
+
if connection is None:
|
|
172
|
+
return
|
|
173
|
+
close_timeout = min(5.0, self.client.connect_timeout)
|
|
174
|
+
try:
|
|
175
|
+
async with timeout_after(close_timeout):
|
|
140
176
|
await connection.close()
|
|
141
|
-
|
|
142
|
-
|
|
177
|
+
except TimeoutError:
|
|
178
|
+
logger.error(
|
|
179
|
+
"Timed out closing RabbitMQ connection after %.1fs (%s)",
|
|
180
|
+
close_timeout,
|
|
181
|
+
reason,
|
|
182
|
+
)
|
|
183
|
+
except Exception as exc: # noqa: BLE001 - best-effort reset
|
|
184
|
+
logger.warning("Error closing RabbitMQ connection (%s): %r", reason, exc)
|
|
143
185
|
|
|
144
186
|
def _invalidate_consume_channel(self, reason: str = "explicit invalidation") -> None:
|
|
145
187
|
"""Drop cached consume channel + queue/exchange handles.
|
|
@@ -193,7 +235,8 @@ class AsyncRabbitMQService:
|
|
|
193
235
|
return
|
|
194
236
|
try:
|
|
195
237
|
if not channel.is_closed:
|
|
196
|
-
|
|
238
|
+
async with timeout_after(min(1.0, self.client.channel_timeout)):
|
|
239
|
+
await channel.close()
|
|
197
240
|
logger.info("Closed dead consume channel (%s)", reason)
|
|
198
241
|
except Exception as exc: # noqa: BLE001 - best-effort teardown
|
|
199
242
|
logger.warning("Error closing consume channel during teardown (%s): %r", reason, exc)
|
|
@@ -365,27 +408,33 @@ class AsyncRabbitMQService:
|
|
|
365
408
|
self._consumer_state = "subscribing"
|
|
366
409
|
poll_completed = False
|
|
367
410
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
arguments=subscription.queue_arguments or None,
|
|
379
|
-
)
|
|
380
|
-
for binding in subscription.bindings:
|
|
381
|
-
exchange = await self.declare_exchange(
|
|
382
|
-
binding.exchange_name,
|
|
383
|
-
binding.exchange_type,
|
|
411
|
+
try:
|
|
412
|
+
# Ensure the consume channel exists with the requested prefetch QoS.
|
|
413
|
+
# The return value is unused here because declarations re-fetch the
|
|
414
|
+
# channel from the cache.
|
|
415
|
+
await self._get_consume_channel(prefetch_count=config.prefetch_count)
|
|
416
|
+
|
|
417
|
+
queues: list[tuple[str, AbstractQueue]] = []
|
|
418
|
+
for subscription in config.resolved_subscriptions():
|
|
419
|
+
queue = await self.declare_queue(
|
|
420
|
+
subscription.queue_name,
|
|
384
421
|
durable=config.durable,
|
|
422
|
+
arguments=subscription.queue_arguments or None,
|
|
385
423
|
)
|
|
386
|
-
for
|
|
387
|
-
await self.
|
|
388
|
-
|
|
424
|
+
for binding in subscription.bindings:
|
|
425
|
+
exchange = await self.declare_exchange(
|
|
426
|
+
binding.exchange_name,
|
|
427
|
+
binding.exchange_type,
|
|
428
|
+
durable=config.durable,
|
|
429
|
+
)
|
|
430
|
+
for key in binding.routing_keys:
|
|
431
|
+
await self.bind_queue(queue, exchange, key)
|
|
432
|
+
queues.append((subscription.queue_name, queue))
|
|
433
|
+
except Exception as exc:
|
|
434
|
+
if not _is_recoverable_broker_interruption(exc):
|
|
435
|
+
raise
|
|
436
|
+
await self._teardown_consume_channel(f"subscription setup interrupted: {exc!r}")
|
|
437
|
+
return []
|
|
389
438
|
|
|
390
439
|
buffer: List[Dict[str, Any]] = []
|
|
391
440
|
timeout = config.consume_timeout
|
|
@@ -404,7 +453,9 @@ class AsyncRabbitMQService:
|
|
|
404
453
|
)
|
|
405
454
|
except TimeoutError:
|
|
406
455
|
poll_completed = True
|
|
407
|
-
except
|
|
456
|
+
except Exception as exc:
|
|
457
|
+
if not _is_recoverable_broker_interruption(exc):
|
|
458
|
+
raise
|
|
408
459
|
# Broker cancelled this subscription mid-iteration; deterministically
|
|
409
460
|
# tear down (cancel consumers + close) the dead channel so the next
|
|
410
461
|
# call rebuilds against a fresh consumer, the broker redelivers what
|
|
@@ -435,15 +486,27 @@ class AsyncRabbitMQService:
|
|
|
435
486
|
batch_size: int,
|
|
436
487
|
auto_ack: bool,
|
|
437
488
|
) -> None:
|
|
438
|
-
"""
|
|
489
|
+
"""Wait for one delivery, then drain only messages already available.
|
|
490
|
+
|
|
491
|
+
``consume_timeout`` bounds the idle wait in :meth:`consume`; it must
|
|
492
|
+
not become a batch-assembly delay after the first delivery. Once any
|
|
493
|
+
message is buffered, give newly scheduled iterator reads one event-loop
|
|
494
|
+
turn to complete and return immediately if none are ready.
|
|
495
|
+
"""
|
|
439
496
|
pending: dict[asyncio.Task, tuple[str, Any]] = {}
|
|
440
|
-
|
|
497
|
+
iterator_contexts: list[AsyncContextManager[Any]] = []
|
|
498
|
+
try:
|
|
441
499
|
for queue_name, queue in queues:
|
|
442
500
|
iterator_context = cast(
|
|
443
501
|
AsyncContextManager[Any],
|
|
444
|
-
|
|
502
|
+
# The framework owns consumer teardown/resubscription.
|
|
503
|
+
# Letting aio-pika restore these short-lived per-fetch
|
|
504
|
+
# consumers can resurrect an iterator with no pending
|
|
505
|
+
# reader after a broker restart, creating a zombie tag.
|
|
506
|
+
queue.iterator(no_ack=auto_ack, robust=False),
|
|
445
507
|
)
|
|
446
|
-
iterator = await
|
|
508
|
+
iterator = await iterator_context.__aenter__()
|
|
509
|
+
iterator_contexts.append(iterator_context)
|
|
447
510
|
self._register_consumer_cancel_callback(iterator)
|
|
448
511
|
consumer_tag = getattr(iterator, "_consumer_tag", None) or getattr(iterator, "consumer_tag", None)
|
|
449
512
|
if consumer_tag:
|
|
@@ -451,33 +514,68 @@ class AsyncRabbitMQService:
|
|
|
451
514
|
pending[asyncio.create_task(anext(iterator))] = (queue_name, iterator)
|
|
452
515
|
|
|
453
516
|
self._active_consumer_tag = next(iter(self._active_consumer_tags.values()), None)
|
|
454
|
-
|
|
517
|
+
while pending and len(buffer) < batch_size:
|
|
518
|
+
if buffer:
|
|
519
|
+
done = {task for task in pending if task.done()}
|
|
520
|
+
if not done:
|
|
521
|
+
await asyncio.sleep(0)
|
|
522
|
+
done = {task for task in pending if task.done()}
|
|
523
|
+
if not done:
|
|
524
|
+
break
|
|
525
|
+
else:
|
|
526
|
+
done, _ = await asyncio.wait(
|
|
527
|
+
pending,
|
|
528
|
+
return_when=asyncio.FIRST_COMPLETED,
|
|
529
|
+
)
|
|
530
|
+
for task in done:
|
|
531
|
+
queue_name, iterator = pending.pop(task)
|
|
532
|
+
try:
|
|
533
|
+
message = task.result()
|
|
534
|
+
except StopAsyncIteration:
|
|
535
|
+
self._active_consumer_tags.pop(queue_name, None)
|
|
536
|
+
continue
|
|
537
|
+
|
|
538
|
+
message_dict = self._message_to_dict(message, queue_name=queue_name)
|
|
539
|
+
self._in_flight_message_ids.add(id(message))
|
|
540
|
+
self._last_fetch_ts = time.time()
|
|
541
|
+
self._consumer_state = "delivering"
|
|
542
|
+
if len(buffer) < batch_size:
|
|
543
|
+
buffer.append(message_dict)
|
|
544
|
+
else:
|
|
545
|
+
self._prefetched_messages.append(message_dict)
|
|
546
|
+
if len(buffer) < batch_size:
|
|
547
|
+
pending[asyncio.create_task(anext(iterator))] = (queue_name, iterator)
|
|
548
|
+
finally:
|
|
549
|
+
cleanup_timeout = min(1.0, self.client.channel_timeout)
|
|
550
|
+
cleanup_failed = False
|
|
551
|
+
for task in pending:
|
|
552
|
+
task.cancel()
|
|
553
|
+
if pending:
|
|
554
|
+
try:
|
|
555
|
+
async with timeout_after(cleanup_timeout):
|
|
556
|
+
await asyncio.gather(*pending, return_exceptions=True)
|
|
557
|
+
except TimeoutError:
|
|
558
|
+
cleanup_failed = True
|
|
559
|
+
logger.warning("Timed out cancelling RabbitMQ iterator reads")
|
|
560
|
+
cleanup_failed = await self._close_iterator_contexts(iterator_contexts) or cleanup_failed
|
|
561
|
+
if cleanup_failed:
|
|
562
|
+
await self._teardown_consume_channel("queue iterator cleanup failed")
|
|
563
|
+
|
|
564
|
+
async def _close_iterator_contexts(
|
|
565
|
+
self,
|
|
566
|
+
iterator_contexts: list[AsyncContextManager[Any]],
|
|
567
|
+
) -> bool:
|
|
568
|
+
"""Bound queue-iterator cleanup so a dead broker cannot stall a poll."""
|
|
569
|
+
cleanup_timeout = min(1.0, self.client.channel_timeout)
|
|
570
|
+
cleanup_failed = False
|
|
571
|
+
for iterator_context in reversed(iterator_contexts):
|
|
455
572
|
try:
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
except StopAsyncIteration:
|
|
463
|
-
self._active_consumer_tags.pop(queue_name, None)
|
|
464
|
-
continue
|
|
465
|
-
|
|
466
|
-
message_dict = self._message_to_dict(message, queue_name=queue_name)
|
|
467
|
-
self._in_flight_message_ids.add(id(message))
|
|
468
|
-
self._last_fetch_ts = time.time()
|
|
469
|
-
self._consumer_state = "delivering"
|
|
470
|
-
if len(buffer) < batch_size:
|
|
471
|
-
buffer.append(message_dict)
|
|
472
|
-
else:
|
|
473
|
-
self._prefetched_messages.append(message_dict)
|
|
474
|
-
if len(buffer) < batch_size:
|
|
475
|
-
pending[asyncio.create_task(anext(iterator))] = (queue_name, iterator)
|
|
476
|
-
finally:
|
|
477
|
-
for task in pending:
|
|
478
|
-
task.cancel()
|
|
479
|
-
if pending:
|
|
480
|
-
await asyncio.gather(*pending, return_exceptions=True)
|
|
573
|
+
async with timeout_after(cleanup_timeout):
|
|
574
|
+
await iterator_context.__aexit__(None, None, None)
|
|
575
|
+
except Exception as exc:
|
|
576
|
+
cleanup_failed = True
|
|
577
|
+
logger.warning("RabbitMQ queue iterator cleanup failed: %r", exc)
|
|
578
|
+
return cleanup_failed
|
|
481
579
|
|
|
482
580
|
def _register_consumer_cancel_callback(self, iterator: Any) -> None:
|
|
483
581
|
"""Best-effort registration of an on-cancel callback on the iterator's consumer.
|
|
@@ -676,9 +774,9 @@ class AsyncRabbitMQService:
|
|
|
676
774
|
logger.warning("Error closing %s: %r", attr_name, exc)
|
|
677
775
|
setattr(self, attr_name, None)
|
|
678
776
|
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
777
|
+
connection = self._connection
|
|
778
|
+
self._connection = None
|
|
779
|
+
await self._close_connection_safely(connection, "service close")
|
|
682
780
|
|
|
683
781
|
self._active_consumer_tag = None
|
|
684
782
|
self._active_consumer_tags.clear()
|
|
@@ -106,15 +106,18 @@ class _TaskLivenessSupervisor:
|
|
|
106
106
|
self,
|
|
107
107
|
provider: LivenessProvider,
|
|
108
108
|
policy: TaskSupervisionPolicy,
|
|
109
|
-
request_shutdown,
|
|
110
|
-
hard_exit=
|
|
109
|
+
request_shutdown: Callable[[str], None],
|
|
110
|
+
hard_exit: Callable[[int], None] | None = None,
|
|
111
111
|
) -> None:
|
|
112
112
|
self.provider = provider
|
|
113
113
|
self.policy = policy
|
|
114
114
|
self.request_shutdown = request_shutdown
|
|
115
|
-
self.hard_exit = hard_exit
|
|
115
|
+
self.hard_exit = hard_exit or os._exit
|
|
116
116
|
self._stop = threading.Event()
|
|
117
117
|
self._thread: threading.Thread | None = None
|
|
118
|
+
self._shutdown_lock = threading.Lock()
|
|
119
|
+
self._shutdown_requested_at: float | None = None
|
|
120
|
+
self._shutdown_reason: str | None = None
|
|
118
121
|
|
|
119
122
|
def start(self) -> None:
|
|
120
123
|
self._thread = threading.Thread(
|
|
@@ -129,13 +132,39 @@ class _TaskLivenessSupervisor:
|
|
|
129
132
|
if self._thread is not None and self._thread is not threading.current_thread():
|
|
130
133
|
self._thread.join(timeout=max(1.0, self.policy.check_interval * 2))
|
|
131
134
|
|
|
135
|
+
def arm_shutdown_deadline(self, reason: str) -> None:
|
|
136
|
+
"""Force process exit if any cooperative shutdown or cleanup wedges."""
|
|
137
|
+
with self._shutdown_lock:
|
|
138
|
+
if self._shutdown_requested_at is not None:
|
|
139
|
+
return
|
|
140
|
+
self._shutdown_requested_at = time.monotonic()
|
|
141
|
+
self._shutdown_reason = reason
|
|
142
|
+
_supervisor_logger.warning(
|
|
143
|
+
"Task shutdown deadline armed for %.1fs (%s)",
|
|
144
|
+
self.policy.shutdown_grace,
|
|
145
|
+
reason,
|
|
146
|
+
)
|
|
147
|
+
|
|
132
148
|
def _run(self) -> None:
|
|
133
149
|
started_at = time.monotonic()
|
|
134
150
|
unhealthy_since: float | None = None
|
|
135
|
-
shutdown_requested_at: float | None = None
|
|
136
151
|
|
|
137
152
|
while not self._stop.wait(self.policy.check_interval):
|
|
138
153
|
now = time.monotonic()
|
|
154
|
+
with self._shutdown_lock:
|
|
155
|
+
shutdown_requested_at = self._shutdown_requested_at
|
|
156
|
+
shutdown_reason = self._shutdown_reason
|
|
157
|
+
if shutdown_requested_at is not None:
|
|
158
|
+
if now - shutdown_requested_at >= self.policy.shutdown_grace:
|
|
159
|
+
_supervisor_logger.critical(
|
|
160
|
+
"Task did not stop within %.1fs after shutdown began (%s); forcing exit",
|
|
161
|
+
self.policy.shutdown_grace,
|
|
162
|
+
shutdown_reason,
|
|
163
|
+
)
|
|
164
|
+
self._stop.set()
|
|
165
|
+
self.hard_exit(int(ExitCode.ERROR))
|
|
166
|
+
continue
|
|
167
|
+
|
|
139
168
|
if now - started_at < self.policy.startup_grace:
|
|
140
169
|
continue
|
|
141
170
|
|
|
@@ -157,22 +186,14 @@ class _TaskLivenessSupervisor:
|
|
|
157
186
|
_supervisor_logger.error("Task liveness check failed: %s", reason)
|
|
158
187
|
continue
|
|
159
188
|
|
|
160
|
-
if
|
|
161
|
-
shutdown_requested_at = now
|
|
189
|
+
if now - unhealthy_since >= self.policy.unhealthy_grace:
|
|
162
190
|
_supervisor_logger.critical(
|
|
163
191
|
"Task remained unhealthy for %.1fs (%s); requesting process shutdown",
|
|
164
192
|
now - unhealthy_since,
|
|
165
193
|
reason,
|
|
166
194
|
)
|
|
195
|
+
self.arm_shutdown_deadline(reason)
|
|
167
196
|
self.request_shutdown(reason)
|
|
168
|
-
continue
|
|
169
|
-
|
|
170
|
-
if shutdown_requested_at is not None and now - shutdown_requested_at >= self.policy.shutdown_grace:
|
|
171
|
-
_supervisor_logger.critical(
|
|
172
|
-
"Task did not stop within %.1fs after liveness shutdown request; forcing exit",
|
|
173
|
-
self.policy.shutdown_grace,
|
|
174
|
-
)
|
|
175
|
-
self.hard_exit(int(ExitCode.ERROR))
|
|
176
197
|
|
|
177
198
|
|
|
178
199
|
# =============================================================
|
|
@@ -313,8 +334,10 @@ async def __run_task_async(
|
|
|
313
334
|
raise
|
|
314
335
|
finally:
|
|
315
336
|
_unregister_shutdown_callback(request_signal_shutdown)
|
|
316
|
-
if supervisor is not None
|
|
317
|
-
supervisor.
|
|
337
|
+
if supervisor is not None:
|
|
338
|
+
supervisor.arm_shutdown_deadline(
|
|
339
|
+
"liveness shutdown cleanup" if liveness_shutdown_requested.is_set() else "task cleanup"
|
|
340
|
+
)
|
|
318
341
|
try:
|
|
319
342
|
if instance is not None:
|
|
320
343
|
await instance.exit()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/connector/__init__.py
RENAMED
|
File without changes
|
{ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/connector/connector.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/connector/excel/__init__.py
RENAMED
|
File without changes
|
{ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/connector/excel/connector.py
RENAMED
|
File without changes
|
{ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/connector/excel/models.py
RENAMED
|
File without changes
|
{ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/connector/excel/service.py
RENAMED
|
File without changes
|
{ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/connector/nylas/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/connector/nylas/connector.py
RENAMED
|
File without changes
|
{ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/connector/nylas/models.py
RENAMED
|
File without changes
|
{ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/connector/nylas/service.py
RENAMED
|
File without changes
|
{ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/connector/nylas/utils.py
RENAMED
|
File without changes
|
{ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/connector/pipefy/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/connector/pipefy/models.py
RENAMED
|
File without changes
|
{ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/connector/pipefy/service.py
RENAMED
|
File without changes
|
{ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/connector/pipefy/version.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/connector/postgres/models.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/connector/rabbitmq/helper.py
RENAMED
|
File without changes
|
{ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/connector/rabbitmq/models.py
RENAMED
|
File without changes
|
|
File without changes
|
{ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/connector/sqs/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/connector/sqs/connector.py
RENAMED
|
File without changes
|
{ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/connector/sqs/models.py
RENAMED
|
File without changes
|
{ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/connector/sqs/service.py
RENAMED
|
File without changes
|
{ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/connector/transaction.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/task/mixins/__init__.py
RENAMED
|
File without changes
|
{ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/task/mixins/consumer.py
RENAMED
|
File without changes
|
{ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/task/mixins/metrics.py
RENAMED
|
File without changes
|
{ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/task/mixins/producer.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/telemetry/__init__.py
RENAMED
|
File without changes
|
{ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/telemetry/_resource.py
RENAMED
|
File without changes
|
{ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/telemetry/logging.py
RENAMED
|
File without changes
|
{ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/telemetry/metrics.py
RENAMED
|
File without changes
|
{ergon_framework_python-0.1.6 → ergon_framework_python-0.1.8}/src/ergon/telemetry/tracing.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|