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