* Follow-up r84610: don't assume a Parser object is attached
[lhc/web/wiklou.git] / includes / PoolCounter.php
1 <?php
2
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.
12 *
13 * By default PoolCounter_Stub is used, which provides no locking. You
14 * can get a useful one in the PoolCounter extension.
15 */
16 abstract class PoolCounter {
17
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 */
22
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 */
28
29 /**
30 * I want to do this task and I need to do it myself.
31 *
32 * @return Locked/Error
33 */
34 abstract function acquireForMe();
35
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.
39 *
40 * @return Locked/Done/Error
41 */
42 abstract function acquireForAnyone();
43
44 /**
45 * I have successfully finished my task.
46 * Lets another one grab the lock, and returns the workers
47 * waiting on acquireForAnyone()
48 *
49 * @return Released/NotLocked/Error
50 */
51 abstract function release();
52
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.
60 */
61 protected $key, $workers, $maxqueue, $timeout;
62
63 /**
64 * Create a Pool counter. This should only be called from the PoolWorks.
65 */
66 public static function factory( $type, $key ) {
67 global $wgPoolCounterConf;
68 if ( !isset( $wgPoolCounterConf[$type] ) ) {
69 return new PoolCounter_Stub;
70 }
71 $conf = $wgPoolCounterConf[$type];
72 $class = $conf['class'];
73
74 return new $class( $conf, $type, $key );
75 }
76
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'];
82 }
83 }
84
85 class PoolCounter_Stub extends PoolCounter {
86 function acquireForMe() {
87 return Status::newGood( PoolCounter::LOCKED );
88 }
89
90 function acquireForAnyone() {
91 return Status::newGood( PoolCounter::LOCKED );
92 }
93
94 function release() {
95 return Status::newGood( PoolCounter::RELEASED );
96 }
97
98 public function __construct() {
99 /* No parameters needed */
100 }
101 }
102
103 /**
104 * Handy class for dealing with PoolCounters using class members instead of callbacks.
105 */
106 abstract class PoolCounterWork {
107 protected $cacheable = false; //Does this override getCachedWork() ?
108
109 /**
110 * Actually perform the work, caching it if needed.
111 */
112 abstract function doWork();
113
114 /**
115 * Retrieve the work from cache
116 * @return mixed work result or false
117 */
118 function getCachedWork() {
119 return false;
120 }
121
122 /**
123 * A work not so good (eg. expired one) but better than an error
124 * message.
125 * @return mixed work result or false
126 */
127 function fallback() {
128 return false;
129 }
130
131 /**
132 * Do something with the error, like showing it to the user.
133 */
134 function error( $status ) {
135 return false;
136 }
137
138 /**
139 * Log an error
140 */
141 function logError( $status ) {
142 wfDebugLog( 'poolcounter', $status->getWikiText() );
143 }
144
145 /**
146 * Get the result of the work (whatever it is), or false.
147 */
148 function execute( $skipcache = false ) {
149 if ( $this->cacheable && !$skipcache ) {
150 $status = $this->poolCounter->acquireForAnyone();
151 } else {
152 $status = $this->poolCounter->acquireForMe();
153 }
154
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();
159 }
160
161 switch ( $status->value ) {
162 case PoolCounter::LOCKED:
163 $result = $this->doWork();
164 $this->poolCounter->release();
165 return $result;
166
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
172 */
173 return $this->execute( true );
174 }
175 return $result;
176
177 case PoolCounter::QUEUE_FULL:
178 case PoolCounter::TIMEOUT:
179 $result = $this->fallback();
180
181 if ( $result !== false ) {
182 return $result;
183 }
184 /* no break */
185
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' );
190
191 $status = Status::newFatal( isset($errors[$status->value]) ? $errors[$status->value] : 'pool-errorunknown' );
192 $this->logError( $status );
193 return $this->error( $status );
194 }
195 }
196
197 function __construct( $type, $key ) {
198 $this->poolCounter = PoolCounter::factory( $type, $key );
199 }
200 }