mirror of
https://github.com/nextcloud/all-in-one.git
synced 2026-09-19 01:37:24 +00:00
aio-interface: fix nginx reverse proxy read timeout blocks AIO from starting consecutive containers (#8016)
* Fix: prevent nginx proxy read timeout from blocking AIO container startup When AIO runs behind an nginx reverse proxy and a user clicks Start, image pulls produce no streaming output for minutes at a time. nginx's proxy_read_timeout fires, drops the upstream connection, and PHP then aborts on the next write attempt (ignore_user_abort defaults to false), leaving all containers after the first one never started. Two fixes: 1. startStreamingResponse(): add ignore_user_abort(true) so PHP never terminates if the connection is already gone. 2. PullImage(): stream the Docker NDJSON pull response and write a "Pulling image" heartbeat at most once every 5 s, keeping the nginx connection alive. Also surfaces Docker-level stream errors that the old buffered call silently ignored, guards against malformed newline-free responses with a 1 MB buffer limit, and unifies the duplicate catch-block retry logic. Agent-Logs-Url: https://github.com/nextcloud/all-in-one/sessions/4fd13605-63fb-4693-8a95-89ccec31f7d3 Co-authored-by: szaimen <42591237+szaimen@users.noreply.github.com> * As heartbeat send a dot regularly Rather than repeating the message, send a "magic" dot, which gets appended to the previous line. Previously the heartbeats weren't sent regulary because reading the data into a buffer caused a lag. Signed-off-by: Pablo Zmdl <pablo@nextcloud.com> * Remove left-over constants Signed-off-by: Pablo Zmdl <pablo@nextcloud.com> * Reduce timeout to stay within nginx's default timeout of 5s Signed-off-by: Pablo Zmdl <pablo@nextcloud.com> * Fix duplicated library namespace inclusion Signed-off-by: Pablo Zmdl <pablo@nextcloud.com> * Deal with preg_replace possibly returning null Signed-off-by: Pablo Zmdl <pablo@nextcloud.com> * update the version tag Signed-off-by: Simon L. <szaimen@e.mail.de> --------- Signed-off-by: Pablo Zmdl <pablo@nextcloud.com> Signed-off-by: Simon L. <szaimen@e.mail.de> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: szaimen <42591237+szaimen@users.noreply.github.com> Co-authored-by: Pablo Zmdl <pablo@nextcloud.com> Co-authored-by: Simon L. <szaimen@e.mail.de>
This commit is contained in:
co-authored by
szaimen
Pablo Zmdl
Simon L.
parent
d20eb5edda
commit
fc4bb548a4
@@ -11,12 +11,13 @@ use AIO\Data\ConfigurationManager;
|
||||
use AIO\Data\DataConst;
|
||||
use AIO\Helper\NetworkHelper;
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Exception\RequestException;
|
||||
use GuzzleHttp\Psr7\Utils;
|
||||
use GuzzleHttp\Exception\RequestException;
|
||||
use http\Env\Response;
|
||||
|
||||
readonly class DockerActionManager {
|
||||
private const string API_VERSION = 'v1.44';
|
||||
private const int PULL_HEARTBEAT_INTERVAL_SECONDS = 4;
|
||||
private Client $guzzleClient;
|
||||
|
||||
public function __construct(
|
||||
@@ -570,10 +571,43 @@ readonly class DockerActionManager {
|
||||
$maxRetries = 3;
|
||||
for ($attempt = 1; $attempt <= $maxRetries; $attempt++) {
|
||||
try {
|
||||
$this->sendHttpRequest('POST', $url);
|
||||
// Use streaming so we can write heartbeat messages to the response while the
|
||||
// image is being pulled. Without this, a long pull produces no output and a
|
||||
// reverse proxy (nginx) can drop the connection after its read timeout expires.
|
||||
// Once the connection is gone, PHP aborts on the next write and all consecutive
|
||||
// containers are never started.
|
||||
$pullResponse = $this->sendHttpRequest('POST', $url, ['stream' => true]);
|
||||
$pullBody = $pullResponse->getBody();
|
||||
$pullErrors = [];
|
||||
$lastHeartbeat = time();
|
||||
while (!$pullBody->eof()) {
|
||||
$line = Utils::readLine($pullBody);
|
||||
$event = json_decode($line, true);
|
||||
if (!is_array($event)) {
|
||||
continue;
|
||||
}
|
||||
if (isset($event['error'])) {
|
||||
$pullErrors[] = $event['error'];
|
||||
} elseif ($addToStreamingResponseBody !== null) {
|
||||
// Write a heartbeat at most once every 5 seconds so the reverse
|
||||
// proxy sees continuous data and does not close the connection.
|
||||
$now = time();
|
||||
$interval = time() - $lastHeartbeat;
|
||||
if ($interval >= self::PULL_HEARTBEAT_INTERVAL_SECONDS) {
|
||||
$addToStreamingResponseBody(".", $container);
|
||||
$lastHeartbeat = $now;
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($pullErrors !== []) {
|
||||
throw new \Exception(implode('; ', $pullErrors));
|
||||
}
|
||||
break;
|
||||
} catch (RequestException $e) {
|
||||
$message = "Could not pull image " . $imageName . " (attempt $attempt/$maxRetries): " . $e->getResponse()?->getBody()->getContents();
|
||||
} catch (\Exception $e) {
|
||||
$errorDetails = $e instanceof RequestException
|
||||
? $e->getResponse()?->getBody()->getContents()
|
||||
: $e->getMessage();
|
||||
$message = "Could not pull image " . $imageName . " (attempt $attempt/$maxRetries): " . $errorDetails;
|
||||
if ($attempt === $maxRetries) {
|
||||
if ($imageIsThere === false) {
|
||||
throw new \Exception($message);
|
||||
|
||||
Reference in New Issue
Block a user