Don't look for pipes in the root node.
[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 * Get the result of the work (whatever it is), or false.
140 */
141 function execute( $skipcache = false ) {
142 if ( $this->cacheable && !$skipcache ) {
143 $status = $this->poolCounter->acquireForAnyone();
144 } else {
145 $status = $this->poolCounter->acquireForMe();
146 }
147
148 if ( $status->isOK() ) {
149 switch ( $status->value ) {
150 case PoolCounter::LOCKED:
151 $result = $this->doWork();
152 $this->poolCounter->release();
153 return $result;
154
155 case PoolCounter::DONE:
156 $result = $this->getCachedWork();
157 if ( $result === false ) {
158 /* That someone else work didn't serve us.
159 * Acquire the lock for me
160 */
161 return $this->execute( true );
162 }
163 return $result;
164
165 case PoolCounter::QUEUE_FULL:
166 case PoolCounter::TIMEOUT:
167 $result = $this->fallback();
168
169 if ( $result !== false ) {
170 return $result;
171 }
172 /* no break */
173
174 /* These two cases should never be hit... */
175 case PoolCounter::ERROR:
176 default:
177 $errors = array( PoolCounter::QUEUE_FULL => 'pool-queuefull', PoolCounter::TIMEOUT => 'pool-timeout' );
178
179 $status = Status::newFatal( isset($errors[$status->value]) ? $errors[$status->value] : 'pool-errorunknown' );
180 /* continue to the error */
181 }
182 }
183 return $this->error( $status );
184 }
185
186 function __construct( $type, $key ) {
187 $this->poolCounter = PoolCounter::factory( $type, $key );
188 }
189 }