MediaWiki master
FileBackendMultiWrite.php
Go to the documentation of this file.
1<?php
27use Wikimedia\Timestamp\ConvertibleTimestamp;
28
51 protected $backends = [];
52
54 protected $masterIndex = -1;
56 protected $readIndex = -1;
57
59 protected $syncChecks = 0;
61 protected $autoResync = false;
62
64 protected $asyncWrites = false;
65
67 private const CHECK_SIZE = 1;
69 private const CHECK_TIME = 2;
71 private const CHECK_SHA1 = 4;
72
101 public function __construct( array $config ) {
102 parent::__construct( $config );
103 $this->syncChecks = $config['syncChecks'] ?? self::CHECK_SIZE;
104 $this->autoResync = $config['autoResync'] ?? false;
105 $this->asyncWrites = isset( $config['replication'] ) && $config['replication'] === 'async';
106 // Construct backends here rather than via registration
107 // to keep these backends hidden from outside the proxy.
108 $namesUsed = [];
109 foreach ( $config['backends'] as $index => $beConfig ) {
110 $name = $beConfig['name'];
111 if ( isset( $namesUsed[$name] ) ) { // don't break FileOp predicates
112 throw new LogicException( "Two or more backends defined with the name $name." );
113 }
114 $namesUsed[$name] = 1;
115 // Alter certain sub-backend settings
116 unset( $beConfig['readOnly'] ); // use proxy backend setting
117 unset( $beConfig['lockManager'] ); // lock under proxy backend
118 $beConfig['domainId'] = $this->domainId; // use the proxy backend wiki ID
119 $beConfig['logger'] = $this->logger; // use the proxy backend logger
120 if ( !empty( $beConfig['isMultiMaster'] ) ) {
121 if ( $this->masterIndex >= 0 ) {
122 throw new LogicException( 'More than one master backend defined.' );
123 }
124 $this->masterIndex = $index; // this is the "master"
125 }
126 if ( !empty( $beConfig['readAffinity'] ) ) {
127 $this->readIndex = $index; // prefer this for reads
128 }
129 // Create sub-backend object
130 if ( !isset( $beConfig['class'] ) ) {
131 throw new InvalidArgumentException( 'No class given for a backend config.' );
132 }
133 $class = $beConfig['class'];
134 $this->backends[$index] = new $class( $beConfig );
135 }
136 if ( $this->masterIndex < 0 ) { // need backends and must have a master
137 throw new LogicException( 'No master backend defined.' );
138 }
139 if ( $this->readIndex < 0 ) {
140 $this->readIndex = $this->masterIndex; // default
141 }
142 }
143
144 final protected function doOperationsInternal( array $ops, array $opts ) {
145 $status = $this->newStatus();
146
147 $fname = __METHOD__;
148 $mbe = $this->backends[$this->masterIndex]; // convenience
149
150 // Acquire any locks as needed
151 $scopeLock = null;
152 if ( empty( $opts['nonLocking'] ) ) {
153 $scopeLock = $this->getScopedLocksForOps( $ops, $status );
154 if ( !$status->isOK() ) {
155 return $status; // abort
156 }
157 }
158 // Get the list of paths to read/write
159 $relevantPaths = $this->fileStoragePathsForOps( $ops );
160 // Clear any cache entries (after locks acquired)
161 $this->clearCache( $relevantPaths );
162 $opts['preserveCache'] = true; // only locked files are cached
163 // Check if the paths are valid and accessible on all backends
164 $status->merge( $this->accessibilityCheck( $relevantPaths ) );
165 if ( !$status->isOK() ) {
166 return $status; // abort
167 }
168 // Do a consistency check to see if the backends are consistent
169 $syncStatus = $this->consistencyCheck( $relevantPaths );
170 if ( !$syncStatus->isOK() ) {
171 $this->logger->error(
172 "$fname: failed sync check: " . FormatJson::encode( $relevantPaths )
173 );
174 // Try to resync the clone backends to the master on the spot
175 if (
176 $this->autoResync === false ||
177 !$this->resyncFiles( $relevantPaths, $this->autoResync )->isOK()
178 ) {
179 $status->merge( $syncStatus );
180
181 return $status; // abort
182 }
183 }
184 // Actually attempt the operation batch on the master backend
185 $realOps = $this->substOpBatchPaths( $ops, $mbe );
186 $masterStatus = $mbe->doOperations( $realOps, $opts );
187 $status->merge( $masterStatus );
188 // Propagate the operations to the clone backends if there were no unexpected errors
189 // and everything didn't fail due to predicted errors. If $ops only had one operation,
190 // this might avoid backend sync inconsistencies.
191 if ( $masterStatus->isOK() && $masterStatus->successCount > 0 ) {
192 foreach ( $this->backends as $index => $backend ) {
193 if ( $index === $this->masterIndex ) {
194 continue; // done already
195 }
196
197 $realOps = $this->substOpBatchPaths( $ops, $backend );
198 if ( $this->asyncWrites && !$this->hasVolatileSources( $ops ) ) {
199 // Bind $scopeLock to the callback to preserve locks
200 DeferredUpdates::addCallableUpdate(
201 function () use (
202 $backend, $realOps, $opts, $scopeLock, $relevantPaths, $fname
203 ) {
204 $this->logger->debug(
205 "$fname: '{$backend->getName()}' async replication; paths: " .
206 FormatJson::encode( $relevantPaths )
207 );
208 $backend->doOperations( $realOps, $opts );
209 }
210 );
211 } else {
212 $this->logger->debug(
213 "$fname: '{$backend->getName()}' sync replication; paths: " .
214 FormatJson::encode( $relevantPaths )
215 );
216 $status->merge( $backend->doOperations( $realOps, $opts ) );
217 }
218 }
219 }
220 // Make 'success', 'successCount', and 'failCount' fields reflect
221 // the overall operation, rather than all the batches for each backend.
222 // Do this by only using success values from the master backend's batch.
223 $status->success = $masterStatus->success;
224 $status->successCount = $masterStatus->successCount;
225 $status->failCount = $masterStatus->failCount;
226
227 return $status;
228 }
229
239 public function consistencyCheck( array $paths ) {
240 $status = $this->newStatus();
241 if ( $this->syncChecks == 0 || count( $this->backends ) <= 1 ) {
242 return $status; // skip checks
243 }
244
245 // Preload all of the stat info in as few round trips as possible
246 foreach ( $this->backends as $backend ) {
247 $realPaths = $this->substPaths( $paths, $backend );
248 $backend->preloadFileStat( [ 'srcs' => $realPaths, 'latest' => true ] );
249 }
250
251 foreach ( $paths as $path ) {
252 $params = [ 'src' => $path, 'latest' => true ];
253 // Get the state of the file on the master backend
254 $masterBackend = $this->backends[$this->masterIndex];
255 $masterParams = $this->substOpPaths( $params, $masterBackend );
256 $masterStat = $masterBackend->getFileStat( $masterParams );
257 if ( $masterStat === self::STAT_ERROR ) {
258 $status->fatal( 'backend-fail-stat', $path );
259 continue;
260 }
261 if ( $this->syncChecks & self::CHECK_SHA1 ) {
262 $masterSha1 = $masterBackend->getFileSha1Base36( $masterParams );
263 if ( ( $masterSha1 !== false ) !== (bool)$masterStat ) {
264 $status->fatal( 'backend-fail-hash', $path );
265 continue;
266 }
267 } else {
268 $masterSha1 = null; // unused
269 }
270
271 // Check if all clone backends agree with the master...
272 foreach ( $this->backends as $index => $cloneBackend ) {
273 if ( $index === $this->masterIndex ) {
274 continue; // master
275 }
276
277 // Get the state of the file on the clone backend
278 $cloneParams = $this->substOpPaths( $params, $cloneBackend );
279 $cloneStat = $cloneBackend->getFileStat( $cloneParams );
280
281 if ( $masterStat ) {
282 // File exists in the master backend
283 if ( !$cloneStat ) {
284 // File is missing from the clone backend
285 $status->fatal( 'backend-fail-synced', $path );
286 } elseif (
287 ( $this->syncChecks & self::CHECK_SIZE ) &&
288 $cloneStat['size'] !== $masterStat['size']
289 ) {
290 // File in the clone backend is different
291 $status->fatal( 'backend-fail-synced', $path );
292 } elseif (
293 ( $this->syncChecks & self::CHECK_TIME ) &&
294 abs(
295 (int)ConvertibleTimestamp::convert( TS_UNIX, $masterStat['mtime'] ) -
296 (int)ConvertibleTimestamp::convert( TS_UNIX, $cloneStat['mtime'] )
297 ) > 30
298 ) {
299 // File in the clone backend is significantly newer or older
300 $status->fatal( 'backend-fail-synced', $path );
301 } elseif (
302 ( $this->syncChecks & self::CHECK_SHA1 ) &&
303 $cloneBackend->getFileSha1Base36( $cloneParams ) !== $masterSha1
304 ) {
305 // File in the clone backend is different
306 $status->fatal( 'backend-fail-synced', $path );
307 }
308 } else {
309 // File does not exist in the master backend
310 if ( $cloneStat ) {
311 // Stray file exists in the clone backend
312 $status->fatal( 'backend-fail-synced', $path );
313 }
314 }
315 }
316 }
317
318 return $status;
319 }
320
327 public function accessibilityCheck( array $paths ) {
328 $status = $this->newStatus();
329 if ( count( $this->backends ) <= 1 ) {
330 return $status; // skip checks
331 }
332
333 foreach ( $paths as $path ) {
334 foreach ( $this->backends as $backend ) {
335 $realPath = $this->substPaths( $path, $backend );
336 if ( !$backend->isPathUsableInternal( $realPath ) ) {
337 $status->fatal( 'backend-fail-usable', $path );
338 }
339 }
340 }
341
342 return $status;
343 }
344
355 public function resyncFiles( array $paths, $resyncMode = true ) {
356 $status = $this->newStatus();
357
358 $fname = __METHOD__;
359 foreach ( $paths as $path ) {
360 $params = [ 'src' => $path, 'latest' => true ];
361 // Get the state of the file on the master backend
362 $masterBackend = $this->backends[$this->masterIndex];
363 $masterParams = $this->substOpPaths( $params, $masterBackend );
364 $masterPath = $masterParams['src'];
365 $masterStat = $masterBackend->getFileStat( $masterParams );
366 if ( $masterStat === self::STAT_ERROR ) {
367 $status->fatal( 'backend-fail-stat', $path );
368 $this->logger->error( "$fname: file '$masterPath' is not available" );
369 continue;
370 }
371 $masterSha1 = $masterBackend->getFileSha1Base36( $masterParams );
372 if ( ( $masterSha1 !== false ) !== (bool)$masterStat ) {
373 $status->fatal( 'backend-fail-hash', $path );
374 $this->logger->error( "$fname: file '$masterPath' hash does not match stat" );
375 continue;
376 }
377
378 // Check of all clone backends agree with the master...
379 foreach ( $this->backends as $index => $cloneBackend ) {
380 if ( $index === $this->masterIndex ) {
381 continue; // master
382 }
383
384 // Get the state of the file on the clone backend
385 $cloneParams = $this->substOpPaths( $params, $cloneBackend );
386 $clonePath = $cloneParams['src'];
387 $cloneStat = $cloneBackend->getFileStat( $cloneParams );
388 if ( $cloneStat === self::STAT_ERROR ) {
389 $status->fatal( 'backend-fail-stat', $path );
390 $this->logger->error( "$fname: file '$clonePath' is not available" );
391 continue;
392 }
393 $cloneSha1 = $cloneBackend->getFileSha1Base36( $cloneParams );
394 if ( ( $cloneSha1 !== false ) !== (bool)$cloneStat ) {
395 $status->fatal( 'backend-fail-hash', $path );
396 $this->logger->error( "$fname: file '$clonePath' hash does not match stat" );
397 continue;
398 }
399
400 if ( $masterSha1 === $cloneSha1 ) {
401 // File is either the same in both backends or absent from both backends
402 $this->logger->debug( "$fname: file '$clonePath' matches '$masterPath'" );
403 } elseif ( $masterSha1 !== false ) {
404 // File is either missing from or different in the clone backend
405 if (
406 $resyncMode === 'conservative' &&
407 $cloneStat &&
408 // @phan-suppress-next-line PhanTypeArraySuspiciousNullable
409 $cloneStat['mtime'] > $masterStat['mtime']
410 ) {
411 // Do not replace files with older ones; reduces the risk of data loss
412 $status->fatal( 'backend-fail-synced', $path );
413 } else {
414 // Copy the master backend file to the clone backend in overwrite mode
415 $fsFile = $masterBackend->getLocalReference( $masterParams );
416 $status->merge( $cloneBackend->quickStore( [
417 'src' => $fsFile,
418 'dst' => $clonePath
419 ] ) );
420 }
421 } elseif ( $masterStat === false ) {
422 // Stray file exists in the clone backend
423 if ( $resyncMode === 'conservative' ) {
424 // Do not delete stray files; reduces the risk of data loss
425 $status->fatal( 'backend-fail-synced', $path );
426 $this->logger->error( "$fname: not allowed to delete file '$clonePath'" );
427 } else {
428 // Delete the stay file from the clone backend
429 $status->merge( $cloneBackend->quickDelete( [ 'src' => $clonePath ] ) );
430 }
431 }
432 }
433 }
434
435 if ( !$status->isOK() ) {
436 $this->logger->error( "$fname: failed to resync: " . FormatJson::encode( $paths ) );
437 }
438
439 return $status;
440 }
441
448 protected function fileStoragePathsForOps( array $ops ) {
449 $paths = [];
450 foreach ( $ops as $op ) {
451 if ( isset( $op['src'] ) ) {
452 // For things like copy/move/delete with "ignoreMissingSource" and there
453 // is no source file, nothing should happen and there should be no errors.
454 if ( empty( $op['ignoreMissingSource'] )
455 || $this->fileExists( [ 'src' => $op['src'] ] )
456 ) {
457 $paths[] = $op['src'];
458 }
459 }
460 if ( isset( $op['srcs'] ) ) {
461 $paths = array_merge( $paths, $op['srcs'] );
462 }
463 if ( isset( $op['dst'] ) ) {
464 $paths[] = $op['dst'];
465 }
466 }
467
468 return array_values( array_unique( array_filter( $paths, [ FileBackend::class, 'isStoragePath' ] ) ) );
469 }
470
479 protected function substOpBatchPaths( array $ops, FileBackendStore $backend ) {
480 $newOps = []; // operations
481 foreach ( $ops as $op ) {
482 $newOp = $op; // operation
483 foreach ( [ 'src', 'srcs', 'dst', 'dir' ] as $par ) {
484 if ( isset( $newOp[$par] ) ) { // string or array
485 $newOp[$par] = $this->substPaths( $newOp[$par], $backend );
486 }
487 }
488 $newOps[] = $newOp;
489 }
490
491 return $newOps;
492 }
493
501 protected function substOpPaths( array $ops, FileBackendStore $backend ) {
502 $newOps = $this->substOpBatchPaths( [ $ops ], $backend );
503
504 return $newOps[0];
505 }
506
514 protected function substPaths( $paths, FileBackendStore $backend ) {
515 return preg_replace(
516 '!^mwstore://' . preg_quote( $this->name, '!' ) . '/!',
517 StringUtils::escapeRegexReplacement( "mwstore://{$backend->getName()}/" ),
518 $paths // string or array
519 );
520 }
521
529 protected function unsubstPaths( $paths, FileBackendStore $backend ) {
530 return preg_replace(
531 '!^mwstore://' . preg_quote( $backend->getName(), '!' ) . '/!',
532 StringUtils::escapeRegexReplacement( "mwstore://{$this->name}/" ),
533 $paths // string or array
534 );
535 }
536
541 protected function hasVolatileSources( array $ops ) {
542 foreach ( $ops as $op ) {
543 if ( $op['op'] === 'store' && !isset( $op['srcRef'] ) ) {
544 return true; // source file might be deleted anytime after do*Operations()
545 }
546 }
547
548 return false;
549 }
550
551 protected function doQuickOperationsInternal( array $ops, array $opts ) {
552 $status = $this->newStatus();
553 // Do the operations on the master backend; setting StatusValue fields
554 $realOps = $this->substOpBatchPaths( $ops, $this->backends[$this->masterIndex] );
555 $masterStatus = $this->backends[$this->masterIndex]->doQuickOperations( $realOps );
556 $status->merge( $masterStatus );
557 // Propagate the operations to the clone backends...
558 foreach ( $this->backends as $index => $backend ) {
559 if ( $index === $this->masterIndex ) {
560 continue; // done already
561 }
562
563 $realOps = $this->substOpBatchPaths( $ops, $backend );
564 if ( $this->asyncWrites && !$this->hasVolatileSources( $ops ) ) {
565 DeferredUpdates::addCallableUpdate(
566 static function () use ( $backend, $realOps ) {
567 $backend->doQuickOperations( $realOps );
568 }
569 );
570 } else {
571 $status->merge( $backend->doQuickOperations( $realOps ) );
572 }
573 }
574 // Make 'success', 'successCount', and 'failCount' fields reflect
575 // the overall operation, rather than all the batches for each backend.
576 // Do this by only using success values from the master backend's batch.
577 $status->success = $masterStatus->success;
578 $status->successCount = $masterStatus->successCount;
579 $status->failCount = $masterStatus->failCount;
580
581 return $status;
582 }
583
584 protected function doPrepare( array $params ) {
585 return $this->doDirectoryOp( 'prepare', $params );
586 }
587
588 protected function doSecure( array $params ) {
589 return $this->doDirectoryOp( 'secure', $params );
590 }
591
592 protected function doPublish( array $params ) {
593 return $this->doDirectoryOp( 'publish', $params );
594 }
595
596 protected function doClean( array $params ) {
597 return $this->doDirectoryOp( 'clean', $params );
598 }
599
605 protected function doDirectoryOp( $method, array $params ) {
606 $status = $this->newStatus();
607
608 $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
609 $masterStatus = $this->backends[$this->masterIndex]->$method( $realParams );
610 $status->merge( $masterStatus );
611
612 foreach ( $this->backends as $index => $backend ) {
613 if ( $index === $this->masterIndex ) {
614 continue; // already done
615 }
616
617 $realParams = $this->substOpPaths( $params, $backend );
618 if ( $this->asyncWrites ) {
619 DeferredUpdates::addCallableUpdate(
620 static function () use ( $backend, $method, $realParams ) {
621 $backend->$method( $realParams );
622 }
623 );
624 } else {
625 $status->merge( $backend->$method( $realParams ) );
626 }
627 }
628
629 return $status;
630 }
631
632 public function concatenate( array $params ) {
633 $status = $this->newStatus();
634 // We are writing to an FS file, so we don't need to do this per-backend
635 $index = $this->getReadIndexFromParams( $params );
636 $realParams = $this->substOpPaths( $params, $this->backends[$index] );
637
638 $status->merge( $this->backends[$index]->concatenate( $realParams ) );
639
640 return $status;
641 }
642
643 public function fileExists( array $params ) {
644 $index = $this->getReadIndexFromParams( $params );
645 $realParams = $this->substOpPaths( $params, $this->backends[$index] );
646
647 return $this->backends[$index]->fileExists( $realParams );
648 }
649
650 public function getFileTimestamp( array $params ) {
651 $index = $this->getReadIndexFromParams( $params );
652 $realParams = $this->substOpPaths( $params, $this->backends[$index] );
653
654 return $this->backends[$index]->getFileTimestamp( $realParams );
655 }
656
657 public function getFileSize( array $params ) {
658 $index = $this->getReadIndexFromParams( $params );
659 $realParams = $this->substOpPaths( $params, $this->backends[$index] );
660
661 return $this->backends[$index]->getFileSize( $realParams );
662 }
663
664 public function getFileStat( array $params ) {
665 $index = $this->getReadIndexFromParams( $params );
666 $realParams = $this->substOpPaths( $params, $this->backends[$index] );
667
668 return $this->backends[$index]->getFileStat( $realParams );
669 }
670
671 public function getFileXAttributes( array $params ) {
672 $index = $this->getReadIndexFromParams( $params );
673 $realParams = $this->substOpPaths( $params, $this->backends[$index] );
674
675 return $this->backends[$index]->getFileXAttributes( $realParams );
676 }
677
678 public function getFileContentsMulti( array $params ) {
679 $index = $this->getReadIndexFromParams( $params );
680 $realParams = $this->substOpPaths( $params, $this->backends[$index] );
681
682 $contentsM = $this->backends[$index]->getFileContentsMulti( $realParams );
683
684 $contents = []; // (path => FSFile) mapping using the proxy backend's name
685 foreach ( $contentsM as $path => $data ) {
686 $contents[$this->unsubstPaths( $path, $this->backends[$index] )] = $data;
687 }
688
689 return $contents;
690 }
691
692 public function getFileSha1Base36( array $params ) {
693 $index = $this->getReadIndexFromParams( $params );
694 $realParams = $this->substOpPaths( $params, $this->backends[$index] );
695
696 return $this->backends[$index]->getFileSha1Base36( $realParams );
697 }
698
699 public function getFileProps( array $params ) {
700 $index = $this->getReadIndexFromParams( $params );
701 $realParams = $this->substOpPaths( $params, $this->backends[$index] );
702
703 return $this->backends[$index]->getFileProps( $realParams );
704 }
705
706 public function streamFile( array $params ) {
707 $index = $this->getReadIndexFromParams( $params );
708 $realParams = $this->substOpPaths( $params, $this->backends[$index] );
709
710 return $this->backends[$index]->streamFile( $realParams );
711 }
712
713 public function getLocalReferenceMulti( array $params ) {
714 $index = $this->getReadIndexFromParams( $params );
715 $realParams = $this->substOpPaths( $params, $this->backends[$index] );
716
717 $fsFilesM = $this->backends[$index]->getLocalReferenceMulti( $realParams );
718
719 $fsFiles = []; // (path => FSFile) mapping using the proxy backend's name
720 foreach ( $fsFilesM as $path => $fsFile ) {
721 $fsFiles[$this->unsubstPaths( $path, $this->backends[$index] )] = $fsFile;
722 }
723
724 return $fsFiles;
725 }
726
727 public function getLocalCopyMulti( array $params ) {
728 $index = $this->getReadIndexFromParams( $params );
729 $realParams = $this->substOpPaths( $params, $this->backends[$index] );
730
731 $tempFilesM = $this->backends[$index]->getLocalCopyMulti( $realParams );
732
733 $tempFiles = []; // (path => TempFSFile) mapping using the proxy backend's name
734 foreach ( $tempFilesM as $path => $tempFile ) {
735 $tempFiles[$this->unsubstPaths( $path, $this->backends[$index] )] = $tempFile;
736 }
737
738 return $tempFiles;
739 }
740
741 public function getFileHttpUrl( array $params ) {
742 $index = $this->getReadIndexFromParams( $params );
743 $realParams = $this->substOpPaths( $params, $this->backends[$index] );
744
745 return $this->backends[$index]->getFileHttpUrl( $realParams );
746 }
747
748 public function directoryExists( array $params ) {
749 $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
750
751 return $this->backends[$this->masterIndex]->directoryExists( $realParams );
752 }
753
754 public function getDirectoryList( array $params ) {
755 $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
756
757 return $this->backends[$this->masterIndex]->getDirectoryList( $realParams );
758 }
759
760 public function getFileList( array $params ) {
761 if ( isset( $params['forWrite'] ) && $params['forWrite'] ) {
762 return $this->getFileListForWrite( $params );
763 }
764
765 $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
766 return $this->backends[$this->masterIndex]->getFileList( $realParams );
767 }
768
769 private function getFileListForWrite( $params ) {
770 $files = [];
771 // Get the list of thumbnails from all backends to allow
772 // deleting all of them. Otherwise, old thumbnails existing on
773 // one backend only won't get updated in reupload (T331138).
774 foreach ( $this->backends as $backend ) {
775 $realParams = $this->substOpPaths( $params, $backend );
776 $iterator = $backend->getFileList( $realParams );
777 if ( $iterator !== null ) {
778 foreach ( $iterator as $file ) {
779 $files[] = $file;
780 }
781 }
782 }
783
784 return array_unique( $files );
785 }
786
787 public function getFeatures() {
788 return $this->backends[$this->masterIndex]->getFeatures();
789 }
790
791 public function clearCache( array $paths = null ) {
792 foreach ( $this->backends as $backend ) {
793 $realPaths = is_array( $paths ) ? $this->substPaths( $paths, $backend ) : null;
794 $backend->clearCache( $realPaths );
795 }
796 }
797
798 public function preloadCache( array $paths ) {
799 $realPaths = $this->substPaths( $paths, $this->backends[$this->readIndex] );
800 $this->backends[$this->readIndex]->preloadCache( $realPaths );
801 }
802
803 public function preloadFileStat( array $params ) {
804 $index = $this->getReadIndexFromParams( $params );
805 $realParams = $this->substOpPaths( $params, $this->backends[$index] );
806
807 return $this->backends[$index]->preloadFileStat( $realParams );
808 }
809
810 public function getScopedLocksForOps( array $ops, StatusValue $status ) {
811 $realOps = $this->substOpBatchPaths( $ops, $this->backends[$this->masterIndex] );
812 $fileOps = $this->backends[$this->masterIndex]->getOperationsInternal( $realOps );
813 // Get the paths to lock from the master backend
814 $paths = $this->backends[$this->masterIndex]->getPathsToLockForOpsInternal( $fileOps );
815 // Get the paths under the proxy backend's name
816 $pbPaths = [
817 LockManager::LOCK_UW => $this->unsubstPaths(
818 $paths[LockManager::LOCK_UW],
819 $this->backends[$this->masterIndex]
820 ),
821 LockManager::LOCK_EX => $this->unsubstPaths(
822 $paths[LockManager::LOCK_EX],
823 $this->backends[$this->masterIndex]
824 )
825 ];
826
827 // Actually acquire the locks
828 return $this->getScopedFileLocks( $pbPaths, 'mixed', $status );
829 }
830
835 protected function getReadIndexFromParams( array $params ) {
836 return !empty( $params['latest'] ) ? $this->masterIndex : $this->readIndex;
837 }
838}
array $params
The job parameters.
Proxy backend that mirrors writes to several internal backends.
FileBackendStore[] $backends
Prioritized list of FileBackendStore objects.
fileExists(array $params)
Check if a file exists at a storage path in the backend.
getLocalReferenceMulti(array $params)
Like getLocalReference() except it takes an array of storage paths and yields an order-preserved map ...
getLocalCopyMulti(array $params)
Like getLocalCopy() except it takes an array of storage paths and yields an order preserved-map of st...
consistencyCheck(array $paths)
Check that a set of files are consistent across all internal backends.
unsubstPaths( $paths, FileBackendStore $backend)
Substitute the backend of internal storage paths with the proxy backend's name.
getFileSha1Base36(array $params)
Get a SHA-1 hash of the content of the file at a storage path in the backend.
int $readIndex
Index of read affinity backend.
clearCache(array $paths=null)
Invalidate any in-process file stat and property cache.
doQuickOperationsInternal(array $ops, array $opts)
getFileStat(array $params)
Get quick information about a file at a storage path in the backend.
getFileContentsMulti(array $params)
Like getFileContents() except it takes an array of storage paths and returns an order preserved map o...
preloadCache(array $paths)
Preload persistent file stat cache and property cache into in-process cache.
resyncFiles(array $paths, $resyncMode=true)
Check that a set of files are consistent across all internal backends and re-synchronize those files ...
getDirectoryList(array $params)
Get an iterator to list all directories under a storage directory.
__construct(array $config)
Construct a proxy backend that consists of several internal backends.
getFileList(array $params)
Get an iterator to list all stored files under a storage directory.
doOperationsInternal(array $ops, array $opts)
streamFile(array $params)
Stream the content of the file at a storage path in the backend.
int $masterIndex
Index of master backend.
accessibilityCheck(array $paths)
Check that a set of file paths are usable across all internal backends.
getFeatures()
Get the a bitfield of extra features supported by the backend medium.
fileStoragePathsForOps(array $ops)
Get a list of file storage paths to read or write for a list of operations.
concatenate(array $params)
Concatenate a list of storage files into a single file system file.
substOpPaths(array $ops, FileBackendStore $backend)
Same as substOpBatchPaths() but for a single operation.
substOpBatchPaths(array $ops, FileBackendStore $backend)
Substitute the backend name in storage path parameters for a set of operations with that of a given i...
getFileProps(array $params)
Get the properties of the content of the file at a storage path in the backend.
doDirectoryOp( $method, array $params)
getScopedLocksForOps(array $ops, StatusValue $status)
Get an array of scoped locks needed for a batch of file operations.
getFileHttpUrl(array $params)
Return an HTTP URL to a given file that requires no authentication to use.
preloadFileStat(array $params)
Preload file stat information (concurrently if possible) into in-process cache.
getFileSize(array $params)
Get the size (bytes) of a file at a storage path in the backend.
getFileXAttributes(array $params)
Get metadata about a file at a storage path in the backend.
substPaths( $paths, FileBackendStore $backend)
Substitute the backend of storage paths with an internal backend's name.
directoryExists(array $params)
Check if a directory exists at a given storage path.
getFileTimestamp(array $params)
Get the last-modified timestamp of the file at a storage path.
Base class for all backends using particular storage medium.
Defer callable updates to run later in the PHP process.
JSON formatter wrapper class.
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Base class for all file backend classes (including multi-write backends).
string $name
Unique backend name.
getName()
Get the unique backend name.
newStatus(... $args)
Yields the result of the status wrapper callback on either:
getScopedFileLocks(array $paths, $type, StatusValue $status, $timeout=0)
Lock the files at the given storage paths in the backend.