polyapi-python 0.3.20.dev3__py3-none-any.whl → 0.3.20.dev4__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.
- polyapi/http_client.py +66 -2
- {polyapi_python-0.3.20.dev3.dist-info → polyapi_python-0.3.20.dev4.dist-info}/METADATA +8 -1
- {polyapi_python-0.3.20.dev3.dist-info → polyapi_python-0.3.20.dev4.dist-info}/RECORD +6 -6
- {polyapi_python-0.3.20.dev3.dist-info → polyapi_python-0.3.20.dev4.dist-info}/WHEEL +0 -0
- {polyapi_python-0.3.20.dev3.dist-info → polyapi_python-0.3.20.dev4.dist-info}/licenses/LICENSE +0 -0
- {polyapi_python-0.3.20.dev3.dist-info → polyapi_python-0.3.20.dev4.dist-info}/top_level.txt +0 -0
polyapi/http_client.py
CHANGED
|
@@ -45,6 +45,12 @@ def _build_timeout() -> httpx.Timeout:
|
|
|
45
45
|
)
|
|
46
46
|
|
|
47
47
|
|
|
48
|
+
def _close_deadline() -> "float | None":
|
|
49
|
+
# Bound pool teardown so one wedged socket cannot hang invocation exit.
|
|
50
|
+
# None (POLY_HTTP_CLOSE_TIMEOUT=none) restores the old unbounded wait.
|
|
51
|
+
return _env_opt_float("POLY_HTTP_CLOSE_TIMEOUT", 5.0)
|
|
52
|
+
|
|
53
|
+
|
|
48
54
|
# Import-time snapshot for reference/config surface. Clients re-read env at creation
|
|
49
55
|
DEFAULT_LIMITS = _build_limits()
|
|
50
56
|
DEFAULT_TIMEOUT = _build_timeout()
|
|
@@ -166,6 +172,40 @@ def _sweep_dead_loops() -> None:
|
|
|
166
172
|
_retire_async_client(_async_clients.pop(loop, None), loop)
|
|
167
173
|
|
|
168
174
|
|
|
175
|
+
def _abandon_close_task(
|
|
176
|
+
close_task: "asyncio.Future[None]",
|
|
177
|
+
client: httpx.AsyncClient,
|
|
178
|
+
loop: asyncio.AbstractEventLoop,
|
|
179
|
+
deadline: "float | None",
|
|
180
|
+
context: str = "",
|
|
181
|
+
) -> None:
|
|
182
|
+
"""Detach a pool close that blew its deadline.
|
|
183
|
+
|
|
184
|
+
The task is cancelled and dropped rather than awaited: waiting for an
|
|
185
|
+
unresponsive pool to unwind is the stall the deadline exists to prevent.
|
|
186
|
+
Cancelling is what keeps loop shutdown bounded too, since asyncio.run()
|
|
187
|
+
tears down by cancelling every task and gathering them; a cancelled close
|
|
188
|
+
completes there instead of blocking. Whatever stays open is released when
|
|
189
|
+
the coroutine is collected, and by the OS at exit.
|
|
190
|
+
"""
|
|
191
|
+
def _swallow(finished: "asyncio.Future[None]") -> None:
|
|
192
|
+
# Retrieve the result so a late failure is not reported as an
|
|
193
|
+
# unretrieved task exception.
|
|
194
|
+
if finished.cancelled():
|
|
195
|
+
return
|
|
196
|
+
error = finished.exception()
|
|
197
|
+
if error is not None:
|
|
198
|
+
logger.debug(
|
|
199
|
+
f"Abandoned aclose later failed with {error!r} id={id(client)} "
|
|
200
|
+
f"pid={os.getpid()} loop={id(loop)}")
|
|
201
|
+
|
|
202
|
+
close_task.add_done_callback(_swallow)
|
|
203
|
+
close_task.cancel()
|
|
204
|
+
logger.warning(
|
|
205
|
+
f"aclose exceeded {deadline}s deadline; abandoning pool "
|
|
206
|
+
f"id={id(client)} pid={os.getpid()} loop={id(loop)}{context}")
|
|
207
|
+
|
|
208
|
+
|
|
169
209
|
def _register_loop_close_hook(loop: asyncio.AbstractEventLoop) -> None:
|
|
170
210
|
"""aclose the loop's async client right before the loop is torn down.
|
|
171
211
|
"""
|
|
@@ -177,8 +217,21 @@ def _register_loop_close_hook(loop: asyncio.AbstractEventLoop) -> None:
|
|
|
177
217
|
_hooked_loops.discard(loop)
|
|
178
218
|
cached = _async_clients.pop(loop, None)
|
|
179
219
|
if cached is not None and not loop.is_closed():
|
|
220
|
+
deadline = _close_deadline()
|
|
180
221
|
try:
|
|
181
|
-
|
|
222
|
+
# asyncio.wait returns at the deadline without cancelling, so
|
|
223
|
+
# loop.close() proceeds even if the pool ignores cancellation.
|
|
224
|
+
close_task = loop.create_task(cached.aclose())
|
|
225
|
+
loop.run_until_complete(
|
|
226
|
+
asyncio.wait({close_task}, timeout=deadline))
|
|
227
|
+
if close_task.done():
|
|
228
|
+
# Surface real aclose failures, including a TimeoutError
|
|
229
|
+
# raised by aclose itself, rather than treating them as
|
|
230
|
+
# our deadline.
|
|
231
|
+
close_task.result()
|
|
232
|
+
else:
|
|
233
|
+
_abandon_close_task(
|
|
234
|
+
close_task, cached, loop, deadline, " on teardown")
|
|
182
235
|
except Exception:
|
|
183
236
|
logger.warning(
|
|
184
237
|
f"Failed to aclose async client id={id(cached)} pid={os.getpid()} "
|
|
@@ -267,8 +320,19 @@ async def _close_clients_for_loop(
|
|
|
267
320
|
return
|
|
268
321
|
|
|
269
322
|
await _drain_inflight(client)
|
|
323
|
+
deadline = _close_deadline()
|
|
324
|
+
close_task = asyncio.ensure_future(client.aclose())
|
|
270
325
|
try:
|
|
271
|
-
|
|
326
|
+
# asyncio.wait returns at the deadline without cancelling the
|
|
327
|
+
# close, so the bound holds even when the pool never processes
|
|
328
|
+
# cancellation. wait_for would block on that unwind instead.
|
|
329
|
+
await asyncio.wait({close_task}, timeout=deadline)
|
|
330
|
+
if close_task.done():
|
|
331
|
+
# Real aclose errors, including a TimeoutError raised by aclose
|
|
332
|
+
# itself, still propagate to the close_async caller.
|
|
333
|
+
close_task.result()
|
|
334
|
+
else:
|
|
335
|
+
_abandon_close_task(close_task, client, current_loop, deadline)
|
|
272
336
|
finally:
|
|
273
337
|
if _async_clients.get(current_loop) is client:
|
|
274
338
|
_async_clients.pop(current_loop, None)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: polyapi-python
|
|
3
|
-
Version: 0.3.20.
|
|
3
|
+
Version: 0.3.20.dev4
|
|
4
4
|
Summary: The Python SDK for the integration platform PolyAPI
|
|
5
5
|
Author-email: PolyAPI <support@polyapi.io>
|
|
6
6
|
License: MIT License
|
|
@@ -98,10 +98,17 @@ client per event loop. New clients read these optional environment variables:
|
|
|
98
98
|
| `POLY_HTTP_READ_TIMEOUT` | `none` | Read timeout in seconds |
|
|
99
99
|
| `POLY_HTTP_WRITE_TIMEOUT` | `none` | Write timeout in seconds |
|
|
100
100
|
| `POLY_HTTP_POOL_TIMEOUT` | `none` | Connection-pool timeout in seconds |
|
|
101
|
+
| `POLY_HTTP_CLOSE_TIMEOUT` | `5` | Deadline in seconds for closing a client's pool |
|
|
101
102
|
|
|
102
103
|
An empty value, `none`, or `null` means unbounded. Limits are per client, so
|
|
103
104
|
processes with multiple event loops can create one configured pool per loop.
|
|
104
105
|
|
|
106
|
+
`POLY_HTTP_CLOSE_TIMEOUT` is read at shutdown rather than at client creation.
|
|
107
|
+
It bounds pool teardown so a single unresponsive socket cannot stall shutdown:
|
|
108
|
+
if the deadline passes, the SDK logs a warning, abandons the remaining
|
|
109
|
+
connections to the operating system, and retires the client so it is never
|
|
110
|
+
reused. Set it to `none` to wait indefinitely.
|
|
111
|
+
|
|
105
112
|
Call `polyapi.http_client.close()` during synchronous shutdown or await
|
|
106
113
|
`polyapi.http_client.close_async()` on each owning event loop during
|
|
107
114
|
asynchronous shutdown. The shutdown functions wait for active SDK requests
|
|
@@ -14,7 +14,7 @@ polyapi/exceptions.py,sha256=Zh7i7eCUhDuXEdUYjatkLFTeZkrx1BJ1P5ePgbJ9eIY,89
|
|
|
14
14
|
polyapi/execute.py,sha256=4jLkQely6cM7pNN9pYpGZTbEis-DfwENi5Fl07xSiig,8966
|
|
15
15
|
polyapi/function_cli.py,sha256=gJE72lRvuWnGl6CG9GpqL78TOnUzxVe4kJMvg0PQixw,4493
|
|
16
16
|
polyapi/generate.py,sha256=U8JQ_XR2gJI_eYNnmvXReILUxapj95QKoBronqMFkyE,22403
|
|
17
|
-
polyapi/http_client.py,sha256=
|
|
17
|
+
polyapi/http_client.py,sha256=5N-zWSsjdMCPnrICR45EJWtXtBNyMP42L6Q8_Bh7n1A,15198
|
|
18
18
|
polyapi/parser.py,sha256=t3jJI6g5F7Qg_tirPOzSkQ02VLkSVhnqs9YGapyHx2k,21901
|
|
19
19
|
polyapi/poly_schemas.py,sha256=JGMQbELPFw4V_0nA0o71WJKFTdxjCGoKqJvIid_Y-a4,10759
|
|
20
20
|
polyapi/poly_tables.py,sha256=SlP7ZwqS3nIji2uGR2VJQH6IdddZ832rRItcmoYdIBc,20293
|
|
@@ -28,8 +28,8 @@ polyapi/typedefs.py,sha256=UeAdyYrgqxrvBRkjisK_xgzQmuTePPvOM74HKf1y0II,5656
|
|
|
28
28
|
polyapi/utils.py,sha256=LUqTkbb0b7y3gjsOU-WO1LVYaTMyExTGpYjKY8wVKg8,16300
|
|
29
29
|
polyapi/variables.py,sha256=hfSDPGQK6YphNCa9MqE0W88WIFfqCQWgpBDWkExMq-A,7582
|
|
30
30
|
polyapi/webhook.py,sha256=0ceLwHNjNd2Yx_8MX5jOIxiQ5Lwhy2pe81ProAuKon0,5108
|
|
31
|
-
polyapi_python-0.3.20.
|
|
32
|
-
polyapi_python-0.3.20.
|
|
33
|
-
polyapi_python-0.3.20.
|
|
34
|
-
polyapi_python-0.3.20.
|
|
35
|
-
polyapi_python-0.3.20.
|
|
31
|
+
polyapi_python-0.3.20.dev4.dist-info/licenses/LICENSE,sha256=6b_I7aPVp8JXhqQwdw7_B84Ca0S4JGjHj0sr_1VOdB4,1068
|
|
32
|
+
polyapi_python-0.3.20.dev4.dist-info/METADATA,sha256=wYIJAgguqGC3FNXxYZHtQcLtumtopR5vf29f4X7W9rI,7737
|
|
33
|
+
polyapi_python-0.3.20.dev4.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
34
|
+
polyapi_python-0.3.20.dev4.dist-info/top_level.txt,sha256=CEFllOnzowci_50RYJac-M54KD2IdAptFsayVVF_f04,8
|
|
35
|
+
polyapi_python-0.3.20.dev4.dist-info/RECORD,,
|
|
File without changes
|
{polyapi_python-0.3.20.dev3.dist-info → polyapi_python-0.3.20.dev4.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|
|
File without changes
|