app: prevent static file reads from blocking the event loop

Perform reads in a thread so File I/O does not block.

This patch also disables ETags for static files.  Tornado's default behavior of caching file hashes will not work as many of Moonraker's can be updated.  The previous workaround to this was to recalculate the checksum if the modified date changed.  This is inefficient  as its behavior is not much different than using "If-Modified-Since".

Signed-off-by:  Eric Callahan <arksine.code@gmail.com>
This commit is contained in:
Eric Callahan 2021-06-28 07:31:40 -04:00
parent 3529acfeec
commit 00f4bd594f
1 changed files with 34 additions and 29 deletions

View File

@ -9,7 +9,6 @@ import os
import mimetypes
import logging
import json
import datetime
from concurrent.futures import ThreadPoolExecutor
import traceback
import ssl
@ -39,6 +38,7 @@ from typing import (
Union,
Dict,
List,
AsyncGenerator,
)
if TYPE_CHECKING:
from tornado.httpserver import HTTPServer
@ -628,6 +628,7 @@ class FileRequestHandler(AuthorizedFileHandler):
self.modified = self.get_modified_time()
self.set_headers()
self.request.headers.pop("If-None-Match", None)
if self.should_return_304():
self.set_status(304)
return
@ -682,6 +683,7 @@ class FileRequestHandler(AuthorizedFileHandler):
elif end is not None:
content_length = end
elif start is not None:
end = size
content_length = size - start
else:
end = size
@ -689,10 +691,8 @@ class FileRequestHandler(AuthorizedFileHandler):
self.set_header("Content-Length", content_length)
if include_body:
content = self.get_content(self.absolute_path, start, end)
if isinstance(content, bytes):
content = [content]
for chunk in content:
content = self.get_content_nonblock(self.absolute_path, start, end)
async for chunk in content:
try:
self.write(chunk)
await self.flush()
@ -707,32 +707,37 @@ class FileRequestHandler(AuthorizedFileHandler):
def _escape_filename_to_utf8(self, basename: str) -> str:
return urllib.parse.quote(basename, encoding="utf-8")
@classmethod
async def get_content_nonblock(
cls, abspath: str, start: Optional[int] = None,
end: Optional[int] = None
) -> AsyncGenerator[bytes, None]:
ioloop = IOLoop.current()
with open(abspath, "rb") as file:
if start is not None:
file.seek(start)
if end is not None:
remaining = end - (start or 0) # type: Optional[int]
else:
remaining = None
while True:
chunk_size = 64 * 1024
if remaining is not None and remaining < chunk_size:
chunk_size = remaining
with ThreadPoolExecutor(max_workers=1) as tpe:
chunk = await ioloop.run_in_executor(
tpe, file.read, chunk_size)
if chunk:
if remaining is not None:
remaining -= len(chunk)
yield chunk
else:
if remaining is not None:
assert remaining == 0
return
@classmethod
def _get_cached_version(cls, abs_path: str) -> Optional[str]:
with cls._lock:
hashes: Dict[str, Dict[str, Any]] = \
cls._static_hashes # type: ignore
try:
mtime = datetime.datetime.fromtimestamp(
os.path.getmtime(abs_path), tz=datetime.timezone.utc)
except Exception:
logging.exception(
f"Unable to get modified time for file: {abs_path}")
hashes.pop(abs_path, None)
return None
if abs_path not in hashes or mtime != hashes[abs_path]['modified']:
try:
hashes[abs_path] = {
'modified': mtime,
'hash': cls.get_content_version(abs_path)
}
except Exception:
logging.exception(f"Could not open static file {abs_path}")
hashes.pop(abs_path, None)
return None
hsh = hashes.get(abs_path, {}).get('hash')
if hsh:
return hsh
return None
@tornado.web.stream_request_body