summaryrefslogtreecommitdiff
path: root/includes/PoolCounter.php
blob: 3851767fad0f235c225a45a467ae005dd513db4b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
<?php

/**
 *  When you have many workers (threads/servers) giving service, and a 
 * cached item expensive to produce expires, you may get several workers
 * doing the job at the same time.
 *  Given enough requests and the item expiring fast (non-cacheable, 
 * lots of edits...) that single work can end up unfairly using most (all)
 * of the cpu of the pool. This is also known as 'Michael Jackson effect'.
 *  The PoolCounter provides semaphore semantics for restricting the number
 * of workers that may be concurrently performing such single task.
 *
 *  By default PoolCounter_Stub is used, which provides no locking. You 
 * can get a useful one in the PoolCounter extension.
 */
abstract class PoolCounter {
	
	/* Return codes */
	const LOCKED = 1; 	/* Lock acquired */
	const RELEASED = 2; /* Lock released */
	const DONE = 3;		/* Another one did the work for you */
	
	const ERROR = -1;		/* Indeterminate error */
	const NOT_LOCKED = -2;	/* Called release() with no lock held */
	const QUEUE_FULL = -3;	/* There are already maxqueue workers on this lock */
	const TIMEOUT = -4;		/* Timeout exceeded */
	const LOCK_HELD = -5;	/* Cannot acquire another lock while you have one lock held */

	/**
	 * I want to do this task and I need to do it myself.
	 * 
	 * @return Locked/Error
	 */
	abstract function acquireForMe();

	/**
	 * I want to do this task, but if anyone else does it 
	 * instead, it's also fine for me. I will read its cached data.
	 * 
	 * @return Locked/Done/Error
	 */
	abstract function acquireForAnyone();

	/**
	 * I have successfully finished my task.
	 * Lets another one grab the lock, and returns the workers 
	 * waiting on acquireForAnyone()
	 * 
	 * @return Released/NotLocked/Error
	 */
	abstract function release();
	
	/**
	 *  $key: All workers with the same key share the lock.
	 *  $workers: It wouldn't be a good idea to have more than this number of 
	 * workers doing the task simultaneously.
	 *  $maxqueue: If this number of workers are already working/waiting, 
	 * fail instead of wait.
	 *  $timeout: Maximum time in seconds to wait for the lock.
	 */
	protected $key, $workers, $maxqueue, $timeout;
	
	/**
	 * Create a Pool counter. This should only be called from the PoolWorks.
	 */
	public static function factory( $type, $key ) {
		global $wgPoolCounterConf;
		if ( !isset( $wgPoolCounterConf[$type] ) ) {
			return new PoolCounter_Stub;
		}
		$conf = $wgPoolCounterConf[$type];
		$class = $conf['class'];
		
		return new $class( $conf, $type, $key );
	}
	
	protected function __construct( $conf, $type, $key ) {
		$this->key = $key;
		$this->workers  = $conf['workers'];
		$this->maxqueue = $conf['maxqueue'];
		$this->timeout  = $conf['timeout'];
	}
}

class PoolCounter_Stub extends PoolCounter {
	function acquireForMe() {
		return Status::newGood( PoolCounter::LOCKED );
	}

	function acquireForAnyone() {
		return Status::newGood( PoolCounter::LOCKED );
	}

	function release() {
		return Status::newGood( PoolCounter::RELEASED );
	}
	
	public function __construct() {
		/* No parameters needed */
	}
}

/**
 * Handy class for dealing with PoolCounters using class members instead of callbacks.
 */
abstract class PoolCounterWork {
	protected $cacheable = false; //Does this override getCachedWork() ?
	
	/**
	 * Actually perform the work, caching it if needed.
	 */
	abstract function doWork();

	/**
	 * Retrieve the work from cache
	 * @return mixed work result or false
	 */
	function getCachedWork() {
		return false;
	}

	/**
	 * A work not so good (eg. expired one) but better than an error 
	 * message.
	 * @return mixed work result or false
	 */
	function fallback() {
		return false;
	}
	
	/**
	 * Do something with the error, like showing it to the user.
	 */
	function error( $status ) {	
		return false;
	}
	
	/**
	 * Get the result of the work (whatever it is), or false.
	 */
	function execute( $skipcache = false ) {
		if ( $this->cacheable && !$skipcache ) {
			$status = $this->poolCounter->acquireForAnyone();
		} else {
			$status = $this->poolCounter->acquireForMe();
		}
		
		if ( $status->isOK() ) {
			switch ( $status->value ) {
				case PoolCounter::LOCKED:
					$result = $this->doWork();
					$this->poolCounter->release();
					return $result;
				
				case PoolCounter::DONE:
					$result = $this->getCachedWork();
					if ( $result === false ) {
						/* That someone else work didn't serve us.
						 * Acquire the lock for me
						 */
						return $this->execute( true );
					}
					return $result;
					
				case PoolCounter::QUEUE_FULL:
				case PoolCounter::TIMEOUT:
					$result = $this->fallback();
					
					if ( $result !== false ) {
						return $result;
					}
					/* no break */
				
				/* These two cases should never be hit... */
				case PoolCounter::ERROR:
				default:
					$errors = array( PoolCounter::QUEUE_FULL => 'pool-queuefull', PoolCounter::TIMEOUT => 'pool-timeout' );
					
					$status = Status::newFatal( isset($errors[$status->value]) ? $errors[$status->value] : 'pool-errorunknown' );
					/* continue to the error */
			}
		}
		return $this->error( $status );
	}
	
	function __construct( $type, $key ) {
		$this->poolCounter = PoolCounter::factory( $type, $key );
	}
}