Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
63.27% |
31 / 49 |
|
42.86% |
3 / 7 |
CRAP | |
0.00% |
0 / 1 |
PoolCounterWork | |
64.58% |
31 / 48 |
|
42.86% |
3 / 7 |
49.59 | |
0.00% |
0 / 1 |
__construct | |
66.67% |
2 / 3 |
|
0.00% |
0 / 1 |
1.04 | |||
doWork | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
getCachedWork | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
fallback | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
error | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
isFastStaleEnabled | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
logError | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
execute | |
72.22% |
26 / 36 |
|
0.00% |
0 / 1 |
24.94 |
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 2 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 along |
14 | * with this program; if not, write to the Free Software Foundation, Inc., |
15 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
16 | * http://www.gnu.org/copyleft/gpl.html |
17 | * |
18 | * @file |
19 | */ |
20 | |
21 | namespace MediaWiki\PoolCounter; |
22 | |
23 | use LogicException; |
24 | use MediaWiki\MediaWikiServices; |
25 | use MediaWiki\Status\Status; |
26 | |
27 | /** |
28 | * Class for dealing with PoolCounters using class members |
29 | */ |
30 | abstract class PoolCounterWork { |
31 | /** @var string */ |
32 | protected $type = 'generic'; |
33 | /** @var bool */ |
34 | protected $cacheable = false; // does this override getCachedWork() ? |
35 | /** @var PoolCounter */ |
36 | private $poolCounter; |
37 | |
38 | /** |
39 | * @param string $type The class of actions to limit concurrency for (task type) |
40 | * @param string $key Key that identifies the queue this work is placed on |
41 | * @param PoolCounter|null $poolCounter |
42 | */ |
43 | public function __construct( string $type, string $key, ?PoolCounter $poolCounter = null ) { |
44 | $this->type = $type; |
45 | // MW >= 1.35 |
46 | $this->poolCounter = $poolCounter ?? |
47 | MediaWikiServices::getInstance()->getPoolCounterFactory()->create( $type, $key ); |
48 | } |
49 | |
50 | /** |
51 | * Actually perform the work, caching it if needed |
52 | * |
53 | * @return mixed|false Work result or false |
54 | */ |
55 | abstract public function doWork(); |
56 | |
57 | /** |
58 | * Retrieve the work from cache |
59 | * |
60 | * @return mixed|false Work result or false |
61 | */ |
62 | public function getCachedWork() { |
63 | return false; |
64 | } |
65 | |
66 | /** |
67 | * A work not so good (eg. expired one) but better than an error |
68 | * message. |
69 | * |
70 | * @param bool $fast True if PoolCounter is requesting a fast stale response (pre-wait) |
71 | * @return mixed|false Work result or false |
72 | */ |
73 | public function fallback( $fast ) { |
74 | return false; |
75 | } |
76 | |
77 | /** |
78 | * Do something with the error, like showing it to the user. |
79 | * |
80 | * @param Status $status |
81 | * @return mixed|false |
82 | */ |
83 | public function error( $status ) { |
84 | return false; |
85 | } |
86 | |
87 | /** |
88 | * Should fast stale mode be used? |
89 | * |
90 | * @return bool |
91 | */ |
92 | protected function isFastStaleEnabled() { |
93 | return $this->poolCounter->isFastStaleEnabled(); |
94 | } |
95 | |
96 | /** |
97 | * Log an error |
98 | * |
99 | * @param Status $status |
100 | * @return void |
101 | */ |
102 | public function logError( $status ) { |
103 | $key = $this->poolCounter->getKey(); |
104 | |
105 | $this->poolCounter->getLogger()->info( |
106 | "Pool key '$key' ({$this->type}): " . |
107 | $status->getMessage()->inLanguage( 'en' )->useDatabase( false )->text() |
108 | ); |
109 | } |
110 | |
111 | /** |
112 | * Get the result of the work (whatever it is), or the result of the error() function. |
113 | * |
114 | * This returns the result of the one of the following methods: |
115 | * |
116 | * - doWork(): Applies if the work is exclusive or no other process |
117 | * is doing it, and on the condition that either this process |
118 | * successfully entered the pool or the pool counter is down. |
119 | * - doCachedWork(): Applies if the work is cacheable and this blocked on another |
120 | * process which finished the work. |
121 | * - fallback(): Applies for all remaining cases. |
122 | * |
123 | * If these all return false, then the result of error() is returned. |
124 | * |
125 | * In slow-stale mode, these three methods are called in the sequence given above, and |
126 | * the first non-false response is used. This means in case of concurrent cache-miss requests |
127 | * for the same revision, later ones will load on DBs and other backend services, and wait for |
128 | * earlier requests to succeed and then read out their saved result. |
129 | * |
130 | * In fast-stale mode, if other requests hold doWork lock already, we call fallback() first |
131 | * to let it try to find an acceptable return value. If fallback() returns false, then we |
132 | * will wait for the doWork lock, as for slow stale mode, including potentially calling |
133 | * fallback() a second time. |
134 | * |
135 | * @param bool $skipcache |
136 | * @return mixed |
137 | */ |
138 | public function execute( $skipcache = false ) { |
139 | if ( !$this->cacheable || $skipcache ) { |
140 | $status = $this->poolCounter->acquireForMe(); |
141 | $skipcache = true; |
142 | } else { |
143 | if ( $this->isFastStaleEnabled() ) { |
144 | // In fast stale mode, check for existing locks by acquiring lock with 0 timeout |
145 | $status = $this->poolCounter->acquireForAnyone( 0 ); |
146 | if ( $status->isOK() && $status->value === PoolCounter::TIMEOUT ) { |
147 | // Lock acquisition would block: try fallback |
148 | $staleResult = $this->fallback( true ); |
149 | if ( $staleResult !== false ) { |
150 | return $staleResult; |
151 | } |
152 | // No fallback available, so wait for the lock |
153 | $status = $this->poolCounter->acquireForAnyone(); |
154 | } // else behave as if $status were returned in slow mode |
155 | } else { |
156 | $status = $this->poolCounter->acquireForAnyone(); |
157 | } |
158 | } |
159 | |
160 | if ( !$status->isOK() ) { |
161 | // Respond gracefully to complete server breakage: just log it and do the work |
162 | $this->logError( $status ); |
163 | return $this->doWork(); |
164 | } |
165 | |
166 | switch ( $status->value ) { |
167 | case PoolCounter::LOCK_HELD: |
168 | // Better to ignore nesting pool counter limits than to fail. |
169 | // Assume that the outer pool limiting is reasonable enough. |
170 | /* no break */ |
171 | case PoolCounter::LOCKED: |
172 | try { |
173 | return $this->doWork(); |
174 | } finally { |
175 | $this->poolCounter->release(); |
176 | } |
177 | // no fall-through, because try returns or throws |
178 | case PoolCounter::DONE: |
179 | $result = $this->getCachedWork(); |
180 | if ( $result === false ) { |
181 | if ( $skipcache ) { |
182 | // We shouldn't get here, because we called acquireForMe(). |
183 | // which should not return DONE. If we do get here, this |
184 | // indicates a faulty test mock. Report the issue instead |
185 | // of calling $this->execute( true ) in endless recursion. |
186 | throw new LogicException( |
187 | 'Got PoolCounter::DONE from acquireForMe() and ' . |
188 | 'getCachedWork() returned nothing' |
189 | ); |
190 | } |
191 | |
192 | /* That someone else work didn't serve us. |
193 | * Acquire the lock for me |
194 | */ |
195 | return $this->execute( true ); |
196 | } |
197 | return $result; |
198 | |
199 | case PoolCounter::QUEUE_FULL: |
200 | case PoolCounter::TIMEOUT: |
201 | $result = $this->fallback( false ); |
202 | |
203 | if ( $result !== false ) { |
204 | return $result; |
205 | } |
206 | /* no break */ |
207 | |
208 | /* These two cases should never be hit... */ |
209 | case PoolCounter::ERROR: |
210 | default: |
211 | $errors = [ |
212 | PoolCounter::QUEUE_FULL => 'pool-queuefull', |
213 | PoolCounter::TIMEOUT => 'pool-timeout', |
214 | ]; |
215 | |
216 | $status = Status::newFatal( $errors[$status->value] ?? 'pool-errorunknown' ); |
217 | $this->logError( $status ); |
218 | return $this->error( $status ); |
219 | } |
220 | } |
221 | } |
222 | |
223 | /** @deprecated class alias since 1.42 */ |
224 | class_alias( PoolCounterWork::class, 'PoolCounterWork' ); |