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