From 05653e294d57b37352882358b0587451b0a03f0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Leuilliot?= Date: Sat, 30 Mar 2019 20:20:41 +0100 Subject: [PATCH] feat(cache): Add ETag support into the cache engine (#34) Upgrade Eseye design to handle ETag in its cache engine. Closes eveseat/eseye#33 --- src/Cache/FileCache.php | 4 +-- src/Cache/MemcachedCache.php | 3 +- src/Cache/RedisCache.php | 3 +- src/Containers/EsiResponse.php | 42 +++++++++++++++++++++++---- src/Eseye.php | 47 ++++++++++++++++++++++++------- src/Fetchers/FetcherInterface.php | 3 ++ 6 files changed, 83 insertions(+), 19 deletions(-) diff --git a/src/Cache/FileCache.php b/src/Cache/FileCache.php index 78b4341..ff23ff6 100644 --- a/src/Cache/FileCache.php +++ b/src/Cache/FileCache.php @@ -179,8 +179,8 @@ public function get(string $uri, string $query = '') // Get the data from the file and unserialize it $file = unserialize(file_get_contents($cache_file_path)); - // If the cached entry is expired, remove it. - if ($file->expired()) { + // If the cached entry is expired and does not have any ETag, remove it. + if ($file->expired() && ! $file->hasHeader('ETag')) { $this->forget($uri, $query); diff --git a/src/Cache/MemcachedCache.php b/src/Cache/MemcachedCache.php index be45877..f2d6f79 100644 --- a/src/Cache/MemcachedCache.php +++ b/src/Cache/MemcachedCache.php @@ -133,7 +133,8 @@ public function get(string $uri, string $query = '') $data = unserialize($value); - if ($data->expired()) { + // If the cached entry is expired and does not have any ETag, remove it. + if ($data->expired() && ! $data->hasHeader('ETag')) { $this->forget($uri, $query); return false; diff --git a/src/Cache/RedisCache.php b/src/Cache/RedisCache.php index 7f409f7..b76237c 100644 --- a/src/Cache/RedisCache.php +++ b/src/Cache/RedisCache.php @@ -109,7 +109,8 @@ public function get(string $uri, string $query = '') $data = unserialize($this->redis ->get($this->buildCacheKey($uri, $query))); - if ($data->expired()) { + // If the cached entry is expired and does not have any ETag, remove it. + if ($data->expired() && ! $data->hasHeader('ETag')) { $this->forget($uri, $query); diff --git a/src/Containers/EsiResponse.php b/src/Containers/EsiResponse.php index 3f2f45e..16c0772 100644 --- a/src/Containers/EsiResponse.php +++ b/src/Containers/EsiResponse.php @@ -109,7 +109,7 @@ public function __construct( $this->expires_at = strlen($expires) > 2 ? $expires : 'now'; $this->response_code = $response_code; - // If there is an error, set that + // If there is an error, set that. if (property_exists($data, 'error')) $this->error_message = $data->error; @@ -152,10 +152,10 @@ private function parseHeaders(array $headers) // Check for some header values that might be interesting // such as the current error limit and number of pages // available. - array_key_exists('X-Esi-Error-Limit-Remain', $headers) ? - $this->error_limit = (int) $headers['X-Esi-Error-Limit-Remain'] : null; + $this->hasHeader('X-Esi-Error-Limit-Remain') ? + $this->error_limit = (int) $this->getHeader('X-Esi-Error-Limit-Remain') : null; - array_key_exists('X-Pages', $headers) ? $this->pages = (int) $headers['X-Pages'] : null; + $this->hasHeader('X-Pages') ? $this->pages = (int) $this->getHeader('X-Pages') : null; } /** @@ -228,7 +228,7 @@ public function getErrorCode(): int /** * @return bool */ - public function setIsCachedload(): bool + public function setIsCachedLoad(): bool { return $this->cached_load = true; @@ -242,4 +242,36 @@ public function isCachedLoad(): bool return $this->cached_load; } + + /** + * @param string $name + * @return bool + */ + public function hasHeader(string $name) + { + // turn headers into case insensitive array + $key_map = array_change_key_case($this->headers, CASE_LOWER); + + // track for the requested header name + return array_key_exists(strtolower($name), $key_map); + } + + /** + * @param string $name + * @return mixed|null + */ + public function getHeader(string $name) + { + // turn header name into case insensitive + $insensitive_key = strtolower($name); + + // turn headers into case insensitive array + $key_map = array_change_key_case($this->headers, CASE_LOWER); + + // track for the requested header name and return its value if exists + if (array_key_exists($insensitive_key, $key_map)) + return $key_map[$insensitive_key]; + + return null; + } } diff --git a/src/Eseye.php b/src/Eseye.php index ad7e218..92fedef 100644 --- a/src/Eseye.php +++ b/src/Eseye.php @@ -206,8 +206,10 @@ public function setBody(array $body): self * * @return \Seat\Eseye\Containers\EsiResponse * @throws \Seat\Eseye\Exceptions\EsiScopeAccessDeniedException - * @throws \Seat\Eseye\Exceptions\UriDataMissingException + * @throws \Seat\Eseye\Exceptions\RequestFailedException + * @throws \Seat\Eseye\Exceptions\InvalidAuthenticationException * @throws \Seat\Eseye\Exceptions\InvalidContainerDataException + * @throws \Seat\Eseye\Exceptions\UriDataMissingException */ public function invoke(string $method, string $uri, array $uri_data = []): EsiResponse { @@ -235,17 +237,39 @@ public function invoke(string $method, string $uri, array $uri_data = []): EsiRe $cached = $this->getCache()->get($uri->getPath(), $uri->getQuery()) ) { - // Mark the response as one that was loaded from the cache - $cached->setIsCachedload(); + // Mark the response as one that was loaded from the cache in case no ETag exists + if (! $cached->hasHeader('ETag')) + $cached->setIsCachedLoad(); + + // Handling ETag marked response specifically (ignoring the expired time) + // Sending a request with the stored ETag in header - if we have a 304 response, data has not been altered. + if ($cached->hasHeader('ETag') && $cached->expired()) { + + $result = $this->rawFetch($method, $uri, $this->getBody(), ['If-None-Match' => $cached->getHeader('ETag')]); + + if ($result->getErrorCode() == 304) + $cached->setIsCachedLoad(); + } + + // In case the result is effectively retrieved from cache, + // return the cached element. + if ($cached->isCachedLoad()) { + + // Perform some debug logging + $logging_msg = 'Loaded cached response for ' . $method . ' -> ' . $uri; - // Perform some debug logging - $this->getLogger()->debug('Loaded cached response for ' . $method . ' -> ' . $uri); + if ($cached->hasHeader('ETag')) + $logging_msg = sprintf('%s [%s]', $logging_msg, $cached->getHeader('ETag')); - return $cached; + $this->getLogger()->debug($logging_msg); + + return $cached; + } } - // Call ESI itself and get the EsiResponse - $result = $this->rawFetch($method, $uri, $this->getBody()); + // Call ESI itself and get the EsiResponse in case it has not already been handled with cache control + if (! isset($result)) + $result = $this->rawFetch($method, $uri, $this->getBody()); // Cache the response if it was a get and is not already expired if (in_array(strtolower($method), $this->cachable_verb) && ! $result->expired()) @@ -431,14 +455,17 @@ private function getCache(): CacheInterface * @param string $method * @param string $uri * @param array $body + * @param array $headers * * @return mixed + * @throws \Seat\Eseye\Exceptions\InvalidAuthenticationException + * @throws \Seat\Eseye\Exceptions\RequestFailedException * @throws \Seat\Eseye\Exceptions\InvalidContainerDataException */ - public function rawFetch(string $method, string $uri, array $body) + public function rawFetch(string $method, string $uri, array $body, array $headers = []) { - return $this->getFetcher()->call($method, $uri, $body); + return $this->getFetcher()->call($method, $uri, $body, $headers); } /** diff --git a/src/Fetchers/FetcherInterface.php b/src/Fetchers/FetcherInterface.php index b8b6eb5..e7e7b86 100644 --- a/src/Fetchers/FetcherInterface.php +++ b/src/Fetchers/FetcherInterface.php @@ -37,6 +37,9 @@ interface FetcherInterface * @param array $headers * * @return \Seat\Eseye\Containers\EsiResponse + * @throws \Seat\Eseye\Exceptions\InvalidAuthenticationException + * @throws \Seat\Eseye\Exceptions\RequestFailedException + * @throws \Seat\Eseye\Exceptions\InvalidContainerDataException */ public function call(string $method, string $uri, array $body, array $headers = []): EsiResponse;