From 62ad60f3dfd0b5770f71a6da5a8e3b89c9e409b3 Mon Sep 17 00:00:00 2001 From: Lukas Rosenstock Date: Wed, 14 Oct 2026 16:03:21 +0000 Subject: [PATCH] Use TTL for in-memory cache to support long-lived instances --- CloudObjects/SDK/ObjectRetriever.php | 47 +++++++++++++++---- docs/caching.md | 18 +++++++ .../OnlineTests/ObjectRetrieverCacheTest.php | 9 +++- 3 files changed, 63 insertions(+), 11 deletions(-) create mode 100644 docs/caching.md diff --git a/CloudObjects/SDK/ObjectRetriever.php b/CloudObjects/SDK/ObjectRetriever.php index c19aa4d..1ca7ea8 100644 --- a/CloudObjects/SDK/ObjectRetriever.php +++ b/CloudObjects/SDK/ObjectRetriever.php @@ -35,9 +35,9 @@ class ObjectRetriever implements CustomCacheAndLogInterface { private $client; private $prefix; - private $options; + private $options = []; private $cache; - private $objects; + private $objects = []; private $defaultReader; private $customCacheDefaultRequest; @@ -245,12 +245,16 @@ class ObjectRetriever implements CustomCacheAndLogInterface { $uriString = (string)$coid; - if (isset($this->objects[$uriString])) - // Return from in-memory cache if it exists - return $this->objects[$uriString]; + $this->removeExpiredFromInMemoryCache(); $ts = microtime(true); + if (isset($this->objects[$uriString])) { + // Return from in-memory cache if it exists + $this->logInfoWithTime('Returning previous node for <'.$uriString.'>.', $ts); + return $this->objects[$uriString][1]; + } + if (isset($this->options['static_config_path'])) { $location = realpath($this->options['static_config_path'].DIRECTORY_SEPARATOR. $coid->getHost().str_replace('/', DIRECTORY_SEPARATOR, $coid->getPath()) @@ -287,8 +291,8 @@ class ObjectRetriever implements CustomCacheAndLogInterface { } $document = JsonLD::getDocument($object); - $this->objects[$uriString] = $document->getGraph()->getNode($uriString); - return $this->objects[$uriString]; + $this->objects[$uriString] = [ time(), $document->getGraph()->getNode($uriString) ]; + return $this->objects[$uriString][1]; } /** @@ -325,7 +329,7 @@ class ObjectRetriever implements CustomCacheAndLogInterface { if (!COIDParser::isValidCOID($iri)) continue; if ($iri->getHost() != $namespaceCoid->getHost()) continue; - $this->objects[$object->getId()] = $object; + $this->objects[$object->getId()] = [ time(), $object ]; $this->putIntoCache($object->getId(), $object, $this->options['cache_ttl']); $allIris[] = $iri; } @@ -366,7 +370,7 @@ class ObjectRetriever implements CustomCacheAndLogInterface { if (!COIDParser::isValidCOID($iri)) continue; if ($iri->getHost() != $namespaceCoid->getHost()) continue; - $this->objects[$object->getId()] = $object; + $this->objects[$object->getId()] = [ time(), $object ]; $this->putIntoCache($object->getId(), $object, $this->options['cache_ttl']); $allIris[] = $iri; } @@ -441,7 +445,7 @@ class ObjectRetriever implements CustomCacheAndLogInterface { $document = JsonLD::getDocument((string)$response->getBody()); $containerNode = $document->getGraph()->getNode('co-namespace-members://'.$namespaceCoid->getHost()); - $reader = new NodeReader([ 'prefixes' => [ 'rdfs' => 'http://www.w3.org/2000/01/rdf-schema#' ]]); + $reader = new NodeReader([ 'prefixes' => [ 'rdfs' => 'http://www.w3.org/2000/01/rdf-schema#' ]]); $allIris = $reader->getAllValuesIRI($containerNode, 'rdfs:member'); $this->logInfoWithTime('Fetched object list with <'.$type.'> for <'.$namespaceCoid->getHost().'> from Core API ['.$response->getStatusCode().'].', $ts); @@ -560,4 +564,27 @@ class ObjectRetriever implements CustomCacheAndLogInterface { return $this->getCloudObject($this->assertAuthenticatingNamespaceAndGetId()); } + /** + * Remove expired objects from the in-memory cache. Does not affect external cache. + */ + public function removeExpiredFromInMemoryCache() { + $expiryThreshold = time() - $this->options['cache_ttl']; + + foreach ($this->objects as $uriString => $entry) { + if ($entry[0] < $expiryThreshold) { + unset($this->objects[$uriString]); + } + } + + return $this; + } + + /** + * Remove all entries from the in-memory cache. Does not affect external cache. + */ + public function clearInMemoryCache() { + $this->objects = []; + return $this; + } + } \ No newline at end of file diff --git a/docs/caching.md b/docs/caching.md new file mode 100644 index 0000000..a6decc5 --- /dev/null +++ b/docs/caching.md @@ -0,0 +1,18 @@ +## Caching + +# Standard Caching + +Object descriptions that are requested through the ObjectRetriever are fetched through the CloudObjects Core Object API. They are cached in-memory inside the class for a default of 60 seconds. This can be changed using the `cache_ttl` option, which takes a number of seconds. + +This in-class memory obviously only works as long as the class is alive and prevents subsequent requests for the same object hitting the API, but the cache will not exist between multiple requests handled in different processes. For this purpose, an external cache should be added. + +To add an external cache, provide a storage using the `cache_storage` interface. The storage must be a class that implements either of the following interfaces: + +- `Kevinrob\GuzzleCache\Storage\CacheStorageInterface` +- `Psr\Cache\CacheItemPoolInterface` +- `Psr\SimpleCache\CacheInterface` + +External storage is used twice: + +- Storing the HTTP request according to the APIs caching headers. +- Storing the object data according to `cache_ttl`. \ No newline at end of file diff --git a/tests/OnlineTests/ObjectRetrieverCacheTest.php b/tests/OnlineTests/ObjectRetrieverCacheTest.php index 9b3d78c..63564ad 100644 --- a/tests/OnlineTests/ObjectRetrieverCacheTest.php +++ b/tests/OnlineTests/ObjectRetrieverCacheTest.php @@ -29,19 +29,26 @@ class ObjectRetrieverCacheTest extends \PHPUnit\Framework\TestCase { $this->assertNotNull($cacheStorage->fetch($retriever->getCacheKey((string)$coid))); $this->assertStringContainsString('from Core API', $logger->getLastLogMessage()); + // Get from same retriever, assert in-memory-cache + $object2 = $retriever->getCloudObject($coid); + + $this->assertNotNull($object1); + $this->assertStringContainsString('previous', $logger->getLastLogMessage()); + // Reinitialize retriever with same cache storage to verify that cache is used $retriever = new ObjectRetriever([ 'cache_storage' => $cacheStorage, 'logger' => $logger ]); - $object2 = $retriever->getCloudObject($coid); + $object3 = $retriever->getCloudObject($coid); $this->assertNotNull($object2); $this->assertNotNull($cacheStorage->fetch($retriever->getCacheKey((string)$coid))); $this->assertStringContainsString('from object cache', $logger->getLastLogMessage()); $this->assertEquals($object1->getRevision(), $object2->getRevision()); + $this->assertEquals($object2->getRevision(), $object3->getRevision()); } }