ergon-framework-python 0.1.7__py3-none-any.whl → 0.1.8__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.
- ergon/__init__.py +1 -1
- ergon/connector/rabbitmq/async_service.py +29 -7
- ergon/task/runner.py +39 -16
- {ergon_framework_python-0.1.7.dist-info → ergon_framework_python-0.1.8.dist-info}/METADATA +1 -1
- {ergon_framework_python-0.1.7.dist-info → ergon_framework_python-0.1.8.dist-info}/RECORD +9 -9
- {ergon_framework_python-0.1.7.dist-info → ergon_framework_python-0.1.8.dist-info}/WHEEL +0 -0
- {ergon_framework_python-0.1.7.dist-info → ergon_framework_python-0.1.8.dist-info}/entry_points.txt +0 -0
- {ergon_framework_python-0.1.7.dist-info → ergon_framework_python-0.1.8.dist-info}/licenses/LICENSE +0 -0
- {ergon_framework_python-0.1.7.dist-info → ergon_framework_python-0.1.8.dist-info}/top_level.txt +0 -0
ergon/__init__.py
CHANGED
|
@@ -155,11 +155,33 @@ class AsyncRabbitMQService:
|
|
|
155
155
|
|
|
156
156
|
connection = self._connection
|
|
157
157
|
self._connection = None
|
|
158
|
-
|
|
159
|
-
|
|
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):
|
|
160
176
|
await connection.close()
|
|
161
|
-
|
|
162
|
-
|
|
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)
|
|
163
185
|
|
|
164
186
|
def _invalidate_consume_channel(self, reason: str = "explicit invalidation") -> None:
|
|
165
187
|
"""Drop cached consume channel + queue/exchange handles.
|
|
@@ -752,9 +774,9 @@ class AsyncRabbitMQService:
|
|
|
752
774
|
logger.warning("Error closing %s: %r", attr_name, exc)
|
|
753
775
|
setattr(self, attr_name, None)
|
|
754
776
|
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
777
|
+
connection = self._connection
|
|
778
|
+
self._connection = None
|
|
779
|
+
await self._close_connection_safely(connection, "service close")
|
|
758
780
|
|
|
759
781
|
self._active_consumer_tag = None
|
|
760
782
|
self._active_consumer_tags.clear()
|
ergon/task/runner.py
CHANGED
|
@@ -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()
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
ergon/__init__.py,sha256=
|
|
1
|
+
ergon/__init__.py,sha256=7fnK8l6RwlG4tyi-l7DfBQbjLLftAFsJBFUSbjY5u4U,218
|
|
2
2
|
ergon/cli.py,sha256=CF8PvOjIguR18a657aEBEyhFXUoSVtSRVXv5PfnuWtA,6021
|
|
3
3
|
ergon/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
4
|
ergon/bootstrap/src/__project__/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -58,7 +58,7 @@ ergon/connector/postgres/async_service.py,sha256=PdPnNElMKzLoov1as3Kcb_viuSyBaVE
|
|
|
58
58
|
ergon/connector/postgres/models.py,sha256=DL9wBdZ6QZPH1iVft71cTabnFUCH1Hg1RmG5f5E8578,1749
|
|
59
59
|
ergon/connector/rabbitmq/__init__.py,sha256=HOiQ2ZmrbgsW20g3HcHzT_PdZ0-M9oDj4Fwzg0N67Rw,817
|
|
60
60
|
ergon/connector/rabbitmq/async_connector.py,sha256=1ltA8wGcB91Sxq6xJcX0OeOTRd6co4cpq5drG-6gHAc,5175
|
|
61
|
-
ergon/connector/rabbitmq/async_service.py,sha256=
|
|
61
|
+
ergon/connector/rabbitmq/async_service.py,sha256=A5qcIl3ucEoMfFv6b1ohdJOxJ2_xzL6ZeTcvJ78P3F8,35101
|
|
62
62
|
ergon/connector/rabbitmq/connector.py,sha256=vsSzbVuuE8rKkwBfPgD97qTsqADxJGALnUvrEwZIL5Q,1633
|
|
63
63
|
ergon/connector/rabbitmq/helper.py,sha256=xlQGx2SGR2DC1fi-5BMlCOvWydWEFkcN7fPJjlNvGLA,372
|
|
64
64
|
ergon/connector/rabbitmq/models.py,sha256=UxqAAHhArMOry0MLKBJEtAKBNdyHNZVAvqJkYhCv71Y,9654
|
|
@@ -78,7 +78,7 @@ ergon/task/helpers.py,sha256=Fzb2WsP--5f54VRo_oaPH3Hq9kIYoykfEDOhpd_QHXY,22879
|
|
|
78
78
|
ergon/task/liveness.py,sha256=TsytIfmi2ua_x3KEPjTdx4WbqHk0WXTqeCSiQUBKPsE,1314
|
|
79
79
|
ergon/task/manager.py,sha256=3Xr8RPRjtCEoupMeyYb19uK0TQhoVehFlBZd8ZFZ-YA,2972
|
|
80
80
|
ergon/task/policies.py,sha256=mGb8iafrkVf438xiAUh0FpXqfgtNAM7RyoaF8jHNf80,7042
|
|
81
|
-
ergon/task/runner.py,sha256=
|
|
81
|
+
ergon/task/runner.py,sha256=xUaMSO6V_iOq6QJdiyW1IA5kfWuwQjh1IJl6HUh06hY,20344
|
|
82
82
|
ergon/task/utils.py,sha256=OmlP2AhLM32V8O9OMky6DdSvxwmMNn9x2LTguf0y-M0,2501
|
|
83
83
|
ergon/task/mixins/__init__.py,sha256=6A8vNgdjaQYNJvymWNF-kBN2oTaQBYRLctS3Sq1w_-Y,320
|
|
84
84
|
ergon/task/mixins/consumer.py,sha256=0S_ZU27cCn74LoDZdPGH-15wmpAtZI1HcS9Iqw2h3DE,46088
|
|
@@ -91,9 +91,9 @@ ergon/telemetry/metrics.py,sha256=0T_CuNw9P5AG1U9k1-bNp1g7McyiNLUIt_zKyLOtrnA,28
|
|
|
91
91
|
ergon/telemetry/tracing.py,sha256=_ktiZWAgB1DU-uN91Mzr0iYNXxbCN21LnKiRo5SOJxY,4454
|
|
92
92
|
ergon/utils/__init__.py,sha256=M0_VCjMN7jvSPlRZLZHeFKTHAl1zu3BnAhjrzJjDfiI,57
|
|
93
93
|
ergon/utils/env.py,sha256=lji4xENvfHqg2GTGnzG95VKpxxM4f3DHIKKP61xQG08,621
|
|
94
|
-
ergon_framework_python-0.1.
|
|
95
|
-
ergon_framework_python-0.1.
|
|
96
|
-
ergon_framework_python-0.1.
|
|
97
|
-
ergon_framework_python-0.1.
|
|
98
|
-
ergon_framework_python-0.1.
|
|
99
|
-
ergon_framework_python-0.1.
|
|
94
|
+
ergon_framework_python-0.1.8.dist-info/licenses/LICENSE,sha256=bWCIhHzaadz_ecLdCdtwyMafeQ17s2d7_FSSuSuEcDs,1079
|
|
95
|
+
ergon_framework_python-0.1.8.dist-info/METADATA,sha256=08L2OhngcleW4qWp0S48Dvqfz_dJAwFnauPp_RJh_eU,15677
|
|
96
|
+
ergon_framework_python-0.1.8.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
97
|
+
ergon_framework_python-0.1.8.dist-info/entry_points.txt,sha256=reZ934txb5OrtcNBgTGqCsXa1biUDQWubD2a9fGZynE,42
|
|
98
|
+
ergon_framework_python-0.1.8.dist-info/top_level.txt,sha256=KSPt6N7RzsjU7PRAalWQ9Cul7YTUxbo84yAJ-y2vLm8,6
|
|
99
|
+
ergon_framework_python-0.1.8.dist-info/RECORD,,
|
|
File without changes
|
{ergon_framework_python-0.1.7.dist-info → ergon_framework_python-0.1.8.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{ergon_framework_python-0.1.7.dist-info → ergon_framework_python-0.1.8.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|
{ergon_framework_python-0.1.7.dist-info → ergon_framework_python-0.1.8.dist-info}/top_level.txt
RENAMED
|
File without changes
|