Skip to content

Commit

Permalink
Merge branch 'main' of github.com:splitio/php-thin-client into task/t…
Browse files Browse the repository at this point in the history
…reatmentsByFlagSet
  • Loading branch information
mmelograno committed Dec 21, 2023
2 parents 95da73f + 78681b8 commit 847f160
Show file tree
Hide file tree
Showing 17 changed files with 548 additions and 18 deletions.
4 changes: 4 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
1.4.0 (Dec 14, 2023):
- Add support for a custom tracer for client methods.
- Support finer granularity on timeouts.

1.3.0 (Nov 10, 2023):
- Added in-memory evaluation cache for the duration of a request.

Expand Down
74 changes: 74 additions & 0 deletions examples/tracer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<?php

require_once '../vendor/autoload.php';

use \SplitIO\ThinSdk\Factory;
use \SplitIO\ThinSdk\Utils\Tracing\Tracer;
use \SplitIO\ThinSdk\Utils\Tracing\TracerHook;

class CustomTracer implements TracerHook
{

private $traces = [];

public function on(array $event)
{

// assume we only care about getTreatment() calls...
if ($event['method'] != Tracer::METHOD_GET_TREATMENT) {
return;
}

$trace = $this->traces[$event['id']] ?? [];

switch ($event['event']) {
case Tracer::EVENT_START:
$trace['start'] = microtime(true);
$trace['args'] = $event['arguments'];
break;
case Tracer::EVENT_RPC_START:
$trace['rpc_start'] = microtime(true);
break;
case Tracer::EVENT_RPC_END:
$trace['rpc_end'] = microtime(true);
break;
case Tracer::EVENT_EXCEPTION:
$trace['exception'] = $event['exception'];
break;
case Tracer::EVENT_END:
$trace['end'] = microtime(true);
break;
}

$this->traces[$event['id']] = $trace;
}

public function getTraces(): array
{
return $this->traces;
}
}

$ct = new CustomTracer();

$factory = Factory::withConfig([
'transfer' => [
'address' => '../../splitd.sock',
'type' => 'unix-stream',
],
'logging' => [
'level' => \Psr\Log\LogLevel::INFO,
],
'utils' => [
'tracer' => [
'hook' => $ct,
'forwardArgs' => true,
]
],

]);

$manager = $factory->manager();
$client = $factory->client();
echo $client->getTreatment("key", null, $manager->splitNames()[0], ['age' => 22]);
var_dump($ct->getTraces());
54 changes: 50 additions & 4 deletions src/Client.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
namespace SplitIO\ThinSdk;

use \SplitIO\ThinSdk\Utils\ImpressionListener;
use \SplitIO\ThinSdk\Utils\Tracing\TracingEventFactory as TEF;
use \SplitIO\ThinSdk\Utils\Tracing\Tracer;
use \SplitIO\ThinSdk\Utils\Tracing\NoOpTracerHook;
use \SplitIO\ThinSdk\Utils\EvalCache\Cache;
use \SplitIO\ThinSdk\Utils\EvalCache\NoCache;
use \SplitIO\ThinSdk\Utils\InputValidator\InputValidator;
Expand All @@ -20,36 +23,49 @@ class Client implements ClientInterface
private /*?ImpressionListener*/ $impressionListener;
private /*InputValidator*/ $inputValidator;
private /*Cache*/ $cache;
private /*Tracer*/ $tracer;

public function __construct(Manager $manager, LoggerInterface $logger, ?ImpressionListener $impressionListener, ?Cache $cache = null)
public function __construct(Manager $manager, LoggerInterface $logger, ?ImpressionListener $impressionListener, ?Cache $cache = null, ?Tracer $tracer = null)
{
$this->logger = $logger;
$this->lm = $manager;
$this->impressionListener = $impressionListener;
$this->inputValidator = new InputValidator($logger);
$this->cache = $cache ?? new NoCache();
$this->tracer = $tracer ?? new Tracer(new NoOpTracerHook());
}

public function getTreatment(string $key, ?string $bucketingKey, string $feature, ?array $attributes = null): string
{
try {
$id = $this->tracer->makeId();
$method = Tracer::METHOD_GET_TREATMENT;
$this->tracer->trace(TEF::forStart($method, $id, $this->tracer->includeArgs() ? func_get_args() : []));
if (($fromCache = $this->cache->get($key, $feature, $attributes)) != null) {
return $fromCache;
}

$this->tracer->trace(TEF::forRPCStart($method, $id));
list($treatment, $ilData) = $this->lm->getTreatment($key, $bucketingKey, $feature, $attributes);
$this->tracer->trace(TEF::forRPCEnd($method, $id));
$this->handleListener($key, $bucketingKey, $feature, $attributes, $treatment, $ilData);
$this->cache->set($key, $feature, $attributes, $treatment);
return $treatment;
} catch (\Exception $exc) {
$this->tracer->trace(TEF::forException($method, $id, $exc));
$this->logger->error($exc);
return "control";
} finally {
$this->tracer->trace(TEF::forEnd($method, $id));
}
}

public function getTreatments(string $key, ?string $bucketingKey, array $features, ?array $attributes = null): array
{
try {
$id = $this->tracer->makeId();
$method = Tracer::METHOD_GET_TREATMENTS;
$this->tracer->trace(TEF::forStart($method, $id, $this->tracer->includeArgs() ? func_get_args() : []));
// 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);
Expand All @@ -58,7 +74,9 @@ public function getTreatments(string $key, ?string $bucketingKey, array $feature
return $toReturn;
}

$this->tracer->trace(TEF::forRPCStart($method, $id));
$results = $this->lm->getTreatments($key, $bucketingKey, $features, $attributes);
$this->tracer->trace(TEF::forRPCEnd($method, $id));
foreach ($results as $feature => $result) {
list($treatment, $ilData) = $result;
$toReturn[$feature] = $treatment;
Expand All @@ -67,43 +85,58 @@ public function getTreatments(string $key, ?string $bucketingKey, array $feature
$this->cache->setMany($key, $attributes, $toReturn);
return $toReturn;
} catch (\Exception $exc) {
$this->tracer->trace(TEF::forException($method, $id, $exc));
$this->logger->error($exc);
return array_reduce($features, function ($r, $k) {
$r[$k] = "control";
return $r;
}, []);
} finally {
$this->tracer->trace(TEF::forEnd($method, $id));
}
}

public function getTreatmentWithConfig(string $key, ?string $bucketingKey, string $feature, ?array $attributes = null): array
{
try {

$id = $this->tracer->makeId();
$method = Tracer::METHOD_GET_TREATMENT_WITH_CONFIG;
$this->tracer->trace(TEF::forStart($method, $id, $this->tracer->includeArgs() ? func_get_args() : []));
if (($fromCache = $this->cache->getWithConfig($key, $feature, $attributes)) != null) {
return $fromCache;
}

$this->tracer->trace(TEF::forRPCStart($method, $id));
list($treatment, $ilData, $config) = $this->lm->getTreatmentWithConfig($key, $bucketingKey, $feature, $attributes);
$this->tracer->trace(TEF::forRPCEnd($method, $id));
$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->tracer->trace(TEF::forException($method, $id, $exc));
$this->logger->error($exc);
return "control";
return ['treatment' => "control", 'config' => null];
} finally {
$this->tracer->trace(TEF::forEnd($method, $id));
}
}

public function getTreatmentsWithConfig(string $key, ?string $bucketingKey, array $features, ?array $attributes = null): array
{
try {
$id = $this->tracer->makeId();
$method = Tracer::METHOD_GET_TREATMENTS_WITH_CONFIG;
$this->tracer->trace(TEF::forStart($method, $id, $this->tracer->includeArgs() ? func_get_args() : []));
$toReturn = $this->cache->getManyWithConfig($key, $features, $attributes);
$features = self::getMissing($toReturn);

if (count($features) == 0) {
return $toReturn;
}

$this->tracer->trace(TEF::forRPCStart($method, $id));
$results = $this->lm->getTreatmentsWithConfig($key, $bucketingKey, $features, $attributes);
$this->tracer->trace(TEF::forRPCEnd($method, $id));
foreach ($results as $feature => $result) {
list($treatment, $ilData, $config) = $result;
$toReturn[$feature] = ['treatment' => $treatment, 'config' => $config];
Expand All @@ -112,11 +145,14 @@ public function getTreatmentsWithConfig(string $key, ?string $bucketingKey, arra
$this->cache->setManyWithConfig($key, $attributes, $toReturn);
return $toReturn;
} catch (\Exception $exc) {
$this->tracer->trace(TEF::forException($method, $id, $exc));
$this->logger->error($exc);
return array_reduce($features, function ($r, $k) {
$r[$k] = ['treatment' => 'control', 'config' => null];
return $r;
}, []);
} finally {
$this->tracer->trace(TEF::forEnd($method, $id));
}
}

Expand Down Expand Up @@ -161,12 +197,22 @@ public function getTreatmentsWithConfigByFlagSet(string $key, ?string $bucketing
public function track(string $key, string $trafficType, string $eventType, ?float $value = null, ?array $properties = null): bool
{
try {
$id = $this->tracer->makeId();
$method = Tracer::METHOD_TRACK;
$this->tracer->trace(TEF::forStart($method, $id, $this->tracer->includeArgs() ? func_get_args() : []));
$properties = $this->inputValidator->validProperties($properties);
return $this->lm->track($key, $trafficType, $eventType, $value, $properties);
$this->tracer->trace(TEF::forRPCStart($method, $id));
$res = $this->lm->track($key, $trafficType, $eventType, $value, $properties);
$this->tracer->trace(TEF::forRPCEnd($method, $id));
return $res;
} catch (ValidationException $exc) {
$this->tracer->trace(TEF::forException($method, $id, $exc));
$this->logger->error("error validating event properties: " . $exc->getMessage());
} catch (\Exception $exc) {
$this->tracer->trace(TEF::forException($method, $id, $exc));
$this->logger->error($exc);
} finally {
$this->tracer->trace(TEF::forEnd($method, $id));
}
return false;
}
Expand Down
43 changes: 43 additions & 0 deletions src/Config/Tracer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

namespace SplitIO\ThinSdk\Config;

use SplitIO\ThinSdk\Utils\Tracing\TracerHook;


class Tracer
{

private /*?TracerHook*/ $hook;
private /*bool*/ $forwardArgs;

private function __construct(?TracerHook $hook, bool $forwardArguments)
{
$this->hook = $hook;
$this->forwardArgs = $forwardArguments;
}

public function hook(): ?TracerHook
{
return $this->hook;
}

public function forwardArgs(): bool
{
return $this->forwardArgs;
}

public static function fromArray(array $config): Tracer
{
$d = self::default();
return new Tracer(
$config['hook'] ?? $d->hook(),
$config['forwardArgs'] ?? $d->forwardArgs,
);
}

public static function default(): Tracer
{
return new Tracer(null, false);
}
}
12 changes: 10 additions & 2 deletions src/Config/Utils.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,25 @@ class Utils
{
private /*?ImpressionListener*/ $listener;
private /*?string*/ $evaluationCache;
private /*?TracerHook*/ $tracer;

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

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

public function tracer(): Tracer
{
return $this->tracer;
}

public function evaluationCache(): ?EvaluationCache
{
return $this->evaluationCache;
Expand All @@ -32,11 +39,12 @@ public static function fromArray(array $config): Utils
return new Utils(
$config['impressionListener'] ?? $d->impressionListener(),
EvaluationCache::fromArray($config['evaluationCache'] ?? []),
Tracer::fromArray($config['tracer'] ?? []),
);
}

public static function default(): Utils
{
return new Utils(null, EvaluationCache::default());
return new Utils(null, EvaluationCache::default(), Tracer::default());
}
}
4 changes: 3 additions & 1 deletion src/Factory.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use SplitIO\ThinSdk\Foundation\Logging;
use SplitIO\ThinSdk\Utils\EvalCache;
use SplitIO\ThinSdk\Utils\Tracing\Tracer;

class Factory implements FactoryInterface
{
Expand Down Expand Up @@ -65,7 +66,8 @@ public function client(): ClientInterface
$this->linkManager,
$this->logger,
$uc->impressionListener(),
EvalCache\Helpers::getCache($uc->evaluationCache(), $this->logger)
EvalCache\Helpers::getCache($uc->evaluationCache(), $this->logger),
new Tracer($uc->tracer()->hook(), $uc->tracer()->forwardArgs()),
);
}

Expand Down
22 changes: 16 additions & 6 deletions src/Link/Transfer/ConnectionFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,16 +32,26 @@ public function create(): RawConnection
throw new \Exception("invalid connection type " . $this->sockType);
}

private static function formatTimeout(?int $milliseconds)/*: ?int */
private static function formatTimeout($timeout)/*: ?int */
{
if ($milliseconds == null) {
$milliseconds = 1000;
if (is_array($timeout)) {
// assume it's a properly formatted unix-like timeout (including 'sec' & 'usec')
if (!array_key_exists('sec', $timeout) || !array_key_exists('usec', $timeout)) {
throw new \Exception("timeout must either be an int (milliseconds) or an array with keys 'sec' & 'usec'");
}
return $timeout;
}

if (!is_null($timeout) && !is_int($timeout)) {
throw new \Exception("timeout must either be an int (milliseconds) or an array with keys 'sec' & 'usec'");
}

if ($timeout == null || $timeout == 0) {
$timeout = 1000;
}
return [
'sec' => $milliseconds / 1000,
'usec' => 0, // TODO(mredolatti): handle seconds fractions in usec units
'sec' => floor($timeout / 1000),
'usec' => ($timeout % 1000) * 1000,
];
}

}
Loading

0 comments on commit 847f160

Please sign in to comment.