-
-
Notifications
You must be signed in to change notification settings - Fork 2.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Improve resource caching #973
Changes from all commits
3ba3822
608c0d7
d76804d
be23600
9c3f2fe
daf8d63
aa5e8f7
61fe077
8e6e2f2
5e61384
1933046
c16d069
47d3eb4
6aa42aa
e0847f5
d180533
c71955e
0e091f1
58afb82
f156ede
199c8da
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -24,6 +24,7 @@ | |
import dash_renderer | ||
|
||
from .dependencies import Input, Output, State | ||
from .fingerprint import build_fingerprint, check_fingerprint | ||
from .resources import Scripts, Css | ||
from .development.base_component import Component, ComponentRegistry | ||
from . import exceptions | ||
|
@@ -541,12 +542,14 @@ def _relative_url_path(relative_package_path="", namespace=""): | |
|
||
modified = int(os.stat(module_path).st_mtime) | ||
|
||
return "{}_dash-component-suites/{}/{}?v={}&m={}".format( | ||
return "{}_dash-component-suites/{}/{}".format( | ||
self.config.requests_pathname_prefix, | ||
namespace, | ||
relative_package_path, | ||
importlib.import_module(namespace).__version__, | ||
modified, | ||
build_fingerprint( | ||
relative_package_path, | ||
importlib.import_module(namespace).__version__, | ||
modified, | ||
), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Isolated the fingerprinting logic with its inverse in a easily testable file / function |
||
) | ||
|
||
srcs = [] | ||
|
@@ -676,6 +679,10 @@ def _generate_meta_html(self): | |
|
||
# Serve the JS bundles for each package | ||
def serve_component_suites(self, package_name, path_in_package_dist): | ||
path_in_package_dist, has_fingerprint = check_fingerprint( | ||
path_in_package_dist | ||
) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If the resource was requested with the correct pattern (from dash filling the html page or from async resources override) - use cache-control, otherwise use Etag |
||
|
||
if package_name not in self.registered_paths: | ||
raise exceptions.DependencyException( | ||
"Error loading dependency.\n" | ||
|
@@ -711,11 +718,28 @@ def serve_component_suites(self, package_name, path_in_package_dist): | |
package.__path__, | ||
) | ||
|
||
return flask.Response( | ||
response = flask.Response( | ||
pkgutil.get_data(package_name, path_in_package_dist), | ||
mimetype=mimetype, | ||
) | ||
|
||
if has_fingerprint: | ||
# Fingerprinted resources are good forever (1 year) | ||
# No need for ETag as the fingerprint changes with each build | ||
response.cache_control.max_age = 31536000 # 1 year | ||
else: | ||
# Non-fingerprinted resources are given an ETag that | ||
# will be used / check on future requests | ||
response.add_etag() | ||
tag = response.get_etag()[0] | ||
|
||
request_etag = flask.request.headers.get('If-None-Match') | ||
|
||
if '"{}"'.format(tag) == request_etag: | ||
response = flask.Response(None, status=304) | ||
|
||
return response | ||
|
||
def index(self, *args, **kwargs): # pylint: disable=unused-argument | ||
scripts = self._generate_scripts_html() | ||
css = self._generate_css_dist_html() | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
import re | ||
|
||
build_regex = re.compile(r'^(?P<filename>[\w@-]+)(?P<extension>.*)$') | ||
|
||
check_regex = re.compile( | ||
r'^(?P<filename>.*)[.]v[\w-]+m[0-9a-fA-F]+(?P<extension>(?:(?:(?<![.])[.])?[\w])+)$' | ||
) | ||
|
||
|
||
def build_fingerprint(path, version, hash_value): | ||
res = build_regex.match(path) | ||
|
||
return '{}.v{}m{}{}'.format( | ||
res.group('filename'), | ||
str(version).replace('.', '_'), | ||
hash_value, | ||
res.group('extension'), | ||
) | ||
|
||
|
||
def check_fingerprint(path): | ||
# Check if the resource has a fingerprint | ||
res = check_regex.match(path) | ||
|
||
# Resolve real resource name from fingerprinted resource path | ||
return ( | ||
res.group('filename') + res.group('extension') | ||
if res is not None | ||
else path, | ||
res is not None, | ||
) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
|
||
from dash.fingerprint import build_fingerprint, check_fingerprint | ||
|
||
version = 1 | ||
hash_value = 1 | ||
|
||
valid_resources = [ | ||
{'path': 'react@16.8.6.min.js', 'fingerprint': 'react@16.v1m1.8.6.min.js'}, | ||
{'path': 'react@16.8.6.min.js', 'fingerprint': 'react@16.v1_1_1m1234567890abcdef.8.6.min.js', 'version': '1.1.1', 'hash': '1234567890abcdef' }, | ||
{'path': 'react@16.8.6.min.js', 'fingerprint': 'react@16.v1_1_1-alpha_1m1234567890abcdef.8.6.min.js', 'version': '1.1.1-alpha.1', 'hash': '1234567890abcdef' }, | ||
{'path': 'dash.plotly.js', 'fingerprint': 'dash.v1m1.plotly.js'}, | ||
{'path': 'dash.plotly.j_s', 'fingerprint': 'dash.v1m1.plotly.j_s'}, | ||
{'path': 'dash.plotly.css', 'fingerprint': 'dash.v1m1.plotly.css'}, | ||
{'path': 'dash.plotly.xxx.yyy.zzz', 'fingerprint': 'dash.v1m1.plotly.xxx.yyy.zzz'} | ||
] | ||
|
||
valid_fingerprints = [ | ||
'react@16.v1_1_2m1571771240.8.6.min.js', | ||
'dash.plotly.v1_1_1m1234567890.js', | ||
'dash.plotly.v1_1_1m1234567890.j_s', | ||
'dash.plotly.v1_1_1m1234567890.css', | ||
'dash.plotly.v1_1_1m1234567890.xxx.yyy.zzz', | ||
'dash.plotly.v1_1_1-alpha1m1234567890.js', | ||
'dash.plotly.v1_1_1-alpha_3m1234567890.js', | ||
'dash.plotly.v1_1_1m1234567890123.js', | ||
'dash.plotly.v1_1_1m4bc3.js' | ||
] | ||
|
||
invalid_fingerprints = [ | ||
'dash.plotly.v1_1_1m1234567890..js', | ||
'dash.plotly.v1_1_1m1234567890.', | ||
'dash.plotly.v1_1_1m1234567890..', | ||
'dash.plotly.v1_1_1m1234567890.js.', | ||
'dash.plotly.v1_1_1m1234567890.j-s' | ||
] | ||
|
||
def test_fingerprint(): | ||
for resource in valid_resources: | ||
# The fingerprint matches expectations | ||
fingerprint = build_fingerprint(resource.get('path'), resource.get('version', version), resource.get('hash', hash_value)) | ||
assert fingerprint == resource.get('fingerprint') | ||
|
||
(original_path, has_fingerprint) = check_fingerprint(fingerprint) | ||
# The inverse operation returns that the fingerprint was valid and the original path | ||
assert has_fingerprint | ||
assert original_path == resource.get('path') | ||
|
||
for resource in valid_fingerprints: | ||
(_, has_fingerprint) = check_fingerprint(resource) | ||
assert has_fingerprint | ||
|
||
for resource in invalid_fingerprints: | ||
(_, has_fingerprint) = check_fingerprint(resource) | ||
assert not has_fingerprint |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't fingerprint if using an external resource!