26use InvalidArgumentException;
29use Shellbox\Command\BoxedCommand;
32use Wikimedia\Timestamp\ConvertibleTimestamp;
72 private const CHECK_SIZE = 1;
74 private const CHECK_TIME = 2;
76 private const CHECK_SHA1 = 4;
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';
111 foreach ( $config[
'backends'] as $index => $beConfig ) {
112 $name = $beConfig[
'name'];
113 if ( isset( $namesUsed[
$name] ) ) {
114 throw new LogicException(
"Two or more backends defined with the name $name." );
116 $namesUsed[
$name] = 1;
118 unset( $beConfig[
'readOnly'] );
119 unset( $beConfig[
'lockManager'] );
122 if ( !empty( $beConfig[
'isMultiMaster'] ) ) {
123 if ( $this->masterIndex >= 0 ) {
124 throw new LogicException(
'More than one master backend defined.' );
126 $this->masterIndex = $index;
128 if ( !empty( $beConfig[
'readAffinity'] ) ) {
129 $this->readIndex = $index;
132 if ( !isset( $beConfig[
'class'] ) ) {
133 throw new InvalidArgumentException(
'No class given for a backend config.' );
135 $class = $beConfig[
'class'];
136 $this->backends[$index] =
new $class( $beConfig );
138 if ( $this->masterIndex < 0 ) {
139 throw new LogicException(
'No master backend defined.' );
141 if ( $this->readIndex < 0 ) {
154 if ( empty( $opts[
'nonLocking'] ) ) {
156 if ( !$status->isOK() ) {
164 $opts[
'preserveCache'] =
true;
167 if ( !$status->isOK() ) {
172 if ( !$syncStatus->isOK() ) {
173 $this->logger->error(
174 "$fname: failed sync check: " . implode(
', ', $relevantPaths )
178 $this->autoResync ===
false ||
179 !$this->
resyncFiles( $relevantPaths, $this->autoResync )->isOK()
181 $status->merge( $syncStatus );
188 $masterStatus = $mbe->doOperations( $realOps, $opts );
189 $status->merge( $masterStatus );
193 if ( $masterStatus->isOK() && $masterStatus->successCount > 0 ) {
194 foreach ( $this->backends as $index => $backend ) {
195 if ( $index === $this->masterIndex ) {
204 $backend, $realOps, $opts, $scopeLock, $relevantPaths, $fname
206 $this->logger->debug(
207 "$fname: '{$backend->getName()}' async replication; paths: " .
208 implode(
', ', $relevantPaths )
210 $backend->doOperations( $realOps, $opts );
214 $this->logger->debug(
215 "$fname: '{$backend->getName()}' sync replication; paths: " .
216 implode(
', ', $relevantPaths )
218 $status->merge( $backend->doOperations( $realOps, $opts ) );
225 $status->success = $masterStatus->success;
226 $status->successCount = $masterStatus->successCount;
227 $status->failCount = $masterStatus->failCount;
243 if ( $this->syncChecks == 0 || count( $this->backends ) <= 1 ) {
248 foreach ( $this->backends as $backend ) {
249 $realPaths = $this->
substPaths( $paths, $backend );
250 $backend->preloadFileStat( [
'srcs' => $realPaths,
'latest' =>
true ] );
253 foreach ( $paths as
$path ) {
258 $masterStat = $masterBackend->getFileStat( $masterParams );
259 if ( $masterStat === self::STAT_ERROR ) {
260 $status->fatal(
'backend-fail-stat',
$path );
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 );
274 foreach ( $this->backends as $index => $cloneBackend ) {
275 if ( $index === $this->masterIndex ) {
281 $cloneStat = $cloneBackend->getFileStat( $cloneParams );
287 $status->fatal(
'backend-fail-synced',
$path );
289 ( $this->syncChecks & self::CHECK_SIZE ) &&
290 $cloneStat[
'size'] !== $masterStat[
'size']
293 $status->fatal(
'backend-fail-synced',
$path );
295 ( $this->syncChecks & self::CHECK_TIME ) &&
297 (
int)ConvertibleTimestamp::convert( TS_UNIX, $masterStat[
'mtime'] ) -
298 (
int)ConvertibleTimestamp::convert( TS_UNIX, $cloneStat[
'mtime'] )
302 $status->fatal(
'backend-fail-synced',
$path );
304 ( $this->syncChecks & self::CHECK_SHA1 ) &&
305 $cloneBackend->getFileSha1Base36( $cloneParams ) !== $masterSha1
308 $status->fatal(
'backend-fail-synced',
$path );
314 $status->fatal(
'backend-fail-synced',
$path );
331 if ( count( $this->backends ) <= 1 ) {
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 );
361 foreach ( $paths as
$path ) {
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" );
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" );
381 foreach ( $this->backends as $index => $cloneBackend ) {
382 if ( $index === $this->masterIndex ) {
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" );
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" );
402 if ( $masterSha1 === $cloneSha1 ) {
404 $this->logger->debug(
"$fname: file '$clonePath' matches '$masterPath'" );
405 } elseif ( $masterSha1 !==
false ) {
408 $resyncMode ===
'conservative' &&
411 $cloneStat[
'mtime'] > $masterStat[
'mtime']
414 $status->fatal(
'backend-fail-synced',
$path );
417 $fsFile = $masterBackend->getLocalReference( $masterParams );
418 $status->merge( $cloneBackend->quickStore( [
423 } elseif ( $masterStat ===
false ) {
425 if ( $resyncMode ===
'conservative' ) {
427 $status->fatal(
'backend-fail-synced',
$path );
428 $this->logger->error(
"$fname: not allowed to delete file '$clonePath'" );
431 $status->merge( $cloneBackend->quickDelete( [
'src' => $clonePath ] ) );
437 if ( !$status->isOK() ) {
438 $this->logger->error(
"$fname: failed to resync: " . implode(
', ', $paths ) );
452 foreach ( $ops as $op ) {
453 if ( isset( $op[
'src'] ) ) {
456 if ( empty( $op[
'ignoreMissingSource'] )
457 || $this->
fileExists( [
'src' => $op[
'src'] ] )
459 $paths[] = $op[
'src'];
462 if ( isset( $op[
'srcs'] ) ) {
463 $paths = array_merge( $paths, $op[
'srcs'] );
465 if ( isset( $op[
'dst'] ) ) {
466 $paths[] = $op[
'dst'];
470 return array_values( array_unique( array_filter( $paths, [ FileBackend::class,
'isStoragePath' ] ) ) );
483 foreach ( $ops as $op ) {
485 foreach ( [
'src',
'srcs',
'dst',
'dir' ] as $par ) {
486 if ( isset( $newOp[$par] ) ) {
487 $newOp[$par] = $this->
substPaths( $newOp[$par], $backend );
518 '!^mwstore://' . preg_quote( $this->name,
'!' ) .
'/!',
519 StringUtils::escapeRegexReplacement(
"mwstore://{$backend->getName()}/" ),
533 '!^mwstore://' . preg_quote( $backend->
getName(),
'!' ) .
'/!',
534 StringUtils::escapeRegexReplacement(
"mwstore://{$this->name}/" ),
544 foreach ( $ops as $op ) {
545 if ( $op[
'op'] ===
'store' && !isset( $op[
'srcRef'] ) ) {
556 $realOps = $this->
substOpBatchPaths( $ops, $this->backends[$this->masterIndex] );
558 $status->merge( $masterStatus );
560 foreach ( $this->backends as $index => $backend ) {
561 if ( $index === $this->masterIndex ) {
568 static function () use ( $backend, $realOps ) {
569 $backend->doQuickOperations( $realOps );
573 $status->merge( $backend->doQuickOperations( $realOps ) );
579 $status->success = $masterStatus->success;
580 $status->successCount = $masterStatus->successCount;
581 $status->failCount = $masterStatus->failCount;
610 $realParams = $this->
substOpPaths( $params, $this->backends[$this->masterIndex] );
612 $status->merge( $masterStatus );
614 foreach ( $this->backends as $index => $backend ) {
615 if ( $index === $this->masterIndex ) {
620 if ( $this->asyncWrites ) {
622 static function () use ( $backend, $method, $realParams ) {
623 $backend->$method( $realParams );
627 $status->merge( $backend->$method( $realParams ) );
638 $realParams = $this->
substOpPaths( $params, $this->backends[$index] );
640 $status->merge( $this->backends[$index]->
concatenate( $realParams ) );
647 $realParams = $this->
substOpPaths( $params, $this->backends[$index] );
649 return $this->backends[$index]->fileExists( $realParams );
654 $realParams = $this->
substOpPaths( $params, $this->backends[$index] );
656 return $this->backends[$index]->getFileTimestamp( $realParams );
661 $realParams = $this->
substOpPaths( $params, $this->backends[$index] );
663 return $this->backends[$index]->getFileSize( $realParams );
668 $realParams = $this->
substOpPaths( $params, $this->backends[$index] );
670 return $this->backends[$index]->getFileStat( $realParams );
675 $realParams = $this->
substOpPaths( $params, $this->backends[$index] );
677 return $this->backends[$index]->getFileXAttributes( $realParams );
682 $realParams = $this->
substOpPaths( $params, $this->backends[$index] );
684 $contentsM = $this->backends[$index]->getFileContentsMulti( $realParams );
687 foreach ( $contentsM as
$path => $data ) {
696 $realParams = $this->
substOpPaths( $params, $this->backends[$index] );
698 return $this->backends[$index]->getFileSha1Base36( $realParams );
703 $realParams = $this->
substOpPaths( $params, $this->backends[$index] );
705 return $this->backends[$index]->getFileProps( $realParams );
710 $realParams = $this->
substOpPaths( $params, $this->backends[$index] );
712 return $this->backends[$index]->streamFile( $realParams );
717 $realParams = $this->
substOpPaths( $params, $this->backends[$index] );
719 $fsFilesM = $this->backends[$index]->getLocalReferenceMulti( $realParams );
722 foreach ( $fsFilesM as
$path => $fsFile ) {
731 $realParams = $this->
substOpPaths( $params, $this->backends[$index] );
733 $tempFilesM = $this->backends[$index]->getLocalCopyMulti( $realParams );
736 foreach ( $tempFilesM as
$path => $tempFile ) {
745 $realParams = $this->
substOpPaths( $params, $this->backends[$index] );
747 return $this->backends[$index]->getFileHttpUrl( $realParams );
754 $realParams = $this->
substOpPaths( $params, $this->backends[$index] );
755 return $this->backends[$index]->addShellboxInputFile( $command, $boxedName, $realParams );
759 $realParams = $this->
substOpPaths( $params, $this->backends[$this->masterIndex] );
765 $realParams = $this->
substOpPaths( $params, $this->backends[$this->masterIndex] );
772 return $this->getFileListForWrite(
$params );
775 $realParams = $this->
substOpPaths( $params, $this->backends[$this->masterIndex] );
779 private function getFileListForWrite(
$params ) {
784 foreach ( $this->backends as $backend ) {
786 $iterator = $backend->getFileList( $realParams );
787 if ( $iterator !==
null ) {
788 foreach ( $iterator as $file ) {
794 return array_unique( $files );
802 foreach ( $this->backends as $backend ) {
803 $realPaths = is_array( $paths ) ? $this->
substPaths( $paths, $backend ) :
null;
804 $backend->clearCache( $realPaths );
809 $realPaths = $this->
substPaths( $paths, $this->backends[$this->readIndex] );
815 $realParams = $this->
substOpPaths( $params, $this->backends[$index] );
817 return $this->backends[$index]->preloadFileStat( $realParams );
821 $realOps = $this->
substOpBatchPaths( $ops, $this->backends[$this->masterIndex] );
824 $paths = $this->backends[
$this->masterIndex]->getPathsToLockForOpsInternal( $fileOps );
828 $paths[LockManager::LOCK_UW],
829 $this->backends[$this->masterIndex]
832 $paths[LockManager::LOCK_EX],
833 $this->backends[$this->masterIndex]
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.