From 5876627a64fc398e3f31306d18892edb69ffd18c Mon Sep 17 00:00:00 2001 From: Tim Otten Date: Mon, 21 Jan 2019 02:06:21 -0800 Subject: [PATCH] Implement CRM_Utils_Cache_ArrayDecorator This allows you to put a static array in front of another cache. It is the same basic idea as CRM_Utils_Cache_Tiered, but it's optimized for a typical case where you only want one front-cache. Based on some naive benchmarking (performing several trials with a few thousand duplicate reads over the same cached data), this basically cut the read-time in half. The following is pretty representative of the results: ``` Redis-only cache write=0.1044s read=1.3266s 2-Tier (ArrayCache+Redis) write=0.1189s read=0.3765s Decorated-Redis cache write=0.1105s read=0.1505s ``` See also: https://gist.github.com/totten/6d6524be115c193e0704ff3cf250336d Note: To ensure that TTL data is respected consistently regardless of how the tiers behave and the order in which they are used, the TTL/expiration must be stored extra times. --- CRM/Utils/Cache/ArrayDecorator.php | 139 ++++++++++++++++++ .../phpunit/E2E/Cache/ArrayDecoratorTest.php | 83 +++++++++++ 2 files changed, 222 insertions(+) create mode 100644 CRM/Utils/Cache/ArrayDecorator.php create mode 100644 tests/phpunit/E2E/Cache/ArrayDecoratorTest.php diff --git a/CRM/Utils/Cache/ArrayDecorator.php b/CRM/Utils/Cache/ArrayDecorator.php new file mode 100644 index 0000000000..b3c7e091c3 --- /dev/null +++ b/CRM/Utils/Cache/ArrayDecorator.php @@ -0,0 +1,139 @@ + mixed $cacheValue). + */ + private $values = []; + + /** + * @var array + * Array(string $cacheKey => int $expirationTime). + */ + private $expires = []; + + /** + * CRM_Utils_Cache_ArrayDecorator constructor. + * @param \CRM_Utils_Cache_Interface $delegate + * @param int $defaultTimeout + * Default number of seconds each cache-item should endure. + */ + public function __construct(\CRM_Utils_Cache_Interface $delegate, $defaultTimeout = 3600) { + $this->defaultTimeout = $defaultTimeout; + $this->delegate = $delegate; + } + + public function set($key, $value, $ttl = NULL) { + if (is_int($ttl) && $ttl <= 0) { + return $this->delete($key); + } + + $expiresAt = CRM_Utils_Date::convertCacheTtlToExpires($ttl, $this->defaultTimeout); + if ($this->delegate->set($key, [$expiresAt, $value], $ttl)) { + $this->values[$key] = $this->reobjectify($value); + $this->expires[$key] = $expiresAt; + return TRUE; + } + else { + return FALSE; + } + } + + public function get($key, $default = NULL) { + if (array_key_exists($key, $this->values) && $this->expires[$key] > CRM_Utils_Time::getTimeRaw()) { + return $this->reobjectify($this->values[$key]); + } + + $nack = CRM_Utils_Cache::nack(); + $value = $this->delegate->get($key, $nack); + if ($value === $nack) { + return $default; + } + + $this->expires[$key] = $value[0]; + $this->values[$key] = $value[1]; + return $this->reobjectify($this->values[$key]); + } + + public function delete($key) { + unset($this->values[$key]); + unset($this->expires[$key]); + return $this->delegate->delete($key); + } + + public function flush() { + return $this->clear(); + } + + public function clear() { + $this->values = []; + $this->expires = []; + return $this->delegate->clear(); + } + + public function has($key) { + if (array_key_exists($key, $this->values) && $this->expires[$key] > CRM_Utils_Time::getTimeRaw()) { + return TRUE; + } + return $this->delegate->has($key); + } + + private function reobjectify($value) { + return is_object($value) ? unserialize(serialize($value)) : $value; + } + +} diff --git a/tests/phpunit/E2E/Cache/ArrayDecoratorTest.php b/tests/phpunit/E2E/Cache/ArrayDecoratorTest.php new file mode 100644 index 0000000000..1dec75a00c --- /dev/null +++ b/tests/phpunit/E2E/Cache/ArrayDecoratorTest.php @@ -0,0 +1,83 @@ +a = CRM_Utils_Cache::create([ + 'name' => 'e2e array-dec test', + 'type' => ['ArrayCache'], + ]) + ); + } + + public function testDoubleLifeWithDelete() { + $this->assertFalse($this->a->has('foo')); + $this->assertEquals('dfl-1', $this->a->get('foo', 'dfl-1')); + + $this->cache->set('foo', 100); + + $this->assertTrue($this->a->has('foo')); + $this->assertEquals(100, $this->a->get('foo', 'dfl-1')[1]); + + $this->cache->set('foo', 200); + + $this->assertTrue($this->a->has('foo')); + $this->assertEquals(200, $this->a->get('foo', 'dfl-1')[1]); + + $this->cache->delete('foo'); + + $this->assertFalse($this->a->has('foo')); + $this->assertEquals('dfl-1', $this->a->get('foo', 'dfl-1')); + } + + public function testDoubleLifeWithClear() { + $this->assertFalse($this->a->has('foo')); + $this->assertEquals('dfl-1', $this->a->get('foo', 'dfl-1')); + + $this->cache->set('foo', 100); + + $this->assertTrue($this->a->has('foo')); + $this->assertEquals(100, $this->a->get('foo', 'dfl-1')[1]); + + $this->cache->clear(); + + $this->assertFalse($this->a->has('foo')); + $this->assertEquals('dfl-1', $this->a->get('foo', 'dfl-1')); + } + +} -- 2.25.1