Skip to content

Commit

Permalink
pybridge: Asynchronously send package files in chunks
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
martinpitt committed Jul 26, 2023
1 parent 30b93c6 commit 97bd809
Showing 1 changed file with 17 additions and 5 deletions.
22 changes: 17 additions & 5 deletions src/cockpit/channels/packages.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.

import asyncio
import logging
import threading
from typing import Dict, Optional

from .. import data
Expand All @@ -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()
Expand Down Expand Up @@ -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))

Expand All @@ -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)

0 comments on commit 97bd809

Please sign in to comment.