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