From 012953102a112f00cebec88f4415e14f25706b21 Mon Sep 17 00:00:00 2001 From: Martin Redolatti Date: Fri, 3 Nov 2023 11:27:42 -0300 Subject: [PATCH] request-scoped evaluation cache implementation --- CHANGES | 3 + src/Client.php | 41 ++++++++- src/Config/Utils.php | 27 +++++- src/Factory.php | 25 +++++- src/Utils/EvalCache/Cache.php | 15 ++++ src/Utils/EvalCache/CacheImpl.php | 82 ++++++++++++++++++ src/Utils/EvalCache/Entry.php | 32 +++++++ src/Utils/EvalCache/InputHasher.php | 8 ++ .../EvalCache/KeyAttributeCRC32Hasher.php | 16 ++++ src/Utils/EvalCache/KeyOnlyHasher.php | 11 +++ src/Utils/EvalCache/NoCache.php | 50 +++++++++++ src/Version.php | 2 +- tests/ClientTest.php | 84 +++++++++++++++++++ tests/Config/UtilsTest.php | 9 +- tests/FactoryTest.php | 2 +- tests/Link/Transfer/UnixSeqPacketTest.php | 4 +- tests/Link/Transfer/UnixStreamTest.php | 5 +- .../SocketServerRemoteControl.php | 3 +- tests/{Utils => TestUtils}/_server.php | 3 +- tests/Utils/EvalCache/CacheImplTest.php | 78 +++++++++++++++++ .../EvalCache/KeyAttributeCRC32HasherTest.php | 33 ++++++++ tests/Utils/EvalCache/KeyOnlyHasherTest.php | 19 +++++ tests/Utils/EvalCache/NoCacheTest.php | 43 ++++++++++ 23 files changed, 575 insertions(+), 20 deletions(-) create mode 100644 src/Utils/EvalCache/Cache.php create mode 100644 src/Utils/EvalCache/CacheImpl.php create mode 100644 src/Utils/EvalCache/Entry.php create mode 100644 src/Utils/EvalCache/InputHasher.php create mode 100644 src/Utils/EvalCache/KeyAttributeCRC32Hasher.php create mode 100644 src/Utils/EvalCache/KeyOnlyHasher.php create mode 100644 src/Utils/EvalCache/NoCache.php rename tests/{Utils => TestUtils}/SocketServerRemoteControl.php (99%) rename tests/{Utils => TestUtils}/_server.php (99%) create mode 100644 tests/Utils/EvalCache/CacheImplTest.php create mode 100644 tests/Utils/EvalCache/KeyAttributeCRC32HasherTest.php create mode 100644 tests/Utils/EvalCache/KeyOnlyHasherTest.php create mode 100644 tests/Utils/EvalCache/NoCacheTest.php diff --git a/CHANGES b/CHANGES index 295c576..1b60d40 100644 --- a/CHANGES +++ b/CHANGES @@ -1,3 +1,6 @@ +1.3.0 (TBD): +- Added in-memory evaluation cache for the duration of a request. + 1.2.0 (Sep 19, 2023): - Add support for Client/GetTreatment(s)WithConfig operations. - Add support for Manager operations. diff --git a/src/Client.php b/src/Client.php index 3e7c2cd..8ce2418 100644 --- a/src/Client.php +++ b/src/Client.php @@ -3,6 +3,8 @@ namespace SplitIO\ThinSdk; use \SplitIO\ThinSdk\Utils\ImpressionListener; +use \SplitIO\ThinSdk\Utils\EvalCache\Cache; +use \SplitIO\ThinSdk\Utils\EvalCache\NoCache; use \SplitIO\ThinSdk\Utils\InputValidator\InputValidator; use \SplitIO\ThinSdk\Utils\InputValidator\ValidationException; use \SplitIO\ThinSdk\Models\Impression; @@ -17,20 +19,27 @@ class Client implements ClientInterface private /*LoggerInterface*/ $logger; private /*?ImpressionListener*/ $impressionListener; private /*InputValidator*/ $inputValidator; + private /*Cache*/ $cache; - public function __construct(Manager $manager, LoggerInterface $logger, ?ImpressionListener $impressionListener) + public function __construct(Manager $manager, LoggerInterface $logger, ?ImpressionListener $impressionListener, ?Cache $cache = null) { $this->logger = $logger; $this->lm = $manager; $this->impressionListener = $impressionListener; $this->inputValidator = new InputValidator($logger); + $this->cache = $cache ?? new NoCache(); } public function getTreatment(string $key, ?string $bucketingKey, string $feature, ?array $attributes = null): string { try { + if (($fromCache = $this->cache->get($key, $feature, $attributes)) != null) { + return $fromCache; + } + list($treatment, $ilData) = $this->lm->getTreatment($key, $bucketingKey, $feature, $attributes); $this->handleListener($key, $bucketingKey, $feature, $attributes, $treatment, $ilData); + $this->cache->set($key, $feature, $attributes, $treatment); return $treatment; } catch (\Exception $exc) { $this->logger->error($exc); @@ -41,13 +50,21 @@ public function getTreatment(string $key, ?string $bucketingKey, string $feature public function getTreatments(string $key, ?string $bucketingKey, array $features, ?array $attributes = null): array { try { + // try to fetch items from cache. return result if all evaluations are cached + // otherwise, send a Treatments RPC for missing ones and return merged result + $toReturn = $this->cache->getMany($key, $features, $attributes); + $features = self::getMissing($toReturn); + if (count($features) == 0) { + return $toReturn; + } + $results = $this->lm->getTreatments($key, $bucketingKey, $features, $attributes); - $toReturn = []; foreach ($results as $feature => $result) { list($treatment, $ilData) = $result; $toReturn[$feature] = $treatment; $this->handleListener($key, $bucketingKey, $feature, $attributes, $treatment, $ilData); } + $this->cache->setMany($key, $attributes, $toReturn); return $toReturn; } catch (\Exception $exc) { $this->logger->error($exc); @@ -61,8 +78,14 @@ public function getTreatments(string $key, ?string $bucketingKey, array $feature public function getTreatmentWithConfig(string $key, ?string $bucketingKey, string $feature, ?array $attributes = null): array { try { + + if (($fromCache = $this->cache->getWithConfig($key, $feature, $attributes)) != null) { + return $fromCache; + } + list($treatment, $ilData, $config) = $this->lm->getTreatmentWithConfig($key, $bucketingKey, $feature, $attributes); $this->handleListener($key, $bucketingKey, $feature, $attributes, $treatment, $ilData); + $this->cache->setWithConfig($key, $feature, $attributes, $treatment, $config); return ['treatment' => $treatment, 'config' => $config]; } catch (\Exception $exc) { $this->logger->error($exc); @@ -73,13 +96,20 @@ public function getTreatmentWithConfig(string $key, ?string $bucketingKey, strin public function getTreatmentsWithConfig(string $key, ?string $bucketingKey, array $features, ?array $attributes = null): array { try { + $toReturn = $this->cache->getManyWithConfig($key, $features, $attributes); + $features = self::getMissing($toReturn); + + if (count($features) == 0) { + return $toReturn; + } + $results = $this->lm->getTreatmentsWithConfig($key, $bucketingKey, $features, $attributes); - $toReturn = []; foreach ($results as $feature => $result) { list($treatment, $ilData, $config) = $result; $toReturn[$feature] = ['treatment' => $treatment, 'config' => $config]; $this->handleListener($key, $bucketingKey, $feature, $attributes, $treatment, $ilData); } + $this->cache->setManyWithConfig($key, $attributes, $toReturn); return $toReturn; } catch (\Exception $exc) { $this->logger->error($exc); @@ -124,4 +154,9 @@ private function handleListener(string $key, ?string $bucketingKey, string $feat $this->logger->error($exc); } } + + private static function getMissing(array $results): array + { + return array_keys(array_filter($results, 'is_null')); + } } diff --git a/src/Config/Utils.php b/src/Config/Utils.php index e52dcb9..732672b 100644 --- a/src/Config/Utils.php +++ b/src/Config/Utils.php @@ -2,16 +2,21 @@ namespace SplitIO\ThinSdk\Config; -use \SplitIO\ThinSdk\Utils\ImpressionListener; +use SplitIO\ThinSdk\Utils\ImpressionListener; +use SplitIO\ThinSdk\Utils\EvalCache\InputHasher; class Utils { private /*?ImpressionListener*/ $listener; + private /*?string*/ $evaluationCache; + private /*?InputHasher*/ $customCacheHash; - private function __construct(?ImpressionListener $listener) + private function __construct(?ImpressionListener $listener, ?string $evaluationCache, ?InputHasher $customCacheHash) { $this->listener = $listener; + $this->evaluationCache = $evaluationCache; + $this->customCacheHash = $customCacheHash; } public function impressionListener(): ?ImpressionListener @@ -19,14 +24,28 @@ public function impressionListener(): ?ImpressionListener return $this->listener; } + public function evaluationCache(): ?string + { + return $this->evaluationCache; + } + + public function customCacheHash(): ?InputHasher + { + return $this->customCacheHash; + } + public static function fromArray(array $config): Utils { $d = self::default(); - return new Utils($config['impressionListener'] ?? $d->impressionListener()); + return new Utils( + $config['impressionListener'] ?? $d->impressionListener(), + $config['evaluationCache'] ?? $d->evaluationCache(), + $config['customCacheHash'] ?? $d->customCacheHash(), + ); } public static function default(): Utils { - return new Utils(null); + return new Utils(null, 'none', null); } } diff --git a/src/Factory.php b/src/Factory.php index 43373c9..bfcc2cf 100644 --- a/src/Factory.php +++ b/src/Factory.php @@ -59,11 +59,34 @@ public static function withConfig(array $config): FactoryInterface public function client(): ClientInterface { - return new Client($this->linkManager, $this->logger, $this->config->utils()->impressionListener()); + return new Client($this->linkManager, $this->logger, $this->config->utils()->impressionListener(), $this->getCache()); } public function manager(): ManagerInterface { return new Manager($this->linkManager, $this->logger); } + + private function getCache(): Utils\EvalCache\Cache + { + $uc = $this->config->utils(); + $cache = new Utils\EvalCache\NoCache(); + switch ($uc->evaluationCache()) { + case 'key-only': + $cache = new Utils\EvalCache\CacheImpl(new Utils\EvalCache\KeyOnlyHasher()); + break; + case 'key-attribute': + $cache = new Utils\EvalCache\CacheImpl(new Utils\EvalCache\KeyAttributeCRC32Hasher()); + break; + case 'custom': + if (is_null($uc->customCacheHash())) { + $this->logger->error(sprintf( + "config indicates 'custom' evaluation cache hasher, but no 'customCacheHash' passed. Cache will be disabled" + )); + } else { + $cache = new Utils\EvalCache\CacheImpl($uc->customCacheHash()); + } + } + return $cache; + } }; diff --git a/src/Utils/EvalCache/Cache.php b/src/Utils/EvalCache/Cache.php new file mode 100644 index 0000000..5087981 --- /dev/null +++ b/src/Utils/EvalCache/Cache.php @@ -0,0 +1,15 @@ +data = []; + $this->hasher = $hasher; + } + + public function get(string $key, string $feature, ?array $attributes): ?string + { + $entry = $this->_get($key, $feature, $attributes); + return ($entry != null) ? $entry->getTreatment() : null; + } + + public function getMany(string $key, array $features, ?array $attributes): array + { + $result = []; + foreach ($features as $feature) { + $result[$feature] = $this->get($key, $feature, $attributes); + } + return $result; + } + + public function getWithConfig(string $key, string $feature, ?array $attributes): ?array + { + // if the entry exists but was previously fetched without config, it's returned as null, + // so that it's properly fetched by `getTreatmentWithConfig` + $entry = $this->_get($key, $feature, $attributes); + return ($entry != null && $entry->hasConfig()) + ? ['treatment' => $entry->getTreatment(), 'config' => $entry->getConfig()] + : null; + } + + public function getManyWithConfig(string $key, array $features, ?array $attributes): array + { + $result = []; + foreach ($features as $feature) { + $result[$feature] = $this->getWithConfig($key, $feature, $attributes); + } + return $result; + } + + public function set(string $key, string $feature, ?array $attributes, string $treatment) + { + $h = $this->hasher->hashInput($key, $feature, $attributes); + $this->data[$h] = new Entry($treatment, false); + } + + public function setMany(string $key, ?array $attributes, array $results) + { + foreach ($results as $feature => $treatment) { + $this->set($key, $feature, $attributes, $treatment); + } + } + + public function setWithConfig(string $key, string $feature, ?array $attributes, string $treatment, ?string $config) + { + $h = $this->hasher->hashInput($key, $feature, $attributes); + $this->data[$h] = new Entry($treatment, true, $config); + } + + public function setManyWithConfig(string $key, ?array $attributes, array $results) + { + foreach ($results as $feature => $result) { + $this->setWithConfig($key, $feature, $attributes, $result['treatment'], $result['config']); + } + } + + private function _get(string $key, string $feature, ?array $attributes): ?Entry + { + $h = $this->hasher->hashInput($key, $feature, $attributes); + return $this->data[$h] ?? null; + } +} diff --git a/src/Utils/EvalCache/Entry.php b/src/Utils/EvalCache/Entry.php new file mode 100644 index 0000000..45827a1 --- /dev/null +++ b/src/Utils/EvalCache/Entry.php @@ -0,0 +1,32 @@ +treatment = $treatment; + $this->config = $config; + $this->hascfg = $hasConfig; + } + + public function getTreatment(): string + { + return $this->treatment; + } + + public function getConfig(): ?string + { + return $this->config; + } + + public function hasConfig(): bool + { + return $this->hascfg; + } +} diff --git a/src/Utils/EvalCache/InputHasher.php b/src/Utils/EvalCache/InputHasher.php new file mode 100644 index 0000000..ec62d81 --- /dev/null +++ b/src/Utils/EvalCache/InputHasher.php @@ -0,0 +1,8 @@ +logger, null); $this->assertEquals(false, $client->track('someKey', 'someTrafficType', 'someEventType', 1.25, ['a' => 1])); } + + public function testGetTreatmentCacheEnabled() + { + $manager = $this->createMock(Manager::class); + $manager->expects($this->once())->method('getTreatment') + ->with('someKey', 'someBuck', 'someFeature', ['someAttr' => 123]) + ->willReturn(['on', null, null]); + + $client = new Client($manager, $this->logger, null, new CacheImpl(new KeyAttributeCRC32Hasher())); + + // 2 calls, expecting only one in manager + $this->assertEquals('on', $client->getTreatment('someKey', 'someBuck', 'someFeature', ['someAttr' => 123])); + $this->assertEquals('on', $client->getTreatment('someKey', 'someBuck', 'someFeature', ['someAttr' => 123])); + } + + public function testGetTreatmentWithConfigCacheEnabled() + { + $manager = $this->createMock(Manager::class); + $manager->expects($this->once())->method('getTreatmentWithConfig') + ->with('someKey', 'someBuck', 'someFeature', ['someAttr' => 123]) + ->willReturn(['on', null, 'some']); + + $client = new Client($manager, $this->logger, null, new CacheImpl(new KeyAttributeCRC32Hasher())); + + // 2 calls to getTreatmentWithConfig, 1 to getTreatment with same input => only one call to link manager + $this->assertEquals(['treatment' => 'on', 'config' => 'some'], $client->getTreatmentWithConfig('someKey', 'someBuck', 'someFeature', ['someAttr' => 123])); + $this->assertEquals(['treatment' => 'on', 'config' => 'some'], $client->getTreatmentWithConfig('someKey', 'someBuck', 'someFeature', ['someAttr' => 123])); + $this->assertEquals('on', $client->getTreatment('someKey', 'someBuck', 'someFeature', ['someAttr' => 123])); + } + + public function testGetTreatmentsCacheEnabled() + { + $manager = $this->createMock(Manager::class); + $manager->expects($this->exactly(2))->method('getTreatments') + ->withConsecutive( + ['someKey', 'someBuck', ['f1', 'f2'], ['someAttr' => 123]], + ['someKey', 'someBuck', ['f3'], ['someAttr' => 123]], + ) + ->willReturnOnConsecutiveCalls( + ['f1' => ['on', null, null], 'f2' => ['off', null, null]], + ['f3' => ['na', null, null]], + ); + + $client = new Client($manager, $this->logger, null, new CacheImpl(new KeyAttributeCRC32Hasher())); + $this->assertEquals(['f1' => 'on', 'f2' => 'off'], $client->getTreatments('someKey', 'someBuck', ['f1', 'f2'], ['someAttr' => 123])); + $this->assertEquals(['f1' => 'on', 'f2' => 'off'], $client->getTreatments('someKey', 'someBuck', ['f1', 'f2'], ['someAttr' => 123])); + $this->assertEquals(['f1' => 'on', 'f2' => 'off', 'f3' => 'na'], $client->getTreatments('someKey', 'someBuck', ['f1', 'f2', 'f3'], ['someAttr' => 123])); + $this->assertEquals(['f1' => 'on', 'f2' => 'off', 'f3' => 'na'], $client->getTreatments('someKey', 'someBuck', ['f1', 'f2', 'f3'], ['someAttr' => 123])); + } + + public function testGetTreatmentsWithConfigCacheEnabled() + { + $manager = $this->createMock(Manager::class); + $manager->expects($this->exactly(2))->method('getTreatmentsWithConfig') + ->withConsecutive( + ['someKey', 'someBuck', ['f1', 'f2'], ['someAttr' => 123]], + ['someKey', 'someBuck', ['f3'], ['someAttr' => 123]], + ) + ->willReturnOnConsecutiveCalls( + ['f1' => ['on', null, 'some'], 'f2' => ['off', null, null]], + ['f3' => ['na', null, 'another']], + ); + + $client = new Client($manager, $this->logger, null, new CacheImpl(new KeyAttributeCRC32Hasher())); + $this->assertEquals( + [ + 'f1' => ['treatment' => 'on', 'config' => 'some'], + 'f2' => ['treatment' => 'off', 'config' => null], + ], + $client->getTreatmentsWithConfig('someKey', 'someBuck', ['f1', 'f2'], ['someAttr' => 123]) + ); + $this->assertEquals(['f1' => 'on', 'f2' => 'off'], $client->getTreatments('someKey', 'someBuck', ['f1', 'f2'], ['someAttr' => 123])); + $this->assertEquals( + [ + 'f1' => ['treatment' => 'on', 'config' => 'some'], + 'f2' => ['treatment' => 'off', 'config' => null], + 'f3' => ['treatment' => 'na', 'config' => 'another'], + ], + $client->getTreatmentsWithConfig('someKey', 'someBuck', ['f1', 'f2', 'f3'], ['someAttr' => 123]) + ); + $this->assertEquals(['f1' => 'on', 'f2' => 'off', 'f3' => 'na'], $client->getTreatments('someKey', 'someBuck', ['f1', 'f2', 'f3'], ['someAttr' => 123])); + } } diff --git a/tests/Config/UtilsTest.php b/tests/Config/UtilsTest.php index 17bd84a..54b6851 100644 --- a/tests/Config/UtilsTest.php +++ b/tests/Config/UtilsTest.php @@ -3,8 +3,8 @@ namespace SplitIO\Test\Link\Consumer; use SplitIO\ThinSdk\Config\Utils; - use SplitIO\ThinSdk\Utils\ImpressionListener; +use SplitIO\ThinSdk\Utils\EvalCache\InputHasher; use PHPUnit\Framework\TestCase; @@ -15,15 +15,22 @@ public function testConfigDefault() { $cfg = Utils::default(); $this->assertEquals(null, $cfg->impressionListener()); + $this->assertEquals('none', $cfg->evaluationCache()); + $this->assertEquals(null, $cfg->customCacheHash()); } public function testConfigParsing() { $ilMock = $this->createMock(ImpressionListener::class); + $ihMock = $this->createMock(InputHasher::class); $cfg = Utils::fromArray([ 'impressionListener' => $ilMock, + 'evaluationCache' => 'custom', + 'customCacheHash' => $ihMock, ]); $this->assertEquals($ilMock, $cfg->impressionListener()); + $this->assertEquals('custom', $cfg->evaluationCache()); + $this->assertEquals($ihMock, $cfg->customCacheHash()); } } diff --git a/tests/FactoryTest.php b/tests/FactoryTest.php index 094504c..200ce25 100644 --- a/tests/FactoryTest.php +++ b/tests/FactoryTest.php @@ -13,7 +13,7 @@ use SplitIO\ThinSdk\Link\Protocol\V1\RPC; use SplitIO\ThinSdk\Link\Protocol\V1\RegisterFlags; -use SplitIO\Test\Utils\SocketServerRemoteControl; +use SplitIO\Test\TestUtils\SocketServerRemoteControl; use MessagePack\Packer; use MessagePack\Extension\TimestampExtension; diff --git a/tests/Link/Transfer/UnixSeqPacketTest.php b/tests/Link/Transfer/UnixSeqPacketTest.php index 0dbb89e..a02a9ad 100644 --- a/tests/Link/Transfer/UnixSeqPacketTest.php +++ b/tests/Link/Transfer/UnixSeqPacketTest.php @@ -4,7 +4,7 @@ use SplitIO\ThinSdk\Link\Transfer\UnixPacket; use SplitIO\ThinSdk\Link\Transfer\ConnectionException; -use SplitIO\Test\Utils\SocketServerRemoteControl; +use SplitIO\Test\TestUtils\SocketServerRemoteControl; use PHPUnit\Framework\TestCase; @@ -20,7 +20,7 @@ public function setUp(): void } if (getenv("DEBUG") == true) { - fwrite(STDERR, "preparing socket server for test: `" . get_class($this)."::".$this->getName()."`\n"); + fwrite(STDERR, "preparing socket server for test: `" . get_class($this) . "::" . $this->getName() . "`\n"); } $this->socketServerRC = new SocketServerRemoteControl(); diff --git a/tests/Link/Transfer/UnixStreamTest.php b/tests/Link/Transfer/UnixStreamTest.php index 7f22b7a..273b35c 100644 --- a/tests/Link/Transfer/UnixStreamTest.php +++ b/tests/Link/Transfer/UnixStreamTest.php @@ -4,7 +4,7 @@ use SplitIO\ThinSdk\Link\Transfer\UnixStream; use SplitIO\ThinSdk\Link\Transfer\ConnectionException; -use SplitIO\Test\Utils\SocketServerRemoteControl; +use SplitIO\Test\TestUtils\SocketServerRemoteControl; use PHPUnit\Framework\TestCase; @@ -15,11 +15,10 @@ class UnixStreamTest extends TestCase public function setUp(): void { if (getenv("DEBUG") == true) { - fwrite(STDERR, "preparing socket server for test: `" . get_class($this)."::".$this->getName()."`\n"); + fwrite(STDERR, "preparing socket server for test: `" . get_class($this) . "::" . $this->getName() . "`\n"); } $this->socketServerRC = new SocketServerRemoteControl(); - } public function testHappyExchange(): void diff --git a/tests/Utils/SocketServerRemoteControl.php b/tests/TestUtils/SocketServerRemoteControl.php similarity index 99% rename from tests/Utils/SocketServerRemoteControl.php rename to tests/TestUtils/SocketServerRemoteControl.php index 124b8eb..c13698d 100644 --- a/tests/Utils/SocketServerRemoteControl.php +++ b/tests/TestUtils/SocketServerRemoteControl.php @@ -1,6 +1,6 @@ handleTestCase($testCase, $clientSock); } - } catch (SocketCloseRequested $exc) { ($exc); // do nothing without complaining about an unused variable } finally { diff --git a/tests/Utils/EvalCache/CacheImplTest.php b/tests/Utils/EvalCache/CacheImplTest.php new file mode 100644 index 0000000..a824ec3 --- /dev/null +++ b/tests/Utils/EvalCache/CacheImplTest.php @@ -0,0 +1,78 @@ +set('key', 'f1', null, 'on'); + $this->assertEquals('on', $c->get('key', 'f1', null)); + $this->assertEquals(null, $c->get('key2', 'f1', null)); + $this->assertEquals(null, $c->get('key', 'f1', [])); + $this->assertEquals(null, $c->get('key', 'f1', ['a' => 1])); + + $c->set('key', 'f2', ['a' => 2], 'off'); + $this->assertEquals('off', $c->get('key', 'f2', ['a' => 2])); + $this->assertEquals(null, $c->get('key', 'f2', ['a' => 3])); + $this->assertEquals(null, $c->get('key', 'f2', [])); + $this->assertEquals(null, $c->get('key', 'f2', null)); + + // only f1 matches for null attributes + $this->assertEquals(['f1' => 'on', 'f2' => null, 'f3' => null], $c->getMany('key', ['f1', 'f2', 'f3'], null)); + + // only f2 matches for ['a' => 1] attributes + $this->assertEquals(['f1' => null, 'f2' => 'off', 'f3' => null], $c->getMany('key', ['f1', 'f2', 'f3'], ['a' => 2])); + + // nothing matches for [] attributes + $this->assertEquals(['f1' => null, 'f2' => null, 'f3' => null], $c->getMany('key', ['f1', 'f2', 'f3'], [])); + + // *WithConfig methods return null regardless of parameters matching if non-config entries have been stored + $this->assertEquals(null, $c->getWithConfig('key', 'f1', null)); + $this->assertEquals( + ['f1' => null, 'f2' => null, 'f3' => null,], + $c->getManyWithConfig('key', ['f1', 'f2', 'f3'], null) + ); + } + + public function testWithConfig() + { + // setting with config works for both `get`, `getMany`, `getWithConfig`, `getManyWithConfig` + $c = new CacheImpl(new KeyAttributeCRC32Hasher()); + $c->setWithConfig('key', 'f1', null, 'on', 'some'); + $this->assertEquals('on', $c->get('key', 'f1', null)); + $this->assertEquals(['treatment' => 'on', 'config' => 'some'], $c->getWithConfig('key', 'f1', null)); + + $c->setWithConfig('key', 'f2', ['a' => 2], 'off', null); + $this->assertEquals('off', $c->get('key', 'f2', ['a' => 2])); + + // only f1 matches for null attributes + $this->assertEquals( + [ + 'f1' => ['treatment' => 'on', 'config' => 'some'], + 'f2' => null, + 'f3' => null, + ], + $c->getManyWithConfig('key', ['f1', 'f2', 'f3'], null) + ); + + // only f2 matches for ['a' => 2] attributes + $this->assertEquals( + [ + 'f1' => null, + 'f2' => ['treatment' => 'off', 'config' => null], + 'f3' => null, + ], + $c->getManyWithConfig('key', ['f1', 'f2', 'f3'], ['a' => 2]) + ); + + + // nothing matches for [] attributes + $this->assertEquals(['f1' => null, 'f2' => null, 'f3' => null], $c->getManyWithConfig('key', ['f1', 'f2', 'f3'], [])); + } +} diff --git a/tests/Utils/EvalCache/KeyAttributeCRC32HasherTest.php b/tests/Utils/EvalCache/KeyAttributeCRC32HasherTest.php new file mode 100644 index 0000000..25a90ec --- /dev/null +++ b/tests/Utils/EvalCache/KeyAttributeCRC32HasherTest.php @@ -0,0 +1,33 @@ +assertEquals("key::feature", $hasher->hashInput("key", "feature")); + $this->assertEquals("key::feature", $hasher->hashInput("key", "feature", null)); + } + + public function testHashWithAttributes() + { + $testCases = [ + [], + ['a' => 1], + ['b' => 'asd'], + ['c' => ['a', 'b', 'à']], + ['d' => null], + ]; + + $hasher = new KeyAttributeCRC32Hasher(); + foreach ($testCases as $testCase) { + $this->assertEquals("key::feature::" . crc32(json_encode($testCase)), $hasher->hashInput("key", "feature", $testCase)); + } + } +} diff --git a/tests/Utils/EvalCache/KeyOnlyHasherTest.php b/tests/Utils/EvalCache/KeyOnlyHasherTest.php new file mode 100644 index 0000000..b0dfd3b --- /dev/null +++ b/tests/Utils/EvalCache/KeyOnlyHasherTest.php @@ -0,0 +1,19 @@ +assertEquals("key::feature", $hasher->hashInput("key", "feature")); + $this->assertEquals("key::feature", $hasher->hashInput("key", "feature", null)); + $this->assertEquals("key::feature", $hasher->hashInput("key", "feature", [])); + $this->assertEquals("key::feature", $hasher->hashInput("key", "feature", ['a' => 1])); + } +} diff --git a/tests/Utils/EvalCache/NoCacheTest.php b/tests/Utils/EvalCache/NoCacheTest.php new file mode 100644 index 0000000..a751339 --- /dev/null +++ b/tests/Utils/EvalCache/NoCacheTest.php @@ -0,0 +1,43 @@ +set('key', 'f1', null, 'on'); + $c->set('key', 'f2', null, 'off'); + $this->assertEquals(null, $c->get('key', 'f1', null)); + $this->assertEquals(null, $c->get('key', 'f2', ['a' => 1])); + $this->assertEquals(['f1' => null, 'f2' => null], $c->getMany('key', ['f1', 'f2'], null)); + $c->setMany('key', null, ['f1' => 'on', 'f2' => 'off']); + $this->assertEquals(null, $c->get('key', 'f1', null)); + $this->assertEquals(null, $c->get('key', 'f2', ['a' => 1])); + $this->assertEquals(['f1' => null, 'f2' => null], $c->getMany('key', ['f1', 'f2'], null)); + + $c->setWithConfig('key', 'f1', null, 'on', 'some'); + $c->setWithConfig('key', 'f2', null, 'off', 'some'); + $this->assertEquals(null, $c->get('key', 'f1', null)); + $this->assertEquals(null, $c->get('key', 'f2', ['a' => 1])); + $this->assertEquals(['f1' => null, 'f2' => null], $c->getMany('key', ['f1', 'f2'], null)); + $this->assertEquals(null, $c->getWithConfig('key', 'f1', null)); + $this->assertEquals(null, $c->getWithConfig('key', 'f2', null)); + $this->assertEquals(['f1' => null, 'f2' => null], $c->getManyWithConfig('key', ['f1', 'f2'], null)); + + $c->setManyWithConfig('key', null, [ + 'f1' => ['treatment' => 'on', 'config' => 'some'], + 'f2' => ['treatment' => 'off', 'config' => null], + ]); + $this->assertEquals(null, $c->get('key', 'f1', null)); + $this->assertEquals(null, $c->get('key', 'f2', ['a' => 1])); + $this->assertEquals(['f1' => null, 'f2' => null], $c->getMany('key', ['f1', 'f2'], null)); + $this->assertEquals(null, $c->getWithConfig('key', 'f1', null)); + $this->assertEquals(null, $c->getWithConfig('key', 'f2', null)); + $this->assertEquals(['f1' => null, 'f2' => null], $c->getManyWithConfig('key', ['f1', 'f2'], null)); + } +}