|
3 | 3 | namespace Eppo\Config; |
4 | 4 |
|
5 | 5 | use Eppo\API\APIRequestWrapper; |
6 | | -use Eppo\Bandits\BanditReferenceIndexer; |
7 | | -use Eppo\Bandits\IBanditReferenceIndexer; |
8 | | -use Eppo\Bandits\IBandits; |
9 | | -use Eppo\DTO\Bandit\Bandit; |
10 | | -use Eppo\DTO\BanditReference; |
11 | | -use Eppo\DTO\Flag; |
| 6 | +use Eppo\DTO\ConfigurationWire\ConfigResponse; |
| 7 | +use Eppo\DTO\FlagConfigResponse; |
12 | 8 | use Eppo\Exception\HttpRequestException; |
13 | 9 | use Eppo\Exception\InvalidApiKeyException; |
14 | | -use Eppo\Exception\InvalidConfigurationException; |
15 | | -use Eppo\Flags\IFlags; |
16 | | -use Eppo\UFCParser; |
17 | 10 |
|
18 | | -class ConfigurationLoader implements IFlags, IBandits |
| 11 | +class ConfigurationLoader |
19 | 12 | { |
20 | | - private const KEY_BANDIT_TIMESTAMP = "banditTimestamp"; |
21 | | - private const KEY_LOADED_BANDIT_VERSIONS = 'banditModelVersions'; |
22 | | - private UFCParser $parser; |
23 | | - |
24 | | - private const KEY_FLAG_TIMESTAMP = "flagTimestamp"; |
25 | | - private const KEY_FLAG_ETAG = "flagETag"; |
26 | | - |
27 | 13 | public function __construct( |
28 | 14 | private readonly APIRequestWrapper $apiRequestWrapper, |
29 | | - private readonly IConfigurationStore $configurationStore, |
30 | | - private readonly int $cacheAgeLimitMillis = 30 * 1000, |
31 | | - private readonly bool $optimizedBanditLoading = false |
| 15 | + public readonly ConfigurationStore $configurationStore, |
| 16 | + private readonly int $cacheAgeLimitMillis = 30 * 1000 |
32 | 17 | ) { |
33 | | - $this->parser = new UFCParser(); |
34 | | - } |
35 | | - |
36 | | - /** |
37 | | - * @throws InvalidApiKeyException |
38 | | - * @throws HttpRequestException |
39 | | - * @throws InvalidConfigurationException |
40 | | - */ |
41 | | - public function getFlag(string $key): ?Flag |
42 | | - { |
43 | | - $this->reloadConfigurationIfExpired(); |
44 | | - return $this->configurationStore->getFlag($key); |
45 | 18 | } |
46 | 19 |
|
47 | 20 | /** |
48 | | - * @param string $flagKey |
49 | | - * @param string $variation |
50 | | - * @return string|null |
51 | 21 | * @throws HttpRequestException |
52 | 22 | * @throws InvalidApiKeyException |
53 | | - * @throws InvalidConfigurationException |
54 | | - */ |
55 | | - public function getBanditByVariation(string $flagKey, string $variation): ?string |
56 | | - { |
57 | | - $this->reloadConfigurationIfExpired(); |
58 | | - return $this->configurationStore->getBanditReferenceIndexer()->getBanditByVariation($flagKey, $variation); |
59 | | - } |
60 | | - |
61 | | - /** |
62 | | - * @throws HttpRequestException |
63 | | - * @throws InvalidApiKeyException |
64 | | - * @throws InvalidConfigurationException |
65 | 23 | */ |
66 | 24 | public function reloadConfigurationIfExpired(): void |
67 | 25 | { |
68 | 26 | $flagCacheAge = $this->getCacheAgeInMillis(); |
69 | | - if ($flagCacheAge < 0 || $flagCacheAge >= ($this->cacheAgeLimitMillis)) { |
| 27 | + if ($flagCacheAge >= ($this->cacheAgeLimitMillis)) { |
70 | 28 | $this->reloadConfiguration(); |
71 | 29 | } |
72 | 30 | } |
73 | 31 |
|
74 | 32 | /** |
75 | 33 | * @throws HttpRequestException |
76 | 34 | * @throws InvalidApiKeyException |
77 | | - * @throws InvalidConfigurationException |
78 | 35 | */ |
79 | | - public function fetchAndStoreConfigurations(?string $flagETag): void |
| 36 | + public function fetchAndStoreConfiguration(?string $flagETag): void |
80 | 37 | { |
81 | 38 | $response = $this->apiRequestWrapper->getUFC($flagETag); |
82 | 39 | if ($response->isModified) { |
83 | | - // Decode and set the data. |
| 40 | + $configResponse = new ConfigResponse( |
| 41 | + $response->body, |
| 42 | + date('c'), |
| 43 | + $response->eTag |
| 44 | + ); |
84 | 45 | $responseData = json_decode($response->body, true); |
85 | | - if (!$responseData) { |
| 46 | + |
| 47 | + if ($responseData === null) { |
86 | 48 | syslog(LOG_WARNING, "[Eppo SDK] Empty or invalid response from the configuration server."); |
87 | 49 | return; |
88 | 50 | } |
89 | | - |
90 | | - $inflated = array_map(fn($object) => $this->parser->parseFlag($object), $responseData['flags']); |
91 | | - |
92 | | - // Create a handy helper class from the `banditReferences` to help connect flags to bandits. |
93 | | - if (isset($responseData['banditReferences'])) { |
94 | | - $banditReferences = array_map( |
95 | | - function ($json) { |
96 | | - return BanditReference::fromJson($json); |
97 | | - }, |
98 | | - $responseData['banditReferences'] |
99 | | - ); |
100 | | - $indexer = BanditReferenceIndexer::from($banditReferences); |
101 | | - } else { |
102 | | - syslog(LOG_WARNING, "[EPPO SDK] No bandit-flag variations found in UFC response."); |
103 | | - $indexer = BanditReferenceIndexer::empty(); |
| 51 | + $fcr = FlagConfigResponse::fromJson($responseData); |
| 52 | + $banditResponse = null; |
| 53 | + // TODO: Also check current bandit models loaded for optimized bandit loading. |
| 54 | + if (count($fcr->banditReferences) > 0) { |
| 55 | + $bandits = $this->apiRequestWrapper->getBandits(); |
| 56 | + $banditResponse = new ConfigResponse($bandits->body, date('c'), $bandits->eTag); |
104 | 57 | } |
105 | 58 |
|
106 | | - $this->configurationStore->setUnifiedFlagConfiguration($inflated, $indexer); |
107 | | - |
108 | | - // Only load bandits if there are any referenced by the flags. |
109 | | - if ($indexer->hasBandits()) { |
110 | | - $this->fetchBanditsAsRequired($indexer); |
111 | | - } |
112 | | - |
113 | | - // Store metadata for next time. |
114 | | - $this->configurationStore->setMetadata(self::KEY_FLAG_TIMESTAMP, $this->millitime()); |
115 | | - $this->configurationStore->setMetadata(self::KEY_FLAG_ETAG, $response->ETag); |
| 59 | + $configuration = Configuration::fromUfcResponses($configResponse, $banditResponse); |
| 60 | + $this->configurationStore->setConfiguration($configuration); |
116 | 61 | } |
117 | 62 | } |
118 | 63 |
|
119 | 64 | private function getCacheAgeInMillis(): int |
120 | 65 | { |
121 | | - $timestamp = $this->configurationStore->getMetadata(self::KEY_FLAG_TIMESTAMP); |
122 | | - if ($timestamp != null) { |
123 | | - return $this->millitime() - $timestamp; |
124 | | - } |
125 | | - return -1; |
126 | | - } |
127 | | - |
128 | | - public function getBanditReferenceIndexer(): IBanditReferenceIndexer |
129 | | - { |
130 | | - return $this->configurationStore->getBanditReferenceIndexer(); |
131 | | - } |
132 | | - |
133 | | - /** |
134 | | - * @throws HttpRequestException |
135 | | - * @throws InvalidApiKeyException |
136 | | - * @throws InvalidConfigurationException |
137 | | - */ |
138 | | - private function fetchAndStoreBandits(): void |
139 | | - { |
140 | | - $banditModelResponse = json_decode($this->apiRequestWrapper->getBandits()->body, true); |
141 | | - if (!$banditModelResponse || !isset($banditModelResponse['bandits'])) { |
142 | | - syslog(LOG_WARNING, "[Eppo SDK] Empty or invalid response from the configuration server."); |
143 | | - $bandits = []; |
144 | | - } else { |
145 | | - $bandits = array_map(fn($json) => Bandit::fromJson($json), $banditModelResponse['bandits']); |
| 66 | + $timestamp = $this->configurationStore->getConfiguration()->getFetchedAt(); |
| 67 | + if (!$timestamp) { |
| 68 | + return PHP_INT_MAX; |
146 | 69 | } |
147 | | - $banditModelVersions = array_map(fn($bandit) => $bandit->modelVersion, $bandits); |
148 | | - |
149 | | - $this->configurationStore->setBandits($bandits); |
150 | | - $this->configurationStore->setMetadata(self::KEY_LOADED_BANDIT_VERSIONS, $banditModelVersions); |
151 | | - $this->configurationStore->setMetadata(self::KEY_BANDIT_TIMESTAMP, time()); |
152 | | - } |
153 | | - |
154 | | - public function getBandit(string $banditKey): ?Bandit |
155 | | - { |
156 | | - return $this->configurationStore->getBandit($banditKey); |
157 | | - } |
158 | | - |
159 | | - /** |
160 | | - * Loads bandits unless `optimizedBanditLoading` is `true` in which case, currently loaded bandit models are |
161 | | - * compared to those required by flags to determine whether to (re)load bandit models. |
162 | | - * |
163 | | - * @param IBanditReferenceIndexer $indexer |
164 | | - * @return void |
165 | | - * @throws HttpRequestException |
166 | | - * @throws InvalidApiKeyException |
167 | | - * @throws InvalidConfigurationException |
168 | | - */ |
169 | | - private function fetchBanditsAsRequired(IBanditReferenceIndexer $indexer): void |
170 | | - { |
171 | | - // Get the currently loaded bandits to determine if they satisfy what's required by the flags |
172 | | - $currentlyLoadedBanditModels = $this->configurationStore->getMetadata( |
173 | | - self::KEY_LOADED_BANDIT_VERSIONS |
174 | | - ) ?? []; |
175 | | - $references = $indexer->getBanditModelKeys(); |
176 | | - |
177 | | - if (array_diff($references, $currentlyLoadedBanditModels)) { |
178 | | - $this->fetchAndStoreBandits(); |
| 70 | + try { |
| 71 | + $dateTime = new \DateTime($timestamp); |
| 72 | + $timestampMillis = (int)($dateTime->format('U.u') * 1000); |
| 73 | + return $this->milliTime() - $timestampMillis; |
| 74 | + } catch (\Exception $e) { |
| 75 | + return PHP_INT_MAX; |
179 | 76 | } |
180 | 77 | } |
181 | 78 |
|
182 | 79 | /** |
183 | 80 | * @return void |
184 | 81 | * @throws HttpRequestException |
185 | 82 | * @throws InvalidApiKeyException |
186 | | - * @throws InvalidConfigurationException |
187 | 83 | */ |
188 | 84 | public function reloadConfiguration(): void |
189 | 85 | { |
190 | | - $flagETag = $this->configurationStore->getMetadata(self::KEY_FLAG_ETAG); |
191 | | - $this->fetchAndStoreConfigurations($flagETag); |
| 86 | + $flagETag = $this->configurationStore->getConfiguration()->getFlagETag(); |
| 87 | + $this->fetchAndStoreConfiguration($flagETag); |
192 | 88 | } |
193 | 89 |
|
194 | | - private function millitime(): int |
| 90 | + private function milliTime(): int |
195 | 91 | { |
196 | 92 | return intval(microtime(true) * 1000); |
197 | 93 | } |
|
0 commit comments