diff --git a/src/Config/Configuration.php b/src/Config/Configuration.php index 6aee87e..9372bac 100644 --- a/src/Config/Configuration.php +++ b/src/Config/Configuration.php @@ -4,7 +4,6 @@ use Eppo\DTO\Bandit\Bandit; use Eppo\DTO\BanditParametersResponse; -use Eppo\DTO\BanditReference; use Eppo\DTO\ConfigurationWire\ConfigResponse; use Eppo\DTO\ConfigurationWire\ConfigurationWire; use Eppo\DTO\Flag; @@ -12,7 +11,6 @@ class Configuration { - private array $parsedFlags = []; private readonly FlagConfigResponse $flags; private readonly BanditParametersResponse $bandits; @@ -23,8 +21,8 @@ private function __construct( ) { $flagJson = json_decode($this->flagsConfig->response, true); $banditsJson = json_decode($this->banditsConfig?->response ?? "", true); - $this->flags = FlagConfigResponse::fromJson($flagJson ?? []); - $this->bandits = BanditParametersResponse::fromJson($banditsJson ?? []); + $this->flags = FlagConfigResponse::fromArray($flagJson ?? []); + $this->bandits = BanditParametersResponse::fromArray($banditsJson ?? []); } public static function fromUfcResponses(ConfigResponse $flagsConfig, ?ConfigResponse $banditsConfig): Configuration @@ -39,10 +37,9 @@ public static function fromConfigurationWire(ConfigurationWire $configurationWir public static function fromFlags(array $flags, ?array $bandits = null) { - $fcr = FlagConfigResponse::fromJson(["flags" => $flags]); - $flagsConfig = new ConfigResponse(response: json_encode($fcr)); + $flagsConfig = new ConfigResponse(response: json_encode(["flags" => $flags])); $banditsConfig = $bandits ? new ConfigResponse( - response: json_encode(BanditParametersResponse::fromJson(["bandits" => $bandits])) + response: json_encode(BanditParametersResponse::fromArray(["bandits" => $bandits])) ) : null; return new self($flagsConfig, $banditsConfig); } @@ -54,13 +51,7 @@ public static function emptyConfig(): self public function getFlag(string $key): ?Flag { - if (!isset($this->parsedFlags[$key])) { - $flagObj = $this->flags->flags[$key] ?? null; - if ($flagObj !== null) { - $this->parsedFlags[$key] = Flag::fromJson($flagObj); - } - } - return $this->parsedFlags[$key] ?? null; + return $this->flags->flags[$key] ?? null; } public function getBandit(string $banditKey): ?Bandit @@ -68,13 +59,12 @@ public function getBandit(string $banditKey): ?Bandit if (!isset($this->bandits->bandits[$banditKey])) { return null; } - return Bandit::fromJson($this->bandits?->bandits[$banditKey]) ?? null; + return Bandit::fromArray($this->bandits?->bandits[$banditKey]) ?? null; } public function getBanditByVariation(string $flagKey, string $variation): ?string { - foreach ($this->flags->banditReferences as $banditKey => $banditReferenceObj) { - $banditReference = BanditReference::fromJson($banditReferenceObj); + foreach ($this->flags->banditReferences as $banditKey => $banditReference) { foreach ($banditReference->flagVariations as $flagVariation) { if ($flagVariation->flagKey === $flagKey && $flagVariation->variationKey === $variation) { return $banditKey; @@ -101,4 +91,14 @@ public function getFlagETag(): ?string { return $this->flagsConfig?->eTag ?? null; } + + public function getBanditModelVersions(): array + { + $models = []; + foreach ($this->bandits->bandits as $key => $banditArr) { + $bandit = Bandit::fromArray($banditArr); + $models[$key] = $bandit->modelVersion; + } + return $models; + } } diff --git a/src/Config/ConfigurationLoader.php b/src/Config/ConfigurationLoader.php index 108770a..6d7a7e1 100644 --- a/src/Config/ConfigurationLoader.php +++ b/src/Config/ConfigurationLoader.php @@ -35,6 +35,7 @@ public function reloadConfigurationIfExpired(): void */ public function fetchAndStoreConfiguration(?string $flagETag): void { + $currentConfig = $this->configurationStore->getConfiguration(); $response = $this->apiRequestWrapper->getUFC($flagETag); if ($response->isModified) { $configResponse = new ConfigResponse( @@ -48,12 +49,41 @@ public function fetchAndStoreConfiguration(?string $flagETag): void syslog(LOG_WARNING, "[Eppo SDK] Empty or invalid response from the configuration server."); return; } - $fcr = FlagConfigResponse::fromJson($responseData); + $fcr = FlagConfigResponse::fromArray($responseData); $banditResponse = null; - // TODO: Also check current bandit models loaded for optimized bandit loading. + // If the flags reference Bandits, load bandits from the API, or reuse models already downloaded. if (count($fcr->banditReferences) > 0) { - $bandits = $this->apiRequestWrapper->getBandits(); - $banditResponse = new ConfigResponse($bandits->body, date('c'), $bandits->eTag); + // Assume we can reuse bandits. + $canReuseBandits = true; + $currentBandits = $currentConfig->getBanditModelVersions(); + + // Check each referenced bandit model (what we need) against the current bandits (what we have). + foreach ($fcr->banditReferences as $banditKey => $banditReference) { + if ( + !array_key_exists( + $banditKey, + $currentBandits + ) || $banditReference->modelVersion !== $currentBandits[$banditKey] + ) { + // We don't have a bandit model at all for this key, or the model versions don't match. + $canReuseBandits = false; + break; + } + } + + if ($canReuseBandits) { + // Get the bandit ConfigResponse from the most recent configuration. + $banditResponse = $currentConfig->toConfigurationWire()->bandits; + } else { + // Fetch the bandits from the API and build a ConfigResponse to populate the new + // Configuration object. + $banditResource = $this->apiRequestWrapper->getBandits(); + if (!$banditResource?->body) { + syslog(E_ERROR, "[Eppo SDK] Empty or invalid bandit response from the configuration server."); + } else { + $banditResponse = new ConfigResponse($banditResource->body, date('c'), $banditResource->eTag); + } + } } $configuration = Configuration::fromUfcResponses($configResponse, $banditResponse); diff --git a/src/DTO/Bandit/ActionCoefficients.php b/src/DTO/Bandit/ActionCoefficients.php index ea943b0..aadba63 100644 --- a/src/DTO/Bandit/ActionCoefficients.php +++ b/src/DTO/Bandit/ActionCoefficients.php @@ -47,29 +47,29 @@ public function __construct( * @return array * @throws InvalidArgumentException */ - public static function arrayFromJson(array $coefficients): array + public static function parseArray(array $coefficients): array { $res = []; foreach ($coefficients as $key => $coefficient) { - $res[$key] = ActionCoefficients::fromJson($coefficient); + $res[$key] = ActionCoefficients::fromArray($coefficient); } return $res; } /** - * @param array $json + * @param array $arr * @return ActionCoefficients * @throws InvalidArgumentException */ - public static function fromJson(array $json): ActionCoefficients + public static function fromArray(array $arr): ActionCoefficients { return new ActionCoefficients( - $json['actionKey'], - $json['intercept'], - NumericAttributeCoefficient::arrayFromJson($json['subjectNumericCoefficients']), - CategoricalAttributeCoefficient::arrayFromJson($json['subjectCategoricalCoefficients']), - NumericAttributeCoefficient::arrayFromJson($json['actionNumericCoefficients']), - CategoricalAttributeCoefficient::arrayFromJson($json['actionCategoricalCoefficients']) + $arr['actionKey'], + $arr['intercept'], + NumericAttributeCoefficient::parseArray($arr['subjectNumericCoefficients']), + CategoricalAttributeCoefficient::parseArray($arr['subjectCategoricalCoefficients']), + NumericAttributeCoefficient::parseArray($arr['actionNumericCoefficients']), + CategoricalAttributeCoefficient::parseArray($arr['actionCategoricalCoefficients']) ); } } diff --git a/src/DTO/Bandit/Bandit.php b/src/DTO/Bandit/Bandit.php index c15cd33..d3aaad9 100644 --- a/src/DTO/Bandit/Bandit.php +++ b/src/DTO/Bandit/Bandit.php @@ -16,32 +16,32 @@ public function __construct( } /** - * @param array $json + * @param array $arr * @return Bandit */ - public static function fromJson(array $json): Bandit + public static function fromArray(array $arr): Bandit { try { - if (!isset($json['updatedAt'])) { + if (!isset($arr['updatedAt'])) { $updatedAt = new DateTime(); - } elseif (is_array($json['updatedAt'])) {// serialized datetime - $updatedAt = new DateTime($json['updatedAt']['date']); + } elseif (is_array($arr['updatedAt'])) {// serialized datetime + $updatedAt = new DateTime($arr['updatedAt']['date']); } else { - $updatedAt = new DateTime($json['updatedAt']); + $updatedAt = new DateTime($arr['updatedAt']); } } catch (\Exception $e) { syslog( LOG_WARNING, - "[Eppo SDK] invalid timestamp for bandit model ${json['updatedAt']}: " . $e->getMessage() + "[Eppo SDK] invalid timestamp for bandit model ${arr['updatedAt']}: " . $e->getMessage() ); $updatedAt = new DateTime(); } finally { return new Bandit( - $json['banditKey'], - $json['modelName'], + $arr['banditKey'], + $arr['modelName'], $updatedAt, - $json['modelVersion'], - BanditModelData::fromJson($json['modelData']) + $arr['modelVersion'], + BanditModelData::fromArray($arr['modelData']) ); } } diff --git a/src/DTO/Bandit/BanditModelData.php b/src/DTO/Bandit/BanditModelData.php index 5934d6b..2a8b638 100644 --- a/src/DTO/Bandit/BanditModelData.php +++ b/src/DTO/Bandit/BanditModelData.php @@ -18,13 +18,13 @@ public function __construct( ) { } - public static function fromJson($json): BanditModelData + public static function fromArray($arr): BanditModelData { return new BanditModelData( - $json['gamma'], - ActionCoefficients::arrayFromJson($json['coefficients']), - $json['defaultActionScore'], - $json['actionProbabilityFloor'] + $arr['gamma'], + ActionCoefficients::parseArray($arr['coefficients']), + $arr['defaultActionScore'], + $arr['actionProbabilityFloor'] ); } } diff --git a/src/DTO/Bandit/BanditResult.php b/src/DTO/Bandit/BanditResult.php index cd261fd..e1cda66 100644 --- a/src/DTO/Bandit/BanditResult.php +++ b/src/DTO/Bandit/BanditResult.php @@ -26,8 +26,8 @@ public function __toString(): string return $this->action ?? $this->variation; } - public static function fromJson($json): self + public static function fromArray($arr): self { - return new self($json['Variation'], $json['Action'] ?? null); + return new self($arr['Variation'], $arr['Action'] ?? null); } } diff --git a/src/DTO/Bandit/CategoricalAttributeCoefficient.php b/src/DTO/Bandit/CategoricalAttributeCoefficient.php index 251e4c2..9bdd79f 100644 --- a/src/DTO/Bandit/CategoricalAttributeCoefficient.php +++ b/src/DTO/Bandit/CategoricalAttributeCoefficient.php @@ -17,20 +17,20 @@ public function __construct( } /** - * @param array $json + * @param array $arr * @return CategoricalAttributeCoefficient */ - public static function fromJson(array $json): CategoricalAttributeCoefficient + public static function fromArray(array $arr): CategoricalAttributeCoefficient { - return new self($json['attributeKey'], $json['missingValueCoefficient'], $json['valueCoefficients']); + return new self($arr['attributeKey'], $arr['missingValueCoefficient'], $arr['valueCoefficients']); } /** * @param array $categoricalCoefficients * @return CategoricalAttributeCoefficient[] */ - public static function arrayFromJson(array $categoricalCoefficients): array + public static function parseArray(array $categoricalCoefficients): array { - return array_map(fn($item) => self::fromJson($item), $categoricalCoefficients); + return array_map(fn($item) => self::fromArray($item), $categoricalCoefficients); } } diff --git a/src/DTO/Bandit/NumericAttributeCoefficient.php b/src/DTO/Bandit/NumericAttributeCoefficient.php index f329e6c..2c4f8f8 100644 --- a/src/DTO/Bandit/NumericAttributeCoefficient.php +++ b/src/DTO/Bandit/NumericAttributeCoefficient.php @@ -12,20 +12,20 @@ public function __construct( } /** - * @param array $json + * @param array $arr * @return NumericAttributeCoefficient */ - public static function fromJson(array $json): NumericAttributeCoefficient + public static function fromArray(array $arr): NumericAttributeCoefficient { - return new self($json['attributeKey'], $json['coefficient'], $json['missingValueCoefficient']); + return new self($arr['attributeKey'], $arr['coefficient'], $arr['missingValueCoefficient']); } /** * @param array $numericCoefficients * @return NumericAttributeCoefficient[] */ - public static function arrayFromJson(array $numericCoefficients): array + public static function parseArray(array $numericCoefficients): array { - return array_map(fn($item) => self::fromJson($item), $numericCoefficients); + return array_map(fn($item) => self::fromArray($item), $numericCoefficients); } } diff --git a/src/DTO/BanditFlagVariation.php b/src/DTO/BanditFlagVariation.php index 11a9ffe..ac981df 100644 --- a/src/DTO/BanditFlagVariation.php +++ b/src/DTO/BanditFlagVariation.php @@ -13,14 +13,14 @@ public function __construct( ) { } - public static function fromJson($json): BanditFlagVariation + public static function fromArray($arr): BanditFlagVariation { return new self( - $json['key'], - $json['flagKey'], - $json['allocationKey'], - $json['variationKey'], - $json['variationValue'] + $arr['key'], + $arr['flagKey'], + $arr['allocationKey'], + $arr['variationKey'], + $arr['variationValue'] ); } diff --git a/src/DTO/BanditParametersResponse.php b/src/DTO/BanditParametersResponse.php index b8f9390..5edfbb5 100644 --- a/src/DTO/BanditParametersResponse.php +++ b/src/DTO/BanditParametersResponse.php @@ -2,12 +2,12 @@ namespace Eppo\DTO; -use Eppo\Traits\StaticFromJson; +use Eppo\Traits\StaticFromArray; use Eppo\Traits\ToArray; class BanditParametersResponse { - use StaticFromJson; + use StaticFromArray; use ToArray; public array $bandits; diff --git a/src/DTO/BanditReference.php b/src/DTO/BanditReference.php index ba96495..ce7ed35 100644 --- a/src/DTO/BanditReference.php +++ b/src/DTO/BanditReference.php @@ -14,17 +14,17 @@ public function __construct( ) { } - public static function fromJson(mixed $json): BanditReference + public static function fromArray(mixed $arr): BanditReference { $flagVariations = []; - if (isset($json['flagVariations']) && is_array($json['flagVariations'])) { - foreach ($json['flagVariations'] as $variation) { - $flagVariations[] = BanditFlagVariation::fromJson($variation); + if (isset($arr['flagVariations']) && is_array($arr['flagVariations'])) { + foreach ($arr['flagVariations'] as $variation) { + $flagVariations[] = BanditFlagVariation::fromArray($variation); } } return new BanditReference( - modelVersion: $json['modelVersion'], + modelVersion: $arr['modelVersion'], flagVariations: $flagVariations ); } diff --git a/src/DTO/ConfigurationWire/ConfigResponse.php b/src/DTO/ConfigurationWire/ConfigResponse.php index 9901769..71b3990 100644 --- a/src/DTO/ConfigurationWire/ConfigResponse.php +++ b/src/DTO/ConfigurationWire/ConfigResponse.php @@ -2,13 +2,13 @@ namespace Eppo\DTO\ConfigurationWire; -use Eppo\Traits\StaticFromJson; +use Eppo\Traits\StaticFromArray; use Eppo\Traits\ToArray; class ConfigResponse { use ToArray; - use StaticFromJson; + use StaticFromArray; public function __construct( public string $response = "", diff --git a/src/DTO/ConfigurationWire/ConfigurationWire.php b/src/DTO/ConfigurationWire/ConfigurationWire.php index a87335b..e74e61e 100644 --- a/src/DTO/ConfigurationWire/ConfigurationWire.php +++ b/src/DTO/ConfigurationWire/ConfigurationWire.php @@ -30,10 +30,10 @@ public static function fromArray(array $arr): self $dto = new self(); $dto->version = $arr['version'] ?? 1; if (isset($arr['config'])) { - $dto->config = ConfigResponse::fromJson($arr['config']); + $dto->config = ConfigResponse::fromArray($arr['config']); } if (isset($arr['bandits'])) { - $dto->bandits = ConfigResponse::fromJson($arr['bandits']); + $dto->bandits = ConfigResponse::fromArray($arr['bandits']); } return $dto; } @@ -46,9 +46,9 @@ public static function fromResponses(ConfigResponse $flags, ?ConfigResponse $ban return $dto; } - public static function fromJsonString(string $jsonEncodedString): self + public static function fromJson(string $jsonEncodedObject): self { - return ConfigurationWire::fromArray(json_decode($jsonEncodedString, associative: true)); + return ConfigurationWire::fromArray(json_decode($jsonEncodedObject, associative: true)); } public function toJsonString(): string diff --git a/src/DTO/Flag.php b/src/DTO/Flag.php index 523b876..0e592b2 100644 --- a/src/DTO/Flag.php +++ b/src/DTO/Flag.php @@ -22,19 +22,19 @@ public function __construct( ) { } - public static function fromJson(array $json): Flag + public static function fromArray(array $arr): Flag { - $variationType = VariationType::from($json['variationType']); - $variations = self::parseVariations($json['variations'], $variationType); - $allocations = self::parseAllocations($json['allocations']); + $variationType = VariationType::from($arr['variationType']); + $variations = self::parseVariations($arr['variations'], $variationType); + $allocations = self::parseAllocations($arr['allocations']); return new Flag( - $json['key'], - $json['enabled'], + $arr['key'], + $arr['enabled'], $allocations, $variationType, $variations, - $json['totalShards'] + $arr['totalShards'] ); } diff --git a/src/DTO/FlagConfigResponse.php b/src/DTO/FlagConfigResponse.php index bc92efc..071dc83 100644 --- a/src/DTO/FlagConfigResponse.php +++ b/src/DTO/FlagConfigResponse.php @@ -2,12 +2,12 @@ namespace Eppo\DTO; -use Eppo\Traits\StaticFromJson; +use Eppo\Traits\StaticFromArray; use Eppo\Traits\ToArray; class FlagConfigResponse { - use StaticFromJson; + use StaticFromArray; use ToArray; public string $createdAt; @@ -17,10 +17,30 @@ class FlagConfigResponse public array $flags; /** - * @var array + * @var BanditReference[] */ public array $banditReferences; + public static function fromArray(array $arr): self + { + $dto = new self(); + $dto->format = $arr['format'] ?? 'SERVER'; + if (isset($arr['environment'])) { + $dto->environment = $arr['environment']; + } + if (isset($arr['flags'])) { + $dto->flags = array_map(function ($flag) { + return Flag::fromArray($flag); + }, $arr['flags']); + } + if (isset($arr['banditReferences'])) { + $dto->banditReferences = array_map(function ($banditReference) { + return BanditReference::fromArray($banditReference); + }, $arr['banditReferences']); + } + return $dto; + } + public function __construct() { $this->banditReferences = []; diff --git a/src/Traits/StaticFromJson.php b/src/Traits/StaticFromArray.php similarity index 65% rename from src/Traits/StaticFromJson.php rename to src/Traits/StaticFromArray.php index 54d2100..a37f306 100644 --- a/src/Traits/StaticFromJson.php +++ b/src/Traits/StaticFromArray.php @@ -4,13 +4,13 @@ namespace Eppo\Traits; -trait StaticFromJson +trait StaticFromArray { - public static function fromJson(array $values): self + public static function fromArray(array $arr): self { $dto = new self(); - foreach ($values as $key => $value) { + foreach ($arr as $key => $value) { if (property_exists($dto, $key)) { $dto->$key = $value; } diff --git a/tests/Config/ConfigurationLoaderTest.php b/tests/Config/ConfigurationLoaderTest.php index 0454568..c6a781c 100644 --- a/tests/Config/ConfigurationLoaderTest.php +++ b/tests/Config/ConfigurationLoaderTest.php @@ -9,6 +9,8 @@ use Eppo\Config\ConfigurationStore; use Eppo\DTO\Bandit\Bandit; use Eppo\DTO\Flag; +use Eppo\Exception\HttpRequestException; +use Eppo\Exception\InvalidApiKeyException; use Http\Discovery\Psr17Factory; use Http\Discovery\Psr18Client; use PHPUnit\Framework\TestCase; @@ -36,7 +38,7 @@ public function testLoadsConfiguration(): void "ETAG" ); $flagsJson = json_decode($flagsRaw, true); - $flags = array_map(fn($flag) => (Flag::fromJson($flag)), $flagsJson['flags']); + $flags = array_map(fn($flag) => (Flag::fromArray($flag)), $flagsJson['flags']); $banditsRaw = '{ "bandits": { "cold_start_bandit": { @@ -145,131 +147,135 @@ function (?string $eTag) use ($flagsResourceResponse, $flagsRaw) { $this->assertEquals($timestamp1, $configStore->getConfiguration()->getFetchedAt()); } -// public function testOnlyLoadsBanditsWhereNeeded(): void -// { -// // Set up mock response data. -// $initialFlagsRaw = '{ -// "flags": { -// }, -// "banditReferences": { -// "cold_starting_bandit": { -// "modelVersion": "cold start", -// "flagVariations": [ -// { -// "key": "cold_starting_bandit", -// "flagKey": "cold_start_flag", -// "allocationKey": "cold_start_allocation", -// "variationKey": "cold_starting_bandit", -// "variationValue": "cold_starting_bandit" -// } -// ] -// } -// } -// }'; -// -// $warmFlagsRaw = '{ -// "flags": { -// }, -// "banditReferences": { -// "cold_starting_bandit": { -// "modelVersion": "v1", -// "flagVariations": [ -// { -// "key": "cold_starting_bandit", -// "flagKey": "cold_start_flag", -// "allocationKey": "cold_start_allocation", -// "variationKey": "cold_starting_bandit", -// "variationValue": "cold_starting_bandit" -// } -// ] -// } -// } -// }'; -// -// $coldBanditsRaw = '{ -// "bandits": { -// "cold_starting_bandit" : { -// "banditKey": "cold_starting_bandit", -// "modelName": "falcon", -// "updatedAt": "2023-09-13T04:52:06.462Z", -// "modelVersion": "cold start", -// "modelData": { -// "gamma": 1.0, -// "defaultActionScore": 0.0, -// "actionProbabilityFloor": 0.0, -// "coefficients": {} -// } -// } -// } -// }'; -// -// $warmBanditsRaw = '{ -// "bandits": { -// "cold_starting_bandit" : { -// "banditKey": "cold_starting_bandit", -// "modelName": "falcon", -// "updatedAt": "2023-09-13T04:52:06.462Z", -// "modelVersion": "v1", -// "modelData": { -// "gamma": 1.0, -// "defaultActionScore": 0.0, -// "actionProbabilityFloor": 0.0, -// "coefficients": {} -// } -// } -// } -// }'; -// -// -// $apiWrapper = $this->getMockBuilder(APIRequestWrapper::class)->disableOriginalConstructor()->getMock(); -// -// $apiWrapper->expects($this->exactly(3)) -// ->method('getUFC') -// ->willReturnOnConsecutiveCalls( -// new APIResource($initialFlagsRaw, true, "initial"), -// new APIResource($initialFlagsRaw, true, "initialButForced"), -// new APIResource($warmFlagsRaw, true, "warm"), -// ); -// -// $apiWrapper->expects($this->exactly(2)) -// ->method('getBandits') -// ->willReturnOnConsecutiveCalls( -// new APIResource($coldBanditsRaw, true, null), -// new APIResource($warmBanditsRaw, true, null), -// ); -// -// $configStore = new ConfigStore(DefaultCacheFactory::create()); -// $loader = new ConfigurationLoader($apiWrapper, $configStore); -// -// -// // First fetch has the bandit cold -// $loader->fetchAndStoreConfiguration(null); -// -// $bandit = $loader->getBandit('cold_starting_bandit'); -// $this->assertNotNull($bandit); -// $this->assertInstanceOf(Bandit::class, $bandit); -// $this->assertEquals('cold_starting_bandit', $bandit->banditKey); -// $this->assertEquals('cold start', $bandit->modelVersion); -// -// -// // Trigger a reload, second fetch shows the bandit as still cold -// $loader->fetchAndStoreConfiguration('initial'); -// -// $bandit = $loader->getBandit('cold_starting_bandit'); -// $this->assertNotNull($bandit); -// $this->assertInstanceOf(Bandit::class, $bandit); -// $this->assertEquals('cold_starting_bandit', $bandit->banditKey); -// $this->assertEquals('cold start', $bandit->modelVersion); -// -// // Trigger a reload, third fetch has the bandit warm with v1 -// $loader->fetchAndStoreConfiguration('initialButForced'); -// -// $bandit = $loader->getBandit('cold_starting_bandit'); -// $this->assertNotNull($bandit); -// $this->assertInstanceOf(Bandit::class, $bandit); -// $this->assertEquals('cold_starting_bandit', $bandit->banditKey); -// $this->assertEquals('v1', $bandit->modelVersion); -// } + /** + * @throws HttpRequestException + * @throws InvalidApiKeyException + */ + public function testOnlyLoadsBanditsWhereNeeded(): void + { + // Set up mock response data. + $initialFlagsRaw = '{ + "flags": { + }, + "banditReferences": { + "cold_starting_bandit": { + "modelVersion": "cold start", + "flagVariations": [ + { + "key": "cold_starting_bandit", + "flagKey": "cold_start_flag", + "allocationKey": "cold_start_allocation", + "variationKey": "cold_starting_bandit", + "variationValue": "cold_starting_bandit" + } + ] + } + } + }'; + + $warmFlagsRaw = '{ + "flags": { + }, + "banditReferences": { + "cold_starting_bandit": { + "modelVersion": "v1", + "flagVariations": [ + { + "key": "cold_starting_bandit", + "flagKey": "cold_start_flag", + "allocationKey": "cold_start_allocation", + "variationKey": "cold_starting_bandit", + "variationValue": "cold_starting_bandit" + } + ] + } + } + }'; + + $coldBanditsRaw = '{ + "bandits": { + "cold_starting_bandit" : { + "banditKey": "cold_starting_bandit", + "modelName": "falcon", + "updatedAt": "2023-09-13T04:52:06.462Z", + "modelVersion": "cold start", + "modelData": { + "gamma": 1.0, + "defaultActionScore": 0.0, + "actionProbabilityFloor": 0.0, + "coefficients": {} + } + } + } + }'; + + $warmBanditsRaw = '{ + "bandits": { + "cold_starting_bandit" : { + "banditKey": "cold_starting_bandit", + "modelName": "falcon", + "updatedAt": "2023-09-13T04:52:06.462Z", + "modelVersion": "v1", + "modelData": { + "gamma": 1.0, + "defaultActionScore": 0.0, + "actionProbabilityFloor": 0.0, + "coefficients": {} + } + } + } + }'; + + + $apiWrapper = $this->getMockBuilder(APIRequestWrapper::class)->disableOriginalConstructor()->getMock(); + + $apiWrapper->expects($this->exactly(3)) + ->method('getUFC') + ->willReturnOnConsecutiveCalls( + new APIResource($initialFlagsRaw, true, "initial"), + new APIResource($initialFlagsRaw, true, "initialButForced"), + new APIResource($warmFlagsRaw, true, "warm"), + ); + + $apiWrapper->expects($this->exactly(2)) + ->method('getBandits') + ->willReturnOnConsecutiveCalls( + new APIResource($coldBanditsRaw, true, null), + new APIResource($warmBanditsRaw, true, null), + ); + + $configStore = new ConfigurationStore(new MockCache()); + $loader = new ConfigurationLoader($apiWrapper, $configStore); + + + // First fetch has the bandit cold + $loader->fetchAndStoreConfiguration(null); + + $bandit = $configStore->getConfiguration()->getBandit('cold_starting_bandit'); + $this->assertNotNull($bandit); + $this->assertInstanceOf(Bandit::class, $bandit); + $this->assertEquals('cold_starting_bandit', $bandit->banditKey); + $this->assertEquals('cold start', $bandit->modelVersion); + + + // Trigger a reload, second fetch shows the bandit as still cold + $loader->fetchAndStoreConfiguration('initial'); + + $bandit = $configStore->getConfiguration()->getBandit('cold_starting_bandit'); + $this->assertNotNull($bandit); + $this->assertInstanceOf(Bandit::class, $bandit); + $this->assertEquals('cold_starting_bandit', $bandit->banditKey); + $this->assertEquals('cold start', $bandit->modelVersion); + + // Trigger a reload, third fetch has the bandit warm with v1 + $loader->fetchAndStoreConfiguration('initialButForced'); + + $bandit = $configStore->getConfiguration()->getBandit('cold_starting_bandit'); + $this->assertNotNull($bandit); + $this->assertInstanceOf(Bandit::class, $bandit); + $this->assertEquals('cold_starting_bandit', $bandit->banditKey); + $this->assertEquals('v1', $bandit->modelVersion); + } public function testRunsWithoutBandits(): void { diff --git a/tests/DTO/BanditDTOTest.php b/tests/DTO/BanditDTOTest.php index 98ac48d..fe409c0 100644 --- a/tests/DTO/BanditDTOTest.php +++ b/tests/DTO/BanditDTOTest.php @@ -113,7 +113,7 @@ class BanditDTOTest extends TestCase public function testParsesFromJson(): void { $json = json_decode(self::BANDIT_JSON, true); - $bandit = Bandit::fromJson($json); + $bandit = Bandit::fromArray($json); $this->assertNotNull($bandit); diff --git a/tests/DTO/FlagDTOTest.php b/tests/DTO/FlagDTOTest.php index 7069a14..4303dfa 100644 --- a/tests/DTO/FlagDTOTest.php +++ b/tests/DTO/FlagDTOTest.php @@ -17,7 +17,7 @@ public function testParsesComplexFlagPayload(): void { $ufcPayload = json_decode(file_get_contents(self::MOCK_DATA_FILENAME), true); $flags = $ufcPayload['flags']; - $flag = Flag::fromJson($flags[self::FLAG_KEY]); + $flag = Flag::fromArray($flags[self::FLAG_KEY]); $this->assertInstanceOf(Flag::class, $flag); diff --git a/tests/Traits/StaticCreateSelfTest.php b/tests/Traits/StaticFromArrayTest.php similarity index 87% rename from tests/Traits/StaticCreateSelfTest.php rename to tests/Traits/StaticFromArrayTest.php index 5a661e9..2b3f3c1 100644 --- a/tests/Traits/StaticCreateSelfTest.php +++ b/tests/Traits/StaticFromArrayTest.php @@ -5,7 +5,7 @@ use Eppo\DTO\ConfigurationWire\ConfigResponse; use PHPUnit\Framework\TestCase; -class StaticCreateSelfTest extends TestCase +class StaticFromArrayTest extends TestCase { public function testCreateCreatesObjectFromArray(): void { @@ -16,7 +16,7 @@ public function testCreateCreatesObjectFromArray(): void ]; // Leverage a class that uses the StaticCreateSelf trait. - $configResponse = ConfigResponse::fromJson($data); + $configResponse = ConfigResponse::fromArray($data); $this->assertInstanceOf(ConfigResponse::class, $configResponse); $this->assertEquals('{"key": "value"}', $configResponse->response);