Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
94.67% |
71 / 75 |
|
75.00% |
6 / 8 |
CRAP | |
0.00% |
0 / 1 |
ThresholdLookup | |
94.67% |
71 / 75 |
|
75.00% |
6 / 8 |
23.08 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
1 | |||
getRawThresholdData | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
getThresholds | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
fetchThresholds | |
50.00% |
2 / 4 |
|
0.00% |
0 / 1 |
2.50 | |||
fetchThresholdsFromCache | |
100.00% |
23 / 23 |
|
100.00% |
1 / 1 |
2 | |||
fetchThresholdsFromApi | |
90.48% |
19 / 21 |
|
0.00% |
0 / 1 |
7.04 | |||
prepareThresholdRequestParam | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
4 | |||
extractKeyPath | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
3 |
1 | <?php |
2 | /** |
3 | * This program is free software: you can redistribute it and/or modify |
4 | * it under the terms of the GNU General Public License as published by |
5 | * the Free Software Foundation, either version 3 of the License, or |
6 | * (at your option) any later version. |
7 | * |
8 | * This program is distributed in the hope that it will be useful, |
9 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
10 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
11 | * GNU General Public License for more details. |
12 | * |
13 | * You should have received a copy of the GNU General Public License |
14 | * along with this program. If not, see <http://www.gnu.org/licenses/>. |
15 | */ |
16 | |
17 | namespace ORES\Storage; |
18 | |
19 | use IBufferingStatsdDataFactory; |
20 | use MediaWiki\Config\Config; |
21 | use ORES\ORESService; |
22 | use ORES\ThresholdParser; |
23 | use Psr\Log\LoggerInterface; |
24 | use RuntimeException; |
25 | use WANObjectCache; |
26 | |
27 | class ThresholdLookup { |
28 | |
29 | /** |
30 | * @var ThresholdParser |
31 | */ |
32 | protected $thresholdParser; |
33 | |
34 | /** |
35 | * @var ModelLookup |
36 | */ |
37 | private $modelLookup; |
38 | |
39 | /** |
40 | * @var ORESService |
41 | */ |
42 | private $oresService; |
43 | |
44 | /** |
45 | * @var WANObjectCache |
46 | */ |
47 | private $cache; |
48 | |
49 | /** |
50 | * @var LoggerInterface |
51 | */ |
52 | private $logger; |
53 | |
54 | /** |
55 | * @var IBufferingStatsdDataFactory |
56 | */ |
57 | private $statsdDataFactory; |
58 | |
59 | /** |
60 | * @var Config |
61 | */ |
62 | protected $mainConfig; |
63 | |
64 | /** |
65 | * @param ThresholdParser $thresholdParser |
66 | * @param ModelLookup $modelLookup |
67 | * @param ORESService $oresService |
68 | * @param WANObjectCache $cache |
69 | * @param LoggerInterface $logger |
70 | * @param IBufferingStatsdDataFactory $statsdDataFactory |
71 | * @param Config $mainConfig |
72 | */ |
73 | public function __construct( |
74 | ThresholdParser $thresholdParser, |
75 | ModelLookup $modelLookup, |
76 | ORESService $oresService, |
77 | WANObjectCache $cache, |
78 | LoggerInterface $logger, |
79 | IBufferingStatsdDataFactory $statsdDataFactory, |
80 | Config $mainConfig |
81 | ) { |
82 | $this->thresholdParser = $thresholdParser; |
83 | $this->modelLookup = $modelLookup; |
84 | $this->oresService = $oresService; |
85 | $this->cache = $cache; |
86 | $this->logger = $logger; |
87 | $this->statsdDataFactory = $statsdDataFactory; |
88 | $this->mainConfig = $mainConfig; |
89 | } |
90 | |
91 | /** |
92 | * @param string $model |
93 | * @param bool $fromCache |
94 | * @return array|false|mixed |
95 | */ |
96 | public function getRawThresholdData( $model, $fromCache = true ) { |
97 | $config = $this->thresholdParser->getFiltersConfig( $model ); |
98 | if ( $config ) { |
99 | return $this->fetchThresholds( $model, $fromCache ); |
100 | } |
101 | return false; |
102 | } |
103 | |
104 | /** |
105 | * @param string $model |
106 | * @param bool $fromCache |
107 | * @return array |
108 | */ |
109 | public function getThresholds( $model, $fromCache = true ) { |
110 | $stats = $this->getRawThresholdData( $model, $fromCache ); |
111 | if ( $stats !== false ) { |
112 | return $this->thresholdParser->parseThresholds( $stats, $model ); |
113 | } |
114 | return []; |
115 | } |
116 | |
117 | /** |
118 | * @param string $model |
119 | * @param bool $fromCache |
120 | * @return array|false|mixed |
121 | */ |
122 | private function fetchThresholds( $model, $fromCache ) { |
123 | if ( $fromCache ) { |
124 | return $this->fetchThresholdsFromCache( $model ); |
125 | } else { |
126 | $this->logger->info( 'Forcing stats fetch, bypassing cache.' ); |
127 | return $this->fetchThresholdsFromApi( $model ); |
128 | } |
129 | } |
130 | |
131 | /** |
132 | * @param string $model |
133 | * @return false|mixed |
134 | */ |
135 | private function fetchThresholdsFromCache( $model ) { |
136 | global $wgOresCacheVersion; |
137 | $modelVersion = $this->modelLookup->getModelVersion( $model ); |
138 | $key = $this->cache->makeKey( |
139 | 'ores_threshold_statistics', |
140 | $model, |
141 | $modelVersion, |
142 | $wgOresCacheVersion, |
143 | md5( json_encode( $this->thresholdParser->getFiltersConfig( $model ) ) ) |
144 | ); |
145 | return $this->cache->getWithSetCallback( |
146 | $key, |
147 | WANObjectCache::TTL_DAY, |
148 | function ( $oldValue, &$ttl, &$setOpts, $opts ) use ( $model ) { |
149 | try { |
150 | $result = $this->fetchThresholdsFromApi( $model ); |
151 | $this->statsdDataFactory->increment( 'ores.api.stats.ok' ); |
152 | |
153 | return $result; |
154 | } catch ( RuntimeException $ex ) { |
155 | $this->statsdDataFactory->increment( 'ores.api.stats.failed' ); |
156 | $this->logger->error( 'Failed to fetch ORES stats.' ); |
157 | |
158 | $ttl = WANObjectCache::TTL_MINUTE; |
159 | return []; |
160 | } |
161 | }, |
162 | [ 'lockTSE' => 10, 'pcTTL' => WANObjectCache::TTL_PROC_LONG ] |
163 | ); |
164 | } |
165 | |
166 | /** |
167 | * @param string $model |
168 | * @return array |
169 | */ |
170 | protected function fetchThresholdsFromApi( $model ) { |
171 | $formulae = [ 'true' => [], 'false' => [] ]; |
172 | $calculatedThresholds = []; |
173 | foreach ( $this->thresholdParser->getFiltersConfig( $model ) as $levelName => $config ) { |
174 | if ( $config === false ) { |
175 | continue; |
176 | } |
177 | $this->prepareThresholdRequestParam( $config, $formulae, $calculatedThresholds ); |
178 | } |
179 | |
180 | if ( count( $calculatedThresholds ) === 0 ) { |
181 | return []; |
182 | } |
183 | |
184 | $data = $this->oresService->request( |
185 | [ 'models' => $model, 'model_info' => implode( "|", $calculatedThresholds ) ] |
186 | ); |
187 | |
188 | $prefix = [ ORESService::getWikiID(), 'models', $model, 'statistics', 'thresholds' ]; |
189 | $resultMap = []; |
190 | |
191 | foreach ( $formulae as $outcome => $outcomeFormulae ) { |
192 | if ( !$outcomeFormulae ) { |
193 | continue; |
194 | } |
195 | |
196 | $pathParts = array_merge( $prefix, [ $outcome ] ); |
197 | $result = $this->extractKeyPath( $data, $pathParts ); |
198 | |
199 | foreach ( $outcomeFormulae as $index => $formula ) { |
200 | $resultMap[$outcome][$formula] = $result[$index]; |
201 | } |
202 | } |
203 | |
204 | return $resultMap; |
205 | } |
206 | |
207 | /** |
208 | * @param string[] $config associative array mapping boundaries to old formulas |
209 | * @param array[] &$formulae associative array mapping boundaries to new formulas |
210 | * @param string[] &$calculatedThresholds array that has threshold request param |
211 | */ |
212 | protected function prepareThresholdRequestParam( |
213 | array $config, |
214 | array &$formulae, |
215 | array &$calculatedThresholds |
216 | ) { |
217 | foreach ( $config as $bound => $formula ) { |
218 | $outcome = ( $bound === 'min' ) ? 'true' : 'false'; |
219 | if ( strpos( $formula, '@' ) !== false ) { |
220 | $calculatedThresholds[] = "statistics.thresholds.{$outcome}.\"{$formula}\""; |
221 | $formulae[$outcome][] = $formula; |
222 | } |
223 | } |
224 | } |
225 | |
226 | /** |
227 | * @param array $data |
228 | * @param string[] $keyPath |
229 | * |
230 | * @return array |
231 | */ |
232 | protected function extractKeyPath( $data, $keyPath ) { |
233 | $current = $data; |
234 | foreach ( $keyPath as $key ) { |
235 | if ( !isset( $current[$key] ) ) { |
236 | $fullPath = implode( '.', $keyPath ); |
237 | throw new RuntimeException( "Failed to parse data at key [{$fullPath}]" ); |
238 | } |
239 | $current = $current[$key]; |
240 | } |
241 | |
242 | return $current; |
243 | } |
244 | |
245 | } |