Make a bunch of incompatible changes to the PoolCounter.
[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
14 abstract class PoolCounter {
15
16 /* Return codes */
17 const LOCKED = 1; /* Lock acquired */
18 const RELEASED = 2; /* Lock released */
19 const DONE = 3; /* Another one did the work for you */
20
21 const ERROR = -1; /* Indeterminate error */
22 const NOT_LOCKED = -2; /* Called release() with no lock held */
23 const QUEUE_FULL = -3; /* There are already maxqueue workers on this lock */
24 const TIMEOUT = -4; /* Timeout exceeded */
25 const LOCK_HELD = -5; /* Cannot acquire another lock while you have one lock held */
26
27 /**
28 * I want to do this task and I need to do it myself.
29 *
30 * @return Locked/Error
31 */
32 abstract function acquireForMe();
33
34 /**
35 * I want to do this task, but if anyone else does it
36 * instead, it's also fine for me. I will read its cached data.
37 *
38 * @return Locked/Done/Error
39 */
40 abstract function acquireForAnyone();
41
42 /**
43 * I have successfully finished my task.
44 * Lets another one grab the lock, and returns the workers
45 * waiting on acquireForAnyone()
46 *
47 * @return Released/NotLocked/Error
48 */
49 abstract function release();
50
51 /**
52 * $key: All workers with the same key share the lock.
53 * $workers: It wouldn't be a good idea to have more than this number of
54 * workers doing the task simultaneously.
55 * $maxqueue: If this number of workers are already working/waiting,
56 * fail instead of wait.
57 * $timeout: Maximum time in seconds to wait for the lock.
58 */
59 protected $key, $workers, $maxqueue, $timeout;
60
61 /**
62 * Create a Pool counter. This should only be called from the PoolWorks.
63 */
64 public static function factory( $type, $key ) {
65 global $wgPoolCounterConf;
66 if ( !isset( $wgPoolCounterConf[$type] ) ) {
67 return new PoolCounter_Stub;
68 }
69 $conf = $wgPoolCounterConf[$type];
70 $class = $conf['class'];
71
72 return new $class( $conf, $type, $key );
73 }
74
75 protected function __construct( $conf, $type, $key ) {
76 $this->key = $key;
77 $this->workers = $conf['workers'];
78 $this->maxqueue = $conf['maxqueue'];
79 $this->timeout = $conf['timeout'];
80 }
81 }
82
83 class PoolCounter_Stub extends PoolCounter {
84 function acquireForMe() {
85 return PoolCounter::LOCKED;
86 }
87
88 function acquireForAnyone() {
89 return PoolCounter::LOCKED;
90 }
91
92 function release() {
93 return PoolCounter::RELEASED;
94 }
95
96 public function __construct() {
97 /* No parameters needed */
98 }
99 }
100
101 /**
102 * Handy class for dealing with PoolCounters using class members instead of callbacks.
103 */
104 abstract class PoolCounterWork {
105 protected $cacheable = false; //Does this override getCachedWork() ?
106
107 /**
108 * Actually perform the work, caching it if needed.
109 */
110 abstract function doWork();
111
112 /**
113 * Retrieve the work from cache
114 * @return mixed work result or false
115 */
116 function getCachedWork() {
117 return false;
118 }
119
120 /**
121 * A work not so good (eg. expired one) but better than an error
122 * message.
123 * @return mixed work result or false
124 */
125 function fallback() {
126 return false;
127 }
128
129 /**
130 * Do something with the error, like showing it to the user.
131 */
132 function error( $status ) {
133 return false;
134 }
135
136 /**
137 * Get the result of the work (whatever it is), or false.
138 */
139 function execute( $skipcache = false ) {
140 if ( $this->cacheable && !$skipcache ) {
141 $status = $this->poolCounter->acquireForAnyone();
142 } else {
143 $status = $this->poolCounter->acquireForMe();
144 }
145
146 $result = false;
147 switch ( is_int( $status ) ? $status : PoolCounter::ERROR ) {
148 case PoolCounter::LOCKED:
149 $result = $this->doWork();
150 $this->poolCounter->release();
151 return $result;
152
153 case PoolCounter::DONE:
154 $result = $this->getCachedWork();
155 if ( $result === false ) {
156 /* That someone else work didn't serve us.
157 * Acquire the lock for me
158 */
159 return $this->execute( true );
160 }
161 return $result;
162
163 case PoolCounter::QUEUE_FULL:
164 case PoolCounter::TIMEOUT:
165 $result = $this->fallback();
166
167 if ( $result !== false ) {
168 return $result;
169 }
170 /* no break */
171
172 case PoolCounter::ERROR:
173 default:
174 return $this->error( $status );
175 }
176 }
177
178 function __construct( $type, $key ) {
179 $this->poolCounter = PoolCounter::factory( $type, $key );
180 }
181 }