[Gax] RetryMiddleware keeps a reference to itself since v1.43.1, leaking the handler chain and its sockets in long-running processes
维护者通常 1 天内回复
@Hectorhammett 已经在做这个了。
开始于 2026年10月23日。
评估
调研方向
报告指出,问题源自 RetryMiddleware.php 和 delayHandler 赋值。阅读该中间件,并追踪 handler 如何被调用以及如何传递给嵌套的重试中间件。在禁用循环收集的情况下复现 WeakReference 检查;取消对中间件的引用后,无需调用 gc_collect_cycles() 即可释放它,同时自定义延迟 handler 仍能正常工作,即为完成。
由索引模型根据 Issue 内容生成。
描述
Environment details
- OS: Linux (Docker), reproduced on macOS too
- PHP version: 8.3.36 (NTS)
- Package name and version:
google/gaxv1.51.1 (regression since v1.43.1, v1.43.0 is fine) - Also:
googleads/google-ads-php35.2.0,guzzlehttp/guzzle7.16.0, REST transport
Summary
Since v1.43.1 (#676, commit 5d3d6da, "RetryMiddleware should sleep on retry"), RetryMiddleware::__construct() stores a callable that points back to the middleware itself:
$this->delayHandler = ($delayHandler ?? [$this, 'sleepMillis']);
This creates a self-reference cycle, so a RetryMiddleware instance is never freed by reference counting. It is only freed when PHP's cycle collector runs. The middleware holds $nextHandler, which in turn holds the transport, the HTTP handler and the Guzzle client. So every call leaves its whole handler chain in memory, together with any open sockets.
Short-lived requests barely notice this. Long-running PHP processes do: queue consumers such as Symfony Messenger workers, daemons, CLI loops. In our worker every Google Ads call left 5 file descriptors open. After a few hours of normal load the process hit ulimit -n (1024) and started failing with Too many open files. The errors then surfaced as unrelated failures:
Class "GuzzleHttp\Promise\RejectedPromise" not found(the autoloader could not open the file)Google\ApiCore\RetrySettings::load(): Argument #2 ($clientConfig) must be of type array, null given(file_get_contents()of the client config returnedfalse)
Steps to reproduce
<?php
require 'vendor/autoload.php';
use Google\ApiCore\Middleware\RetryMiddleware;
use Google\ApiCore\RetrySettings;
gc_disable(); // make cycle collection explicit
$middleware = new RetryMiddleware(fn () => null, RetrySettings::constructDefault());
$ref = WeakReference::create($middleware);
unset($middleware);
var_dump($ref->get() === null); // v1.43.0: true; v1.43.1 … v1.51.1: false
gc_collect_cycles();
var_dump($ref->get() === null); // true: only the cycle collector frees it
Impact measured with a real client
We made 20 sequential GoogleAdsServiceClient::search() calls over REST against a local HTTPS stub. A new GAPIC client was built for each call, and no httpHandler was passed, so gax builds its default Guzzle client:
google/gax |
open fds after 20 calls | after gc_collect_cycles() |
|---|---|---|
| v1.42.5, v1.43.0 | +0 | +0 |
| v1.43.1, v1.43.2, v1.44.0, v1.45.0, v1.46.0, v1.48.0, v1.51.1 | +100 | +0 |
The only change to src/ between v1.43.0 and v1.43.1 is in Middleware/RetryMiddleware.php (the delayHandler addition).
Expected behavior
A RetryMiddleware instance, and the handler chain it references, is freed as soon as the call completes, as it was in v1.43.0.
Suggested fix
Do not store a callable bound to $this. Resolve the default when the delay is applied:
public function __construct(
callable $nextHandler,
RetrySettings $retrySettings,
$deadlineMs = null,
$retryAttempts = 0,
?callable $delayHandler = null
) {
// ...
$this->delayHandler = $delayHandler;
}
// in __invoke / retry():
if ($this->delayHandler) {
($this->delayHandler)($delayMs);
} else {
$this->sleepMillis($delayMs);
}
Passing $this->delayHandler (possibly null) to the nested RetryMiddleware keeps the current behavior.
Workarounds
- Pass one shared
httpHandlerto all clients, so the leaked chains do not each hold their own sockets. - Call
gc_collect_cycles()periodically in long-running processes.
- 主要语言
- PHP
- 星标
- 1.2k
- 派生
- 466
- 平均合并
- 1 天 17 小时
- 30 天内合并 PR
- 40
环境准备
- 没有 Dockerfile 或 Docker Compose 文件
- 没有 Pull Request 模板
- 阅读贡献指南
从这里开始
- 先读完整个 Issue,再读项目的贡献指南。
- 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
- Fork 仓库,在一个分支上完成修改。
- 提交 Pull Request,并在描述里引用这个 Issue 编号。
googleapis/google-cloud-php 的其他 Issue
-
难度 2/5 1-3 小时 新手友好度 84/100
googleapis/google-cloud-php#9811 ·
维护者通常 1 天内回复
-
Consider clarifying which RFCs are supported可能已有人在做 @bshaffer 于 14 天前认领。 未关闭type: feature request
难度 1/5 1 小时以内 新手友好度 75/100
googleapis/google-cloud-php#9716 · 11 个 reaction ·
维护者通常 1 天内回复
-
难度 1/5 1 小时以内 新手友好度 8/100
googleapis/google-cloud-php#9844 ·
维护者通常 1 天内回复
-
Release verification failed可能已有人在做 @bshaffer 于 13 天前认领。 未关闭:rotating_light: critical
googleapis/google-cloud-php#9838 · 12 条评论 · 已指派 1 人 ·
维护者通常 1 天内回复
-
[firebase/php-jwt] Support ML-DSA signing algorithms可能已有人在做 @bshaffer 于 36 天前认领。 未关闭
难度 5/5 一周以上 新手友好度 35/100
googleapis/google-cloud-php#9777 · 4 条评论 · 2 个 reaction · 已指派 1 人 ·
维护者通常 1 天内回复
查看 googleapis/google-cloud-php 的全部 Issue
相似的 Issue
-
难度 1/5 1 小时以内 新手友好度 89/100
backdrop-contrib/commerce#65 ·
-
backend enhancement good first issue php
难度 2/5 1-3 小时 新手友好度 82/100
维护者通常 1 天内回复
-
enhancement good first issue help wanted
难度 2/5 1-3 小时 新手友好度 78/100
Evalogical-Inc/devReady#66 ·
维护者通常 1 天内回复
-
难度 2/5 1-3 小时 新手友好度 78/100
mollie/PrestaShop#1627 ·
维护者通常 5 天内回复
-
bug
难度 2/5 1-3 小时 新手友好度 72/100
FriendsOfFlarum/follow-tags#118 ·