fix: enable consistent etag across workers and force no-cache for dashboards (#11137)

* fix: enable consistent etag across workers

* Use CACHE_DEFAULT_TIMEOUT instead of 0

* Change timeout to 0 and set expires header even for no-cache

* Reduce number of if branches to appease Pylint

* Fix mypy error
This commit is contained in:
Jesse Yang
2020-10-06 09:43:43 -07:00
committed by GitHub
parent 3d7b805755
commit 38dccf9e12
2 changed files with 29 additions and 13 deletions

View File

@@ -51,6 +51,7 @@ def etag_cache(
check_perms: Callable[..., Any],
get_last_modified: Optional[Callable[..., Any]] = None,
skip: Optional[Callable[..., Any]] = None,
must_revalidate: Optional[bool] = False,
) -> Callable[..., Any]:
"""
A decorator for caching views and handling etag conditional requests.
@@ -78,6 +79,8 @@ def etag_cache(
return f(*args, **kwargs)
response = None
last_modified = get_last_modified and get_last_modified(*args, **kwargs)
if cache:
try:
# build the cache key from the function arguments and any
@@ -94,32 +97,37 @@ def etag_cache(
raise
logger.exception("Exception possibly due to cache backend.")
# if cache is stale?
if get_last_modified:
content_changed_time = get_last_modified(*args, **kwargs)
# if cache is stale?
if (
response
and last_modified
and response.last_modified
and response.last_modified.timestamp()
< content_changed_time.timestamp()
and response.last_modified < last_modified
):
response = None
else:
# if caller didn't provide content's last_modified time, assume
# its cache won't be stale.
content_changed_time = datetime.utcnow()
# if no response was cached, compute it using the wrapped function
if response is None:
# if no response was cached, compute it using the wrapped function
response = f(*args, **kwargs)
# add headers for caching: Last Modified, Expires and ETag
response.cache_control.public = True
response.last_modified = content_changed_time
# set expiration headers:
# Last-Modified, Expires, Cache-Control, ETag
response.last_modified = last_modified or datetime.utcnow()
expiration = max_age if max_age != 0 else FAR_FUTURE
response.expires = response.last_modified + timedelta(
seconds=expiration
)
# when needed, instruct the browser to always revalidate cache
if must_revalidate:
# `Cache-Control: no-cache` asks the browser to always store
# the cache, but also must validate it with the server.
response.cache_control.no_cache = True
else:
# `Cache-Control: Public` asks the browser to always store
# the cache.
response.cache_control.public = True
response.add_etag()
# if we have a cache, store the response from the request

View File

@@ -171,6 +171,13 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods
logger = logging.getLogger(__name__)
def __repr__(self) -> str:
"""Determinate string representation of the view instance for etag_cache."""
return "Superset.views.core.Superset@v{}{}".format(
self.appbuilder.app.config["VERSION_STRING"],
self.appbuilder.app.config["VERSION_SHA"],
)
@has_access_api
@expose("/datasources/")
def datasources(self) -> FlaskResponse:
@@ -1605,6 +1612,7 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods
skip=lambda _self, dashboard_id_or_slug: not is_feature_enabled(
"ENABLE_DASHBOARD_ETAG_HEADER"
),
must_revalidate=True,
)
@expose("/dashboard/<dashboard_id_or_slug>/")
def dashboard( # pylint: disable=too-many-locals