Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
67.44% covered (warning)
67.44%
29 / 43
42.86% covered (danger)
42.86%
3 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
PoolCounterWork
69.05% covered (warning)
69.05%
29 / 42
42.86% covered (danger)
42.86%
3 / 7
38.69
0.00% covered (danger)
0.00%
0 / 1
 __construct
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
1.04
 doWork
n/a
0 / 0
n/a
0 / 0
0
 getCachedWork
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 fallback
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 error
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isFastStaleEnabled
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 logError
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 execute
80.00% covered (warning)
80.00%
24 / 30
0.00% covered (danger)
0.00%
0 / 1
19.31
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
21namespace MediaWiki\PoolCounter;
22
23use MediaWiki\MediaWikiServices;
24use MediaWiki\Status\Status;
25
26/**
27 * Class for dealing with PoolCounters using class members
28 */
29abstract class PoolCounterWork {
30    /** @var string */
31    protected $type = 'generic';
32    /** @var bool */
33    protected $cacheable = false; // does this override getCachedWork() ?
34    /** @var PoolCounter */
35    private $poolCounter;
36
37    /**
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
41     */
42    public function __construct( string $type, string $key, PoolCounter $poolCounter = null ) {
43        $this->type = $type;
44        // MW >= 1.35
45        $this->poolCounter = $poolCounter ??
46            MediaWikiServices::getInstance()->getPoolCounterFactory()->create( $type, $key );
47    }
48
49    /**
50     * Actually perform the work, caching it if needed
51     *
52     * @return mixed|false Work result or false
53     */
54    abstract public function doWork();
55
56    /**
57     * Retrieve the work from cache
58     *
59     * @return mixed|false Work result or false
60     */
61    public function getCachedWork() {
62        return false;
63    }
64
65    /**
66     * A work not so good (eg. expired one) but better than an error
67     * message.
68     *
69     * @param bool $fast True if PoolCounter is requesting a fast stale response (pre-wait)
70     * @return mixed|false Work result or false
71     */
72    public function fallback( $fast ) {
73        return false;
74    }
75
76    /**
77     * Do something with the error, like showing it to the user.
78     *
79     * @param Status $status
80     * @return mixed|false
81     */
82    public function error( $status ) {
83        return false;
84    }
85
86    /**
87     * Should fast stale mode be used?
88     *
89     * @return bool
90     */
91    protected function isFastStaleEnabled() {
92        return $this->poolCounter->isFastStaleEnabled();
93    }
94
95    /**
96     * Log an error
97     *
98     * @param Status $status
99     * @return void
100     */
101    public function logError( $status ) {
102        $key = $this->poolCounter->getKey();
103
104        $this->poolCounter->getLogger()->info(
105            "Pool key '$key' ({$this->type}): " .
106            $status->getMessage()->inLanguage( 'en' )->useDatabase( false )->text()
107        );
108    }
109
110    /**
111     * Get the result of the work (whatever it is), or the result of the error() function.
112     *
113     * This returns the result of the one of the following methods:
114     *
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.
121     *
122     * If these all return false, then the result of error() is returned.
123     *
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.
128     *
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.
133     *
134     * @param bool $skipcache
135     * @return mixed
136     */
137    public function execute( $skipcache = false ) {
138        if ( !$this->cacheable || $skipcache ) {
139            $status = $this->poolCounter->acquireForMe();
140        } else {
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 ) {
148                        return $staleResult;
149                    }
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
153            } else {
154                $status = $this->poolCounter->acquireForAnyone();
155            }
156        }
157
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();
162        }
163
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.
168                /* no break */
169            case PoolCounter::LOCKED:
170                try {
171                    return $this->doWork();
172                } finally {
173                    $this->poolCounter->release();
174                }
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
181                     */
182                    return $this->execute( true );
183                }
184                return $result;
185
186            case PoolCounter::QUEUE_FULL:
187            case PoolCounter::TIMEOUT:
188                $result = $this->fallback( false );
189
190                if ( $result !== false ) {
191                    return $result;
192                }
193            /* no break */
194
195            /* These two cases should never be hit... */
196            case PoolCounter::ERROR:
197            default:
198                $errors = [
199                    PoolCounter::QUEUE_FULL => 'pool-queuefull',
200                    PoolCounter::TIMEOUT => 'pool-timeout',
201                ];
202
203                $status = Status::newFatal( $errors[$status->value] ?? 'pool-errorunknown' );
204                $this->logError( $status );
205                return $this->error( $status );
206        }
207    }
208}
209
210/** @deprecated class alias since 1.41 */
211class_alias( PoolCounterWork::class, 'PoolCounterWork' );