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