4a135222bf0a16d8e8ac6e3c8aa56ebb9ee55800
[lhc/web/wiklou.git] / includes / db / loadbalancer / LBFactory.php
1 <?php
2 /**
3 * Generator of database load balancing objects.
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 * http://www.gnu.org/copyleft/gpl.html
19 *
20 * @file
21 * @ingroup Database
22 */
23
24 /**
25 * An interface for generating database load balancers
26 * @ingroup Database
27 */
28 abstract class LBFactory {
29 /** @var ChronologyProtector */
30 protected $chronProt;
31 /** @var TransactionProfiler */
32 protected $trxProfiler;
33
34 /** @var LBFactory */
35 private static $instance;
36
37 /** @var string|bool Reason all LBs are read-only or false if not */
38 protected $readOnlyReason = false;
39
40 const SHUTDOWN_NO_CHRONPROT = 1; // don't save ChronologyProtector positions (for async code)
41
42 /**
43 * Construct a factory based on a configuration array (typically from $wgLBFactoryConf)
44 * @param array $conf
45 */
46 public function __construct( array $conf ) {
47 if ( isset( $conf['readOnlyReason'] ) && is_string( $conf['readOnlyReason'] ) ) {
48 $this->readOnlyReason = $conf['readOnlyReason'];
49 }
50
51 $this->chronProt = $this->newChronologyProtector();
52 $this->trxProfiler = Profiler::instance()->getTransactionProfiler();
53 }
54
55 /**
56 * Disables all access to the load balancer, will cause all database access
57 * to throw a DBAccessError
58 */
59 public static function disableBackend() {
60 global $wgLBFactoryConf;
61 self::$instance = new LBFactoryFake( $wgLBFactoryConf );
62 }
63
64 /**
65 * Get an LBFactory instance
66 *
67 * @return LBFactory
68 */
69 public static function singleton() {
70 global $wgLBFactoryConf;
71
72 if ( is_null( self::$instance ) ) {
73 $class = self::getLBFactoryClass( $wgLBFactoryConf );
74 $config = $wgLBFactoryConf;
75 if ( !isset( $config['readOnlyReason'] ) ) {
76 $config['readOnlyReason'] = wfConfiguredReadOnlyReason();
77 }
78 self::$instance = new $class( $config );
79 }
80
81 return self::$instance;
82 }
83
84 /**
85 * Returns the LBFactory class to use and the load balancer configuration.
86 *
87 * @param array $config (e.g. $wgLBFactoryConf)
88 * @return string Class name
89 */
90 public static function getLBFactoryClass( array $config ) {
91 // For configuration backward compatibility after removing
92 // underscores from class names in MediaWiki 1.23.
93 $bcClasses = array(
94 'LBFactory_Simple' => 'LBFactorySimple',
95 'LBFactory_Single' => 'LBFactorySingle',
96 'LBFactory_Multi' => 'LBFactoryMulti',
97 'LBFactory_Fake' => 'LBFactoryFake',
98 );
99
100 $class = $config['class'];
101
102 if ( isset( $bcClasses[$class] ) ) {
103 $class = $bcClasses[$class];
104 wfDeprecated(
105 '$wgLBFactoryConf must be updated. See RELEASE-NOTES for details',
106 '1.23'
107 );
108 }
109
110 return $class;
111 }
112
113 /**
114 * Shut down, close connections and destroy the cached instance.
115 */
116 public static function destroyInstance() {
117 if ( self::$instance ) {
118 self::$instance->shutdown();
119 self::$instance->forEachLBCallMethod( 'closeAll' );
120 self::$instance = null;
121 }
122 }
123
124 /**
125 * Set the instance to be the given object
126 *
127 * @param LBFactory $instance
128 */
129 public static function setInstance( $instance ) {
130 self::destroyInstance();
131 self::$instance = $instance;
132 }
133
134 /**
135 * Create a new load balancer object. The resulting object will be untracked,
136 * not chronology-protected, and the caller is responsible for cleaning it up.
137 *
138 * @param bool|string $wiki Wiki ID, or false for the current wiki
139 * @return LoadBalancer
140 */
141 abstract public function newMainLB( $wiki = false );
142
143 /**
144 * Get a cached (tracked) load balancer object.
145 *
146 * @param bool|string $wiki Wiki ID, or false for the current wiki
147 * @return LoadBalancer
148 */
149 abstract public function getMainLB( $wiki = false );
150
151 /**
152 * Create a new load balancer for external storage. The resulting object will be
153 * untracked, not chronology-protected, and the caller is responsible for
154 * cleaning it up.
155 *
156 * @param string $cluster External storage cluster, or false for core
157 * @param bool|string $wiki Wiki ID, or false for the current wiki
158 * @return LoadBalancer
159 */
160 abstract protected function newExternalLB( $cluster, $wiki = false );
161
162 /**
163 * Get a cached (tracked) load balancer for external storage
164 *
165 * @param string $cluster External storage cluster, or false for core
166 * @param bool|string $wiki Wiki ID, or false for the current wiki
167 * @return LoadBalancer
168 */
169 abstract public function &getExternalLB( $cluster, $wiki = false );
170
171 /**
172 * Execute a function for each tracked load balancer
173 * The callback is called with the load balancer as the first parameter,
174 * and $params passed as the subsequent parameters.
175 *
176 * @param callable $callback
177 * @param array $params
178 */
179 abstract public function forEachLB( $callback, array $params = array() );
180
181 /**
182 * Prepare all tracked load balancers for shutdown
183 * @param integer $flags Supports SHUTDOWN_* flags
184 * STUB
185 */
186 public function shutdown( $flags = 0 ) {
187 }
188
189 /**
190 * Call a method of each tracked load balancer
191 *
192 * @param string $methodName
193 * @param array $args
194 */
195 private function forEachLBCallMethod( $methodName, array $args = array() ) {
196 $this->forEachLB(
197 function ( LoadBalancer $loadBalancer, $methodName, array $args ) {
198 call_user_func_array( array( $loadBalancer, $methodName ), $args );
199 },
200 array( $methodName, $args )
201 );
202 }
203
204 /**
205 * Commit on all connections. Done for two reasons:
206 * 1. To commit changes to the masters.
207 * 2. To release the snapshot on all connections, master and slave.
208 * @param string $fname Caller name
209 */
210 public function commitAll( $fname = __METHOD__ ) {
211 $start = microtime( true );
212 $this->forEachLBCallMethod( 'commitAll', array( $fname ) );
213 $timeMs = 1000 * ( microtime( true ) - $start );
214
215 RequestContext::getMain()->getStats()->timing( "db.commit-all", $timeMs );
216 }
217
218 /**
219 * Commit changes on all master connections
220 * @param string $fname Caller name
221 */
222 public function commitMasterChanges( $fname = __METHOD__ ) {
223 $start = microtime( true );
224 $this->forEachLBCallMethod( 'commitMasterChanges', array( $fname ) );
225 $timeMs = 1000 * ( microtime( true ) - $start );
226
227 RequestContext::getMain()->getStats()->timing( "db.commit-masters", $timeMs );
228 }
229
230 /**
231 * Rollback changes on all master connections
232 * @param string $fname Caller name
233 * @since 1.23
234 */
235 public function rollbackMasterChanges( $fname = __METHOD__ ) {
236 $this->forEachLBCallMethod( 'rollbackMasterChanges', array( $fname ) );
237 }
238
239 /**
240 * Determine if any master connection has pending changes
241 * @return bool
242 * @since 1.23
243 */
244 public function hasMasterChanges() {
245 $ret = false;
246 $this->forEachLB( function ( LoadBalancer $lb ) use ( &$ret ) {
247 $ret = $ret || $lb->hasMasterChanges();
248 } );
249
250 return $ret;
251 }
252
253 /**
254 * Detemine if any lagged slave connection was used
255 * @since 1.27
256 * @return bool
257 */
258 public function laggedSlaveUsed() {
259 $ret = false;
260 $this->forEachLB( function ( LoadBalancer $lb ) use ( &$ret ) {
261 $ret = $ret || $lb->laggedSlaveUsed();
262 } );
263
264 return $ret;
265 }
266
267 /**
268 * Determine if any master connection has pending/written changes from this request
269 * @return bool
270 * @since 1.27
271 */
272 public function hasOrMadeRecentMasterChanges() {
273 $ret = false;
274 $this->forEachLB( function ( LoadBalancer $lb ) use ( &$ret ) {
275 $ret = $ret || $lb->hasOrMadeRecentMasterChanges();
276 } );
277 return $ret;
278 }
279
280 /**
281 * Disable the ChronologyProtector for all load balancers
282 *
283 * This can be called at the start of special API entry points
284 *
285 * @since 1.27
286 */
287 public function disableChronologyProtection() {
288 $this->chronProt->setEnabled( false );
289 }
290
291 /**
292 * @return ChronologyProtector
293 */
294 protected function newChronologyProtector() {
295 $request = RequestContext::getMain()->getRequest();
296 $chronProt = new ChronologyProtector(
297 ObjectCache::getMainStashInstance(),
298 array(
299 'ip' => $request->getIP(),
300 'agent' => $request->getHeader( 'User-Agent' )
301 )
302 );
303 if ( PHP_SAPI === 'cli' ) {
304 $chronProt->setEnabled( false );
305 } elseif ( $request->getHeader( 'ChronologyProtection' ) === 'false' ) {
306 // Request opted out of using position wait logic. This is useful for requests
307 // done by the job queue or background ETL that do not have a meaningful session.
308 $chronProt->setWaitEnabled( false );
309 }
310
311 return $chronProt;
312 }
313
314 /**
315 * @param ChronologyProtector $cp
316 */
317 protected function shutdownChronologyProtector( ChronologyProtector $cp ) {
318 // Get all the master positions needed
319 $this->forEachLB( function ( LoadBalancer $lb ) use ( $cp ) {
320 $cp->shutdownLB( $lb );
321 } );
322 // Write them to the stash
323 $unsavedPositions = $cp->shutdown();
324 // If the positions failed to write to the stash, at least wait on local datacenter
325 // slaves to catch up before responding. Even if there are several DCs, this increases
326 // the chance that the user will see their own changes immediately afterwards. As long
327 // as the sticky DC cookie applies (same domain), this is not even an issue.
328 $this->forEachLB( function ( LoadBalancer $lb ) use ( $unsavedPositions ) {
329 $masterName = $lb->getServerName( $lb->getWriterIndex() );
330 if ( isset( $unsavedPositions[$masterName] ) ) {
331 $lb->waitForAll( $unsavedPositions[$masterName] );
332 }
333 } );
334 }
335 }
336
337 /**
338 * Exception class for attempted DB access
339 */
340 class DBAccessError extends MWException {
341 public function __construct() {
342 parent::__construct( "Mediawiki tried to access the database via wfGetDB(). " .
343 "This is not allowed." );
344 }
345 }