Quick fix for bug 15892: intermittent SQL-based cache failures during parser test...
[mediawiki.git] / includes / PoolCounter.php
blob25bdd2d52a7d4c0da18f16946f7a351a7b1bd201
1 <?php
3 /**
4 * When you have many workers (threads/servers) giving service, and a
5 * cached item expensive to produce expires, you may get several workers
6 * doing the job at the same time.
7 * Given enough requests and the item expiring fast (non-cacheable,
8 * lots of edits...) that single work can end up unfairly using most (all)
9 * of the cpu of the pool. This is also known as 'Michael Jackson effect'.
10 * The PoolCounter provides semaphore semantics for restricting the number
11 * of workers that may be concurrently performing such single task.
13 * By default PoolCounter_Stub is used, which provides no locking. You
14 * can get a useful one in the PoolCounter extension.
16 abstract class PoolCounter {
18 /* Return codes */
19 const LOCKED = 1; /* Lock acquired */
20 const RELEASED = 2; /* Lock released */
21 const DONE = 3; /* Another one did the work for you */
23 const ERROR = -1; /* Indeterminate error */
24 const NOT_LOCKED = -2; /* Called release() with no lock held */
25 const QUEUE_FULL = -3; /* There are already maxqueue workers on this lock */
26 const TIMEOUT = -4; /* Timeout exceeded */
27 const LOCK_HELD = -5; /* Cannot acquire another lock while you have one lock held */
29 /**
30 * I want to do this task and I need to do it myself.
32 * @return Locked/Error
34 abstract function acquireForMe();
36 /**
37 * I want to do this task, but if anyone else does it
38 * instead, it's also fine for me. I will read its cached data.
40 * @return Locked/Done/Error
42 abstract function acquireForAnyone();
44 /**
45 * I have successfully finished my task.
46 * Lets another one grab the lock, and returns the workers
47 * waiting on acquireForAnyone()
49 * @return Released/NotLocked/Error
51 abstract function release();
53 /**
54 * $key: All workers with the same key share the lock.
55 * $workers: It wouldn't be a good idea to have more than this number of
56 * workers doing the task simultaneously.
57 * $maxqueue: If this number of workers are already working/waiting,
58 * fail instead of wait.
59 * $timeout: Maximum time in seconds to wait for the lock.
61 protected $key, $workers, $maxqueue, $timeout;
63 /**
64 * Create a Pool counter. This should only be called from the PoolWorks.
66 public static function factory( $type, $key ) {
67 global $wgPoolCounterConf;
68 if ( !isset( $wgPoolCounterConf[$type] ) ) {
69 return new PoolCounter_Stub;
71 $conf = $wgPoolCounterConf[$type];
72 $class = $conf['class'];
74 return new $class( $conf, $type, $key );
77 protected function __construct( $conf, $type, $key ) {
78 $this->key = $key;
79 $this->workers = $conf['workers'];
80 $this->maxqueue = $conf['maxqueue'];
81 $this->timeout = $conf['timeout'];
85 class PoolCounter_Stub extends PoolCounter {
86 function acquireForMe() {
87 return Status::newGood( PoolCounter::LOCKED );
90 function acquireForAnyone() {
91 return Status::newGood( PoolCounter::LOCKED );
94 function release() {
95 return Status::newGood( PoolCounter::RELEASED );
98 public function __construct() {
99 /* No parameters needed */
104 * Handy class for dealing with PoolCounters using class members instead of callbacks.
106 abstract class PoolCounterWork {
107 protected $cacheable = false; //Does this override getCachedWork() ?
110 * Actually perform the work, caching it if needed.
112 abstract function doWork();
115 * Retrieve the work from cache
116 * @return mixed work result or false
118 function getCachedWork() {
119 return false;
123 * A work not so good (eg. expired one) but better than an error
124 * message.
125 * @return mixed work result or false
127 function fallback() {
128 return false;
132 * Do something with the error, like showing it to the user.
134 function error( $status ) {
135 return false;
139 * Log an error
141 function logError( $status ) {
142 wfDebugLog( 'poolcounter', $status->getWikiText() );
146 * Get the result of the work (whatever it is), or false.
148 function execute( $skipcache = false ) {
149 if ( $this->cacheable && !$skipcache ) {
150 $status = $this->poolCounter->acquireForAnyone();
151 } else {
152 $status = $this->poolCounter->acquireForMe();
155 if ( !$status->isOK() ) {
156 // Respond gracefully to complete server breakage: just log it and do the work
157 $this->logError( $status );
158 return $this->doWork();
161 switch ( $status->value ) {
162 case PoolCounter::LOCKED:
163 $result = $this->doWork();
164 $this->poolCounter->release();
165 return $result;
167 case PoolCounter::DONE:
168 $result = $this->getCachedWork();
169 if ( $result === false ) {
170 /* That someone else work didn't serve us.
171 * Acquire the lock for me
173 return $this->execute( true );
175 return $result;
177 case PoolCounter::QUEUE_FULL:
178 case PoolCounter::TIMEOUT:
179 $result = $this->fallback();
181 if ( $result !== false ) {
182 return $result;
184 /* no break */
186 /* These two cases should never be hit... */
187 case PoolCounter::ERROR:
188 default:
189 $errors = array( PoolCounter::QUEUE_FULL => 'pool-queuefull', PoolCounter::TIMEOUT => 'pool-timeout' );
191 $status = Status::newFatal( isset($errors[$status->value]) ? $errors[$status->value] : 'pool-errorunknown' );
192 $this->logError( $status );
193 return $this->error( $status );
197 function __construct( $type, $key ) {
198 $this->poolCounter = PoolCounter::factory( $type, $key );