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
{
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 */
30 * I want to do this task and I need to do it myself.
32 * @return Locked/Error
34 abstract function acquireForMe();
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();
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();
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;
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 ) {
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
);
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() {
123 * A work not so good (eg. expired one) but better than an error
125 * @return mixed work result or false
127 function fallback() {
132 * Do something with the error, like showing it to the user.
134 function error( $status ) {
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();
152 $status = $this->poolCounter
->acquireForMe();
155 if ( $status->isOK() ) {
156 switch ( $status->value
) {
157 case PoolCounter
::LOCKED
:
158 $result = $this->doWork();
159 $this->poolCounter
->release();
162 case PoolCounter
::DONE
:
163 $result = $this->getCachedWork();
164 if ( $result === false ) {
165 /* That someone else work didn't serve us.
166 * Acquire the lock for me
168 return $this->execute( true );
172 case PoolCounter
::QUEUE_FULL
:
173 case PoolCounter
::TIMEOUT
:
174 $result = $this->fallback();
176 if ( $result !== false ) {
181 /* These two cases should never be hit... */
182 case PoolCounter
::ERROR
:
184 $errors = array( PoolCounter
::QUEUE_FULL
=> 'pool-queuefull', PoolCounter
::TIMEOUT
=> 'pool-timeout' );
186 $status = Status
::newFatal( isset($errors[$status->value
]) ?
$errors[$status->value
] : 'pool-errorunknown' );
187 /* continue to the error */
190 $this->logError( $status );
191 return $this->error( $status );
194 function __construct( $type, $key ) {
195 $this->poolCounter
= PoolCounter
::factory( $type, $key );