In FileBackend:
[lhc/web/wiklou.git] / includes / filerepo / backend / FileBackendMultiWrite.php
1 <?php
2 /**
3 * @file
4 * @ingroup FileBackend
5 * @author Aaron Schulz
6 */
7
8 /**
9 * This class defines a multi-write backend. Multiple backends can be
10 * registered to this proxy backend and it will act as a single backend.
11 * Use this when all access to those backends is through this proxy backend.
12 * At least one of the backends must be declared the "master" backend.
13 *
14 * Only use this class when transitioning from one storage system to another.
15 *
16 * Read operations are only done on the 'master' backend for consistency.
17 * Write operations are performed on all backends, in the order defined.
18 * If an operation fails on one backend it will be rolled back from the others.
19 *
20 * @ingroup FileBackend
21 * @since 1.19
22 */
23 class FileBackendMultiWrite extends FileBackendBase {
24 /** @var Array Prioritized list of FileBackend objects */
25 protected $backends = array(); // array of (backend index => backends)
26 protected $masterIndex = -1; // index of master backend
27
28 /**
29 * Construct a proxy backend that consists of several internal backends.
30 * Additional $config params include:
31 * 'backends' : Array of backend config and multi-backend settings.
32 * Each value is the config used in the constructor of a
33 * FileBackend class, but with these additional settings:
34 * 'class' : The name of the backend class
35 * 'isMultiMaster' : This must be set for one backend.
36 * @param $config Array
37 */
38 public function __construct( array $config ) {
39 parent::__construct( $config );
40 $namesUsed = array();
41 // Construct backends here rather than via registration
42 // to keep these backends hidden from outside the proxy.
43 foreach ( $config['backends'] as $index => $config ) {
44 $name = $config['name'];
45 if ( isset( $namesUsed[$name] ) ) { // don't break FileOp predicates
46 throw new MWException( "Two or more backends defined with the name $name." );
47 }
48 $namesUsed[$name] = 1;
49 if ( !isset( $config['class'] ) ) {
50 throw new MWException( 'No class given for a backend config.' );
51 }
52 $class = $config['class'];
53 $this->backends[$index] = new $class( $config );
54 if ( !empty( $config['isMultiMaster'] ) ) {
55 if ( $this->masterIndex >= 0 ) {
56 throw new MWException( 'More than one master backend defined.' );
57 }
58 $this->masterIndex = $index;
59 }
60 }
61 if ( $this->masterIndex < 0 ) { // need backends and must have a master
62 throw new MWException( 'No master backend defined.' );
63 }
64 }
65
66 /**
67 * @see FileBackendBase::doOperationsInternal()
68 */
69 final protected function doOperationsInternal( array $ops, array $opts ) {
70 $status = Status::newGood();
71
72 $performOps = array(); // list of FileOp objects
73 $filesRead = $filesChanged = array(); // storage paths used
74 // Build up a list of FileOps. The list will have all the ops
75 // for one backend, then all the ops for the next, and so on.
76 // These batches of ops are all part of a continuous array.
77 // Also build up a list of files read/changed...
78 foreach ( $this->backends as $index => $backend ) {
79 $backendOps = $this->substOpBatchPaths( $ops, $backend );
80 // Add on the operation batch for this backend
81 $performOps = array_merge( $performOps, $backend->getOperations( $backendOps ) );
82 if ( $index == 0 ) { // first batch
83 // Get the files used for these operations. Each backend has a batch of
84 // the same operations, so we only need to get them from the first batch.
85 foreach ( $performOps as $fileOp ) {
86 $filesRead = array_merge( $filesRead, $fileOp->storagePathsRead() );
87 $filesChanged = array_merge( $filesChanged, $fileOp->storagePathsChanged() );
88 }
89 // Get the paths under the proxy backend's name
90 $filesRead = $this->unsubstPaths( $filesRead );
91 $filesChanged = $this->unsubstPaths( $filesChanged );
92 }
93 }
94
95 // Try to lock those files for the scope of this function...
96 if ( empty( $opts['nonLocking'] ) ) {
97 $filesLockSh = array_diff( $filesRead, $filesChanged ); // optimization
98 $filesLockEx = $filesChanged;
99 $scopeLockS = $this->getScopedFileLocks( $filesLockSh, LockManager::LOCK_UW, $status );
100 $scopeLockE = $this->getScopedFileLocks( $filesLockEx, LockManager::LOCK_EX, $status );
101 if ( !$status->isOK() ) {
102 return $status; // abort
103 }
104 }
105
106 // Clear any cache entries (after locks acquired)
107 $this->clearCache();
108
109 // Do a consistency check to see if the backends agree
110 if ( count( $this->backends ) > 1 ) {
111 $status->merge( $this->consistencyCheck( array_merge( $filesRead, $filesChanged ) ) );
112 if ( !$status->isOK() ) {
113 return $status; // abort
114 }
115 }
116
117 // Actually attempt the operation batch...
118 $subStatus = FileOp::attemptBatch( $performOps, $opts );
119
120 $success = array();
121 $failCount = $successCount = 0;
122 // Make 'success', 'successCount', and 'failCount' fields reflect
123 // the overall operation, rather than all the batches for each backend.
124 // Do this by only using success values from the master backend's batch.
125 $batchStart = $this->masterIndex * count( $ops );
126 $batchEnd = $batchStart + count( $ops ) - 1;
127 for ( $i = $batchStart; $i <= $batchEnd; $i++ ) {
128 if ( !isset( $subStatus->success[$i] ) ) {
129 break; // failed out before trying this op
130 } elseif ( $subStatus->success[$i] ) {
131 ++$successCount;
132 } else {
133 ++$failCount;
134 }
135 $success[] = $subStatus->success[$i];
136 }
137 $subStatus->success = $success;
138 $subStatus->successCount = $successCount;
139 $subStatus->failCount = $failCount;
140
141 // Merge errors into status fields
142 $status->merge( $subStatus );
143 $status->success = $subStatus->success; // not done in merge()
144
145 return $status;
146 }
147
148 /**
149 * Check that a set of files are consistent across all internal backends
150 *
151 * @param $paths Array
152 * @return Status
153 */
154 public function consistencyCheck( array $paths ) {
155 $status = Status::newGood();
156
157 $mBackend = $this->backends[$this->masterIndex];
158 foreach ( array_unique( $paths ) as $path ) {
159 $params = array( 'src' => $path );
160 // Stat the file on the 'master' backend
161 $mStat = $mBackend->getFileStat( $this->substOpPaths( $params, $mBackend ) );
162 // Check of all clone backends agree with the master...
163 foreach ( $this->backends as $index => $cBackend ) {
164 if ( $index === $this->masterIndex ) {
165 continue; // master
166 }
167 $cStat = $cBackend->getFileStat( $this->substOpPaths( $params, $cBackend ) );
168 if ( $mStat ) { // file is in master
169 if ( !$cStat ) { // file should exist
170 $status->fatal( 'backend-fail-synced', $path );
171 } elseif ( $cStat['size'] != $mStat['size'] ) { // wrong size
172 $status->fatal( 'backend-fail-synced', $path );
173 } else {
174 $mTs = wfTimestamp( TS_UNIX, $mStat['mtime'] );
175 $cTs = wfTimestamp( TS_UNIX, $cStat['mtime'] );
176 if ( abs( $mTs - $cTs ) > 30 ) { // outdated file somewhere
177 $status->fatal( 'backend-fail-synced', $path );
178 }
179 }
180 } else { // file is not in master
181 if ( $cStat ) { // file should not exist
182 $status->fatal( 'backend-fail-synced', $path );
183 }
184 }
185 }
186 }
187
188 return $status;
189 }
190
191 /**
192 * Substitute the backend name in storage path parameters
193 * for a set of operations with that of a given internal backend.
194 *
195 * @param $ops Array List of file operation arrays
196 * @param $backend FileBackend
197 * @return Array
198 */
199 protected function substOpBatchPaths( array $ops, FileBackend $backend ) {
200 $newOps = array(); // operations
201 foreach ( $ops as $op ) {
202 $newOp = $op; // operation
203 foreach ( array( 'src', 'srcs', 'dst', 'dir' ) as $par ) {
204 if ( isset( $newOp[$par] ) ) { // string or array
205 $newOp[$par] = $this->substPaths( $newOp[$par], $backend );
206 }
207 }
208 $newOps[] = $newOp;
209 }
210 return $newOps;
211 }
212
213 /**
214 * Same as substOpBatchPaths() but for a single operation
215 *
216 * @param $op File operation array
217 * @param $backend FileBackend
218 * @return Array
219 */
220 protected function substOpPaths( array $ops, FileBackend $backend ) {
221 $newOps = $this->substOpBatchPaths( array( $ops ), $backend );
222 return $newOps[0];
223 }
224
225 /**
226 * Substitute the backend of storage paths with an internal backend's name
227 *
228 * @param $paths Array|string List of paths or single string path
229 * @param $backend FileBackend
230 * @return Array|string
231 */
232 protected function substPaths( $paths, FileBackend $backend ) {
233 return preg_replace(
234 '!^mwstore://' . preg_quote( $this->name ) . '/!',
235 'mwstore://' . $backend->getName() . '/',
236 $paths // string or array
237 );
238 }
239
240 /**
241 * Substitute the backend of internal storage paths with the proxy backend's name
242 *
243 * @param $paths Array|string List of paths or single string path
244 * @return Array|string
245 */
246 protected function unsubstPaths( $paths ) {
247 return preg_replace(
248 '!^mwstore://([^/]+)!',
249 "mwstore://{$this->name}",
250 $paths // string or array
251 );
252 }
253
254 /**
255 * @see FileBackendBase::doPrepare()
256 */
257 public function doPrepare( array $params ) {
258 $status = Status::newGood();
259 foreach ( $this->backends as $backend ) {
260 $realParams = $this->substOpPaths( $params, $backend );
261 $status->merge( $backend->doPrepare( $realParams ) );
262 }
263 return $status;
264 }
265
266 /**
267 * @see FileBackendBase::doSecure()
268 */
269 public function doSecure( array $params ) {
270 $status = Status::newGood();
271 foreach ( $this->backends as $backend ) {
272 $realParams = $this->substOpPaths( $params, $backend );
273 $status->merge( $backend->doSecure( $realParams ) );
274 }
275 return $status;
276 }
277
278 /**
279 * @see FileBackendBase::doClean()
280 */
281 public function doClean( array $params ) {
282 $status = Status::newGood();
283 foreach ( $this->backends as $backend ) {
284 $realParams = $this->substOpPaths( $params, $backend );
285 $status->merge( $backend->doClean( $realParams ) );
286 }
287 return $status;
288 }
289
290 /**
291 * @see FileBackendBase::getFileList()
292 */
293 public function concatenate( array $params ) {
294 // We are writing to an FS file, so we don't need to do this per-backend
295 $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
296 return $this->backends[$this->masterIndex]->concatenate( $realParams );
297 }
298
299 /**
300 * @see FileBackendBase::fileExists()
301 */
302 public function fileExists( array $params ) {
303 $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
304 return $this->backends[$this->masterIndex]->fileExists( $realParams );
305 }
306
307 /**
308 * @see FileBackendBase::getFileTimestamp()
309 */
310 public function getFileTimestamp( array $params ) {
311 $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
312 return $this->backends[$this->masterIndex]->getFileTimestamp( $realParams );
313 }
314
315 /**
316 * @see FileBackendBase::getFileSize()
317 */
318 public function getFileSize( array $params ) {
319 $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
320 return $this->backends[$this->masterIndex]->getFileSize( $realParams );
321 }
322
323 /**
324 * @see FileBackendBase::getFileStat()
325 */
326 public function getFileStat( array $params ) {
327 $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
328 return $this->backends[$this->masterIndex]->getFileStat( $realParams );
329 }
330
331 /**
332 * @see FileBackendBase::getFileContents()
333 */
334 public function getFileContents( array $params ) {
335 $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
336 return $this->backends[$this->masterIndex]->getFileContents( $realParams );
337 }
338
339 /**
340 * @see FileBackendBase::getFileSha1Base36()
341 */
342 public function getFileSha1Base36( array $params ) {
343 $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
344 return $this->backends[$this->masterIndex]->getFileSha1Base36( $realParams );
345 }
346
347 /**
348 * @see FileBackendBase::getFileProps()
349 */
350 public function getFileProps( array $params ) {
351 $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
352 return $this->backends[$this->masterIndex]->getFileProps( $realParams );
353 }
354
355 /**
356 * @see FileBackendBase::streamFile()
357 */
358 public function streamFile( array $params ) {
359 $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
360 return $this->backends[$this->masterIndex]->streamFile( $realParams );
361 }
362
363 /**
364 * @see FileBackendBase::getLocalReference()
365 */
366 public function getLocalReference( array $params ) {
367 $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
368 return $this->backends[$this->masterIndex]->getLocalReference( $realParams );
369 }
370
371 /**
372 * @see FileBackendBase::getLocalCopy()
373 */
374 public function getLocalCopy( array $params ) {
375 $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
376 return $this->backends[$this->masterIndex]->getLocalCopy( $realParams );
377 }
378
379 /**
380 * @see FileBackendBase::getFileList()
381 */
382 public function getFileList( array $params ) {
383 $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
384 return $this->backends[$this->masterIndex]->getFileList( $realParams );
385 }
386
387 /**
388 * @see FileBackendBase::clearCache()
389 */
390 public function clearCache( array $paths = null ) {
391 foreach ( $this->backends as $backend ) {
392 $realPaths = is_array( $paths ) ? $this->substPaths( $paths ) : null;
393 $backend->clearCache( $realPaths );
394 }
395 }
396 }