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.
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.
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
21 namespace MediaWiki\PoolCounter
;
23 use MediaWiki\MediaWikiServices
;
24 use MediaWiki\Status\Status
;
27 * Class for dealing with PoolCounters using class members
29 abstract class PoolCounterWork
{
31 protected $type = 'generic';
33 protected $cacheable = false; // does this override getCachedWork() ?
34 /** @var PoolCounter */
38 * @param string $type The class of actions to limit concurrency for (task type)
39 * @param string $key Key that identifies the queue this work is placed on
40 * @param PoolCounter|null $poolCounter
42 public function __construct( string $type, string $key, ?PoolCounter
$poolCounter = null ) {
45 $this->poolCounter
= $poolCounter ??
46 MediaWikiServices
::getInstance()->getPoolCounterFactory()->create( $type, $key );
50 * Actually perform the work, caching it if needed
52 * @return mixed|false Work result or false
54 abstract public function doWork();
57 * Retrieve the work from cache
59 * @return mixed|false Work result or false
61 public function getCachedWork() {
66 * A work not so good (eg. expired one) but better than an error
69 * @param bool $fast True if PoolCounter is requesting a fast stale response (pre-wait)
70 * @return mixed|false Work result or false
72 public function fallback( $fast ) {
77 * Do something with the error, like showing it to the user.
79 * @param Status $status
82 public function error( $status ) {
87 * Should fast stale mode be used?
91 protected function isFastStaleEnabled() {
92 return $this->poolCounter
->isFastStaleEnabled();
98 * @param Status $status
101 public function logError( $status ) {
102 $key = $this->poolCounter
->getKey();
104 $this->poolCounter
->getLogger()->info(
105 "Pool key '$key' ({$this->type}): " .
106 $status->getMessage()->inLanguage( 'en' )->useDatabase( false )->text()
111 * Get the result of the work (whatever it is), or the result of the error() function.
113 * This returns the result of the one of the following methods:
115 * - doWork(): Applies if the work is exclusive or no other process
116 * is doing it, and on the condition that either this process
117 * successfully entered the pool or the pool counter is down.
118 * - doCachedWork(): Applies if the work is cacheable and this blocked on another
119 * process which finished the work.
120 * - fallback(): Applies for all remaining cases.
122 * If these all return false, then the result of error() is returned.
124 * In slow-stale mode, these three methods are called in the sequence given above, and
125 * the first non-false response is used. This means in case of concurrent cache-miss requests
126 * for the same revision, later ones will load on DBs and other backend services, and wait for
127 * earlier requests to succeed and then read out their saved result.
129 * In fast-stale mode, if other requests hold doWork lock already, we call fallback() first
130 * to let it try to find an acceptable return value. If fallback() returns false, then we
131 * will wait for the doWork lock, as for slow stale mode, including potentially calling
132 * fallback() a second time.
134 * @param bool $skipcache
137 public function execute( $skipcache = false ) {
138 if ( !$this->cacheable ||
$skipcache ) {
139 $status = $this->poolCounter
->acquireForMe();
141 if ( $this->isFastStaleEnabled() ) {
142 // In fast stale mode, check for existing locks by acquiring lock with 0 timeout
143 $status = $this->poolCounter
->acquireForAnyone( 0 );
144 if ( $status->isOK() && $status->value
=== PoolCounter
::TIMEOUT
) {
145 // Lock acquisition would block: try fallback
146 $staleResult = $this->fallback( true );
147 if ( $staleResult !== false ) {
150 // No fallback available, so wait for the lock
151 $status = $this->poolCounter
->acquireForAnyone();
152 } // else behave as if $status were returned in slow mode
154 $status = $this->poolCounter
->acquireForAnyone();
158 if ( !$status->isOK() ) {
159 // Respond gracefully to complete server breakage: just log it and do the work
160 $this->logError( $status );
161 return $this->doWork();
164 switch ( $status->value
) {
165 case PoolCounter
::LOCK_HELD
:
166 // Better to ignore nesting pool counter limits than to fail.
167 // Assume that the outer pool limiting is reasonable enough.
169 case PoolCounter
::LOCKED
:
171 return $this->doWork();
173 $this->poolCounter
->release();
175 // no fall-through, because try returns or throws
176 case PoolCounter
::DONE
:
177 $result = $this->getCachedWork();
178 if ( $result === false ) {
179 /* That someone else work didn't serve us.
180 * Acquire the lock for me
182 return $this->execute( true );
186 case PoolCounter
::QUEUE_FULL
:
187 case PoolCounter
::TIMEOUT
:
188 $result = $this->fallback( false );
190 if ( $result !== false ) {
195 /* These two cases should never be hit... */
196 case PoolCounter
::ERROR
:
199 PoolCounter
::QUEUE_FULL
=> 'pool-queuefull',
200 PoolCounter
::TIMEOUT
=> 'pool-timeout',
203 $status = Status
::newFatal( $errors[$status->value
] ??
'pool-errorunknown' );
204 $this->logError( $status );
205 return $this->error( $status );
210 /** @deprecated class alias since 1.42 */
211 class_alias( PoolCounterWork
::class, 'PoolCounterWork' );