From 97bd80971984cf6ce2b4faba1cad79fcb921e068 Mon Sep 17 00:00:00 2001 From: Martin Pitt Date: Wed, 26 Jul 2023 07:10:04 +0200 Subject: [PATCH] pybridge: Asynchronously send package files in chunks Don't synchronously send the whole document in a single channel data block. They are often quite large (especially in `NODE_ENV=development` mode, but even in production). The synchronous send_data() blocked the bridge for too long, broke flow control, and stalled parallel package channel downloads for too long. That often led to Failed to load resource: net::ERR_INCOMPLETE_CHUNKED_ENCODING browser errors. This got aggravated a lot when going through cockpit-ssh (as it happens on our OSTree images with the cockpit/ws container), but even occasionally happened with the standard setup. Send them in 4K blocks instead, like the C bridge does. Use the same threading approach as our http-stream2 channel to avoid blocking. --- src/cockpit/channels/packages.py | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/src/cockpit/channels/packages.py b/src/cockpit/channels/packages.py index 7dd61c8a1cb..4d0fcdd5bd8 100644 --- a/src/cockpit/channels/packages.py +++ b/src/cockpit/channels/packages.py @@ -15,7 +15,9 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import asyncio import logging +import threading from typing import Dict, Optional from .. import data @@ -36,6 +38,8 @@ def http_error(self, status: int, message: str) -> None: template = data.read_cockpit_data_file('fail.html') self.send_message(status=status, reason='ERROR', headers={'Content-Type': 'text/html; charset=utf-8'}) self.send_data(template.replace(b'@@message@@', message.encode('utf-8'))) + self.done() + self.close() def do_open(self, options: Dict[str, object]) -> None: self.ready() @@ -93,9 +97,6 @@ def do_done(self) -> None: out_headers['Content-Security-Policy'] = policy - self.send_message(status=200, reason='OK', headers=out_headers) - self.send_data(document.data) - except ValueError as exc: self.http_error(400, str(exc)) @@ -105,5 +106,16 @@ def do_done(self) -> None: except OSError as exc: self.http_error(500, f'Internal error: {exc!s}') - self.done() - self.close() + else: + self.send_message(status=200, reason='OK', headers=out_headers) + threading.Thread(args=(asyncio.get_running_loop(), document.data), + target=self.send_document_data, + daemon=True).start() + + def send_document_data(self, loop, data): + # split data into 4K blocks, to not overwhelm the channel + block_size = 4096 + for i in range(0, len(data), block_size): + loop.call_soon_threadsafe(self.send_data, data[i:i + block_size]) + loop.call_soon_threadsafe(self.done) + loop.call_soon_threadsafe(self.close)