]> scripts.mit.edu Git - autoinstallsdev/mediawiki.git/blob - includes/poolcounter/PoolCounter.php
MediaWiki 1.30.2-scripts2
[autoinstallsdev/mediawiki.git] / includes / poolcounter / PoolCounter.php
1 <?php
2 /**
3  * Provides of semaphore semantics for restricting the number
4  * of workers that may be concurrently performing the same task.
5  *
6  * This program is free software; you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation; either version 2 of the License, or
9  * (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License along
17  * with this program; if not, write to the Free Software Foundation, Inc.,
18  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19  * http://www.gnu.org/copyleft/gpl.html
20  *
21  * @file
22  */
23
24 /**
25  * When you have many workers (threads/servers) giving service, and a
26  * cached item expensive to produce expires, you may get several workers
27  * doing the job at the same time.
28  *
29  * Given enough requests and the item expiring fast (non-cacheable,
30  * lots of edits...) that single work can end up unfairly using most (all)
31  * of the cpu of the pool. This is also known as 'Michael Jackson effect'
32  * since this effect triggered on the english wikipedia on the day Michael
33  * Jackson died, the biographical article got hit with several edits per
34  * minutes and hundreds of read hits.
35  *
36  * The PoolCounter provides semaphore semantics for restricting the number
37  * of workers that may be concurrently performing such single task. Only one
38  * key can be locked by any PoolCounter instance of a process, except for keys
39  * that start with "nowait:". However, only 0 timeouts (non-blocking requests)
40  * can be used with "nowait:" keys.
41  *
42  * By default PoolCounter_Stub is used, which provides no locking. You
43  * can get a useful one in the PoolCounter extension.
44  */
45 abstract class PoolCounter {
46         /* Return codes */
47         const LOCKED = 1; /* Lock acquired */
48         const RELEASED = 2; /* Lock released */
49         const DONE = 3; /* Another worker did the work for you */
50
51         const ERROR = -1; /* Indeterminate error */
52         const NOT_LOCKED = -2; /* Called release() with no lock held */
53         const QUEUE_FULL = -3; /* There are already maxqueue workers on this lock */
54         const TIMEOUT = -4; /* Timeout exceeded */
55         const LOCK_HELD = -5; /* Cannot acquire another lock while you have one lock held */
56
57         /** @var string All workers with the same key share the lock */
58         protected $key;
59         /** @var int Maximum number of workers working on tasks with the same key simultaneously */
60         protected $workers;
61         /**
62          * Maximum number of workers working on this task type, regardless of key.
63          * 0 means unlimited. Max allowed value is 65536.
64          * The way the slot limit is enforced is overzealous - this option should be used with caution.
65          * @var int
66          */
67         protected $slots = 0;
68         /** @var int If this number of workers are already working/waiting, fail instead of wait */
69         protected $maxqueue;
70         /** @var float Maximum time in seconds to wait for the lock */
71         protected $timeout;
72
73         /**
74          * @var bool Whether the key is a "might wait" key
75          */
76         private $isMightWaitKey;
77         /**
78          * @var bool Whether this process holds a "might wait" lock key
79          */
80         private static $acquiredMightWaitKey = 0;
81
82         /**
83          * @param array $conf
84          * @param string $type The class of actions to limit concurrency for (task type)
85          * @param string $key
86          */
87         protected function __construct( $conf, $type, $key ) {
88                 $this->workers = $conf['workers'];
89                 $this->maxqueue = $conf['maxqueue'];
90                 $this->timeout = $conf['timeout'];
91                 if ( isset( $conf['slots'] ) ) {
92                         $this->slots = $conf['slots'];
93                 }
94
95                 if ( $this->slots ) {
96                         $key = $this->hashKeyIntoSlots( $type, $key, $this->slots );
97                 }
98
99                 $this->key = $key;
100                 $this->isMightWaitKey = !preg_match( '/^nowait:/', $this->key );
101         }
102
103         /**
104          * Create a Pool counter. This should only be called from the PoolWorks.
105          *
106          * @param string $type The class of actions to limit concurrency for (task type)
107          * @param string $key
108          *
109          * @return PoolCounter
110          */
111         public static function factory( $type, $key ) {
112                 global $wgPoolCounterConf;
113                 if ( !isset( $wgPoolCounterConf[$type] ) ) {
114                         return new PoolCounter_Stub;
115                 }
116                 $conf = $wgPoolCounterConf[$type];
117                 $class = $conf['class'];
118
119                 return new $class( $conf, $type, $key );
120         }
121
122         /**
123          * @return string
124          */
125         public function getKey() {
126                 return $this->key;
127         }
128
129         /**
130          * I want to do this task and I need to do it myself.
131          *
132          * @return Status Value is one of Locked/Error
133          */
134         abstract public function acquireForMe();
135
136         /**
137          * I want to do this task, but if anyone else does it
138          * instead, it's also fine for me. I will read its cached data.
139          *
140          * @return Status Value is one of Locked/Done/Error
141          */
142         abstract public function acquireForAnyone();
143
144         /**
145          * I have successfully finished my task.
146          * Lets another one grab the lock, and returns the workers
147          * waiting on acquireForAnyone()
148          *
149          * @return Status Value is one of Released/NotLocked/Error
150          */
151         abstract public function release();
152
153         /**
154          * Checks that the lock request is sane.
155          * @return Status - good for sane requests fatal for insane
156          * @since 1.25
157          */
158         final protected function precheckAcquire() {
159                 if ( $this->isMightWaitKey ) {
160                         if ( self::$acquiredMightWaitKey ) {
161                                 /*
162                                  * The poolcounter itself is quite happy to allow you to wait
163                                  * on another lock while you have a lock you waited on already
164                                  * but we think that it is unlikely to be a good idea.  So we
165                                  * made it an error.  If you are _really_ _really_ sure it is a
166                                  * good idea then feel free to implement an unsafe flag or
167                                  * something.
168                                  */
169                                 return Status::newFatal( 'poolcounter-usage-error',
170                                         'You may only aquire a single non-nowait lock.' );
171                         }
172                 } elseif ( $this->timeout !== 0 ) {
173                         return Status::newFatal( 'poolcounter-usage-error',
174                                 'Locks starting in nowait: must have 0 timeout.' );
175                 }
176                 return Status::newGood();
177         }
178
179         /**
180          * Update any lock tracking information when the lock is acquired
181          * @since 1.25
182          */
183         final protected function onAcquire() {
184                 self::$acquiredMightWaitKey |= $this->isMightWaitKey;
185         }
186
187         /**
188          * Update any lock tracking information when the lock is released
189          * @since 1.25
190          */
191         final protected function onRelease() {
192                 self::$acquiredMightWaitKey &= !$this->isMightWaitKey;
193         }
194
195         /**
196          * Given a key (any string) and the number of lots, returns a slot key (a prefix with a suffix
197          * integer from the [0..($slots-1)] range). This is used for a global limit on the number of
198          * instances of a given type that can acquire a lock. The hashing is deterministic so that
199          * PoolCounter::$workers is always an upper limit of how many instances with the same key
200          * can acquire a lock.
201          *
202          * @param string $type The class of actions to limit concurrency for (task type)
203          * @param string $key PoolCounter instance key (any string)
204          * @param int $slots The number of slots (max allowed value is 65536)
205          * @return string Slot key with the type and slot number
206          */
207         protected function hashKeyIntoSlots( $type, $key, $slots ) {
208                 return $type . ':' . ( hexdec( substr( sha1( $key ), 0, 4 ) ) % $slots );
209         }
210 }
211
212 // @codingStandardsIgnoreStart Squiz.Classes.ValidClassName.NotCamelCaps
213 class PoolCounter_Stub extends PoolCounter {
214         // @codingStandardsIgnoreEnd
215
216         public function __construct() {
217                 /* No parameters needed */
218         }
219
220         public function acquireForMe() {
221                 return Status::newGood( PoolCounter::LOCKED );
222         }
223
224         public function acquireForAnyone() {
225                 return Status::newGood( PoolCounter::LOCKED );
226         }
227
228         public function release() {
229                 return Status::newGood( PoolCounter::RELEASED );
230         }
231 }