Skip to content

Commit

Permalink
Merge pull request #14 from splitio/enh/evalcache
Browse files Browse the repository at this point in the history
request-scoped evaluation cache implementation
  • Loading branch information
mredolatti authored Nov 10, 2023
2 parents 8797594 + 23aa76d commit 07896c1
Show file tree
Hide file tree
Showing 31 changed files with 817 additions and 24 deletions.
3 changes: 3 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
1.3.0 (Nov 10, 2023):
- 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.
Expand Down
41 changes: 38 additions & 3 deletions src/Client.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -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'));
}
}
58 changes: 58 additions & 0 deletions src/Config/EvaluationCache.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?php

namespace SplitIO\ThinSdk\Config;

use SplitIO\ThinSdk\Utils\EvalCache\InputHasher;


class EvaluationCache
{
private /*?string*/ $type;
private /*?InputHasher*/ $customHash;
private /*string*/ $evictionPolicy;
private /*int*/ $maxSize;

private function __construct(string $type, ?InputHasher $customHash, string $evictionPolicy, int $maxSize)
{
$this->type = $type;
$this->customHash = $customHash;
$this->evictionPolicy = $evictionPolicy;
$this->maxSize = $maxSize;
}

public function type(): string
{
return $this->type;
}

public function customHash(): ?InputHasher
{
return $this->customHash;
}

public function evictionPolicy(): string
{
return $this->evictionPolicy;
}

public function maxSize(): int
{
return $this->maxSize;
}

public static function fromArray(array $config): EvaluationCache
{
$d = self::default();
return new EvaluationCache(
$config['type'] ?? $d->type(),
$config['customHash'] ?? $d->customHash(),
$config['evictionPolicy'] ?? $d->evictionPolicy(),
$config['maxSize'] ?? $d->maxSize(),
);
}

public static function default(): EvaluationCache
{
return new EvaluationCache('none', null, 'no-eviction', 1000);
}
}
18 changes: 14 additions & 4 deletions src/Config/Utils.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,41 @@

namespace SplitIO\ThinSdk\Config;

use \SplitIO\ThinSdk\Utils\ImpressionListener;
use SplitIO\ThinSdk\Utils\ImpressionListener;


class Utils
{
private /*?ImpressionListener*/ $listener;
private /*?string*/ $evaluationCache;

private function __construct(?ImpressionListener $listener)
private function __construct(?ImpressionListener $listener, EvaluationCache $cache)
{
$this->listener = $listener;
$this->evaluationCache = $cache;
}

public function impressionListener(): ?ImpressionListener
{
return $this->listener;
}

public function evaluationCache(): ?EvaluationCache
{
return $this->evaluationCache;
}

public static function fromArray(array $config): Utils
{
$d = self::default();
return new Utils($config['impressionListener'] ?? $d->impressionListener());
return new Utils(
$config['impressionListener'] ?? $d->impressionListener(),
EvaluationCache::fromArray($config['evaluationCache'] ?? []),
);
}

public static function default(): Utils
{
return new Utils(null);
return new Utils(null, EvaluationCache::default());
}
}
17 changes: 12 additions & 5 deletions src/Factory.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

namespace SplitIO\ThinSdk;

use SplitIO\ThinSdk\Foundation\Logging\Helpers;
use SplitIO\ThinSdk\Foundation\Logging;
use SplitIO\ThinSdk\Utils\EvalCache;

class Factory implements FactoryInterface
{
Expand All @@ -13,7 +14,7 @@ class Factory implements FactoryInterface
private function __construct(Config\Main $config)
{
$this->config = $config;
$this->logger = Helpers::getLogger($config->logging());
$this->logger = Logging\Helpers::getLogger($config->logging());
$this->linkManager = Link\Consumer\Initializer::setup(
Link\Protocol\Version::V1(),
$config->transfer(),
Expand All @@ -40,7 +41,7 @@ public static function withConfig(array $config): FactoryInterface
throw new Fallback\FallbackDisabledException($e);
}

$logger = Helpers::getLogger($parsedConfig->logging());
$logger = Logging\Helpers::getLogger($parsedConfig->logging());
$logger->error(sprintf("error instantiating a factory with supplied config (%s). will return a fallback.", $e->getMessage()));
$logger->debug($e);
return new Fallback\GenericFallbackFactory($parsedConfig->fallback()->client(), $parsedConfig->fallback()->manager());
Expand All @@ -50,7 +51,7 @@ public static function withConfig(array $config): FactoryInterface
} catch (\Exception $e) {
// This branch is virtually unreachable (hence untestable) unless we introduce a bug.
// it's basically a safeguard to prevent the customer app from crashing if we do.
$logger = Helpers::getLogger(Config\Logging::default());
$logger = Logging\Helpers::getLogger(Config\Logging::default());
$logger->error(sprintf("error parsing supplied factory config config (%s). will return a fallback.", $e->getMessage()));
return new Fallback\GenericFallbackFactory(new Fallback\AlwaysControlClient(), new Fallback\AlwaysEmptyManager());
}
Expand All @@ -59,7 +60,13 @@ public static function withConfig(array $config): FactoryInterface

public function client(): ClientInterface
{
return new Client($this->linkManager, $this->logger, $this->config->utils()->impressionListener());
$uc = $this->config->utils();
return new Client(
$this->linkManager,
$this->logger,
$uc->impressionListener(),
EvalCache\Helpers::getCache($uc->evaluationCache(), $this->logger)
);
}

public function manager(): ManagerInterface
Expand Down
15 changes: 15 additions & 0 deletions src/Utils/EvalCache/Cache.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

namespace SplitIO\ThinSdk\Utils\EvalCache;

interface Cache
{
public function get(string $key, string $feature, ?array $attributes): ?string;
public function getMany(string $key, array $features, ?array $attributes): array;
public function getWithConfig(string $key, string $feature, ?array $attributes): ?array;
public function getManyWithConfig(string $key, array $features, ?array $attributes): array;
public function set(string $key, string $feature, ?array $attributes, string $treatment);
public function setMany(string $key, ?array $attributes, array $results);
public function setWithConfig(string $key, string $feature, ?array $attributes, string $treatment, ?string $config);
public function setManyWithConfig(string $key, ?array $attributes, array $results);
}
86 changes: 86 additions & 0 deletions src/Utils/EvalCache/CacheImpl.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<?php

namespace SplitIO\ThinSdk\Utils\EvalCache;

class CacheImpl implements Cache
{

private /*array*/ $data;
private /*InputHasher*/ $hasher;
private /*EvictionPolicy*/ $evictionPolicy;

public function __construct(InputHasher $hasher, EvictionPolicy $evictionPolicy)
{
$this->data = [];
$this->hasher = $hasher;
$this->evictionPolicy = $evictionPolicy;
}

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);
$this->evictionPolicy->postCacheInsertionHook($h, $this->data);
}

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);
$this->evictionPolicy->postCacheInsertionHook($h, $this->data);
}

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;
}
}
Loading

0 comments on commit 07896c1

Please sign in to comment.