26use InvalidArgumentException;
31use Shellbox\Command\BoxedCommand;
34use Wikimedia\Timestamp\ConvertibleTimestamp;
74 private const CHECK_SIZE = 1;
76 private const CHECK_TIME = 2;
78 private const CHECK_SHA1 = 4;
108 parent::__construct( $config );
109 $this->syncChecks = $config[
'syncChecks'] ?? self::CHECK_SIZE;
110 $this->autoResync = $config[
'autoResync'] ??
false;
111 $this->asyncWrites = isset( $config[
'replication'] ) && $config[
'replication'] ===
'async';
115 foreach ( $config[
'backends'] as $index => $beConfig ) {
116 $name = $beConfig[
'name'];
117 if ( isset( $namesUsed[
$name] ) ) {
118 throw new LogicException(
"Two or more backends defined with the name $name." );
120 $namesUsed[
$name] = 1;
122 unset( $beConfig[
'readOnly'] );
123 unset( $beConfig[
'lockManager'] );
126 if ( !empty( $beConfig[
'isMultiMaster'] ) ) {
127 if ( $this->masterIndex >= 0 ) {
128 throw new LogicException(
'More than one master backend defined.' );
130 $this->masterIndex = $index;
132 if ( !empty( $beConfig[
'readAffinity'] ) ) {
133 $this->readIndex = $index;
136 if ( !isset( $beConfig[
'class'] ) ) {
137 throw new InvalidArgumentException(
'No class given for a backend config.' );
139 $class = $beConfig[
'class'];
140 $this->backends[$index] =
new $class( $beConfig );
142 if ( $this->masterIndex < 0 ) {
143 throw new LogicException(
'No master backend defined.' );
145 if ( $this->readIndex < 0 ) {
158 if ( empty( $opts[
'nonLocking'] ) ) {
160 if ( !$status->isOK() ) {
168 $opts[
'preserveCache'] =
true;
171 if ( !$status->isOK() ) {
176 if ( !$syncStatus->isOK() ) {
177 $this->logger->error(
178 "$fname: failed sync check: " . FormatJson::encode( $relevantPaths )
182 $this->autoResync ===
false ||
183 !$this->
resyncFiles( $relevantPaths, $this->autoResync )->isOK()
185 $status->merge( $syncStatus );
192 $masterStatus = $mbe->doOperations( $realOps, $opts );
193 $status->merge( $masterStatus );
197 if ( $masterStatus->isOK() && $masterStatus->successCount > 0 ) {
198 foreach ( $this->backends as $index => $backend ) {
199 if ( $index === $this->masterIndex ) {
206 DeferredUpdates::addCallableUpdate(
208 $backend, $realOps, $opts, $scopeLock, $relevantPaths, $fname
210 $this->logger->debug(
211 "$fname: '{$backend->getName()}' async replication; paths: " .
212 FormatJson::encode( $relevantPaths )
214 $backend->doOperations( $realOps, $opts );
218 $this->logger->debug(
219 "$fname: '{$backend->getName()}' sync replication; paths: " .
220 FormatJson::encode( $relevantPaths )
222 $status->merge( $backend->doOperations( $realOps, $opts ) );
229 $status->success = $masterStatus->success;
230 $status->successCount = $masterStatus->successCount;
231 $status->failCount = $masterStatus->failCount;
247 if ( $this->syncChecks == 0 || count( $this->backends ) <= 1 ) {
252 foreach ( $this->backends as $backend ) {
253 $realPaths = $this->
substPaths( $paths, $backend );
254 $backend->preloadFileStat( [
'srcs' => $realPaths,
'latest' =>
true ] );
257 foreach ( $paths as
$path ) {
262 $masterStat = $masterBackend->getFileStat( $masterParams );
263 if ( $masterStat === self::STAT_ERROR ) {
264 $status->fatal(
'backend-fail-stat',
$path );
267 if ( $this->syncChecks & self::CHECK_SHA1 ) {
268 $masterSha1 = $masterBackend->getFileSha1Base36( $masterParams );
269 if ( ( $masterSha1 !==
false ) !== (
bool)$masterStat ) {
270 $status->fatal(
'backend-fail-hash',
$path );
278 foreach ( $this->backends as $index => $cloneBackend ) {
279 if ( $index === $this->masterIndex ) {
285 $cloneStat = $cloneBackend->getFileStat( $cloneParams );
291 $status->fatal(
'backend-fail-synced',
$path );
293 ( $this->syncChecks & self::CHECK_SIZE ) &&
294 $cloneStat[
'size'] !== $masterStat[
'size']
297 $status->fatal(
'backend-fail-synced',
$path );
299 ( $this->syncChecks & self::CHECK_TIME ) &&
301 (
int)ConvertibleTimestamp::convert( TS_UNIX, $masterStat[
'mtime'] ) -
302 (
int)ConvertibleTimestamp::convert( TS_UNIX, $cloneStat[
'mtime'] )
306 $status->fatal(
'backend-fail-synced',
$path );
308 ( $this->syncChecks & self::CHECK_SHA1 ) &&
309 $cloneBackend->getFileSha1Base36( $cloneParams ) !== $masterSha1
312 $status->fatal(
'backend-fail-synced',
$path );
318 $status->fatal(
'backend-fail-synced',
$path );
335 if ( count( $this->backends ) <= 1 ) {
339 foreach ( $paths as
$path ) {
340 foreach ( $this->backends as $backend ) {
341 $realPath = $this->
substPaths( $path, $backend );
342 if ( !$backend->isPathUsableInternal( $realPath ) ) {
343 $status->fatal(
'backend-fail-usable',
$path );
365 foreach ( $paths as
$path ) {
370 $masterPath = $masterParams[
'src'];
371 $masterStat = $masterBackend->getFileStat( $masterParams );
372 if ( $masterStat === self::STAT_ERROR ) {
373 $status->fatal(
'backend-fail-stat',
$path );
374 $this->logger->error(
"$fname: file '$masterPath' is not available" );
377 $masterSha1 = $masterBackend->getFileSha1Base36( $masterParams );
378 if ( ( $masterSha1 !==
false ) !== (
bool)$masterStat ) {
379 $status->fatal(
'backend-fail-hash',
$path );
380 $this->logger->error(
"$fname: file '$masterPath' hash does not match stat" );
385 foreach ( $this->backends as $index => $cloneBackend ) {
386 if ( $index === $this->masterIndex ) {
392 $clonePath = $cloneParams[
'src'];
393 $cloneStat = $cloneBackend->getFileStat( $cloneParams );
394 if ( $cloneStat === self::STAT_ERROR ) {
395 $status->fatal(
'backend-fail-stat',
$path );
396 $this->logger->error(
"$fname: file '$clonePath' is not available" );
399 $cloneSha1 = $cloneBackend->getFileSha1Base36( $cloneParams );
400 if ( ( $cloneSha1 !==
false ) !== (
bool)$cloneStat ) {
401 $status->fatal(
'backend-fail-hash',
$path );
402 $this->logger->error(
"$fname: file '$clonePath' hash does not match stat" );
406 if ( $masterSha1 === $cloneSha1 ) {
408 $this->logger->debug(
"$fname: file '$clonePath' matches '$masterPath'" );
409 } elseif ( $masterSha1 !==
false ) {
412 $resyncMode ===
'conservative' &&
415 $cloneStat[
'mtime'] > $masterStat[
'mtime']
418 $status->fatal(
'backend-fail-synced',
$path );
421 $fsFile = $masterBackend->getLocalReference( $masterParams );
422 $status->merge( $cloneBackend->quickStore( [
427 } elseif ( $masterStat ===
false ) {
429 if ( $resyncMode ===
'conservative' ) {
431 $status->fatal(
'backend-fail-synced',
$path );
432 $this->logger->error(
"$fname: not allowed to delete file '$clonePath'" );
435 $status->merge( $cloneBackend->quickDelete( [
'src' => $clonePath ] ) );
441 if ( !$status->isOK() ) {
442 $this->logger->error(
"$fname: failed to resync: " . FormatJson::encode( $paths ) );
456 foreach ( $ops as $op ) {
457 if ( isset( $op[
'src'] ) ) {
460 if ( empty( $op[
'ignoreMissingSource'] )
461 || $this->
fileExists( [
'src' => $op[
'src'] ] )
463 $paths[] = $op[
'src'];
466 if ( isset( $op[
'srcs'] ) ) {
467 $paths = array_merge( $paths, $op[
'srcs'] );
469 if ( isset( $op[
'dst'] ) ) {
470 $paths[] = $op[
'dst'];
474 return array_values( array_unique( array_filter( $paths, [ FileBackend::class,
'isStoragePath' ] ) ) );
487 foreach ( $ops as $op ) {
489 foreach ( [
'src',
'srcs',
'dst',
'dir' ] as $par ) {
490 if ( isset( $newOp[$par] ) ) {
491 $newOp[$par] = $this->
substPaths( $newOp[$par], $backend );
522 '!^mwstore://' . preg_quote( $this->name,
'!' ) .
'/!',
523 StringUtils::escapeRegexReplacement(
"mwstore://{$backend->getName()}/" ),
537 '!^mwstore://' . preg_quote( $backend->
getName(),
'!' ) .
'/!',
538 StringUtils::escapeRegexReplacement(
"mwstore://{$this->name}/" ),
548 foreach ( $ops as $op ) {
549 if ( $op[
'op'] ===
'store' && !isset( $op[
'srcRef'] ) ) {
560 $realOps = $this->
substOpBatchPaths( $ops, $this->backends[$this->masterIndex] );
562 $status->merge( $masterStatus );
564 foreach ( $this->backends as $index => $backend ) {
565 if ( $index === $this->masterIndex ) {
571 DeferredUpdates::addCallableUpdate(
572 static function () use ( $backend, $realOps ) {
573 $backend->doQuickOperations( $realOps );
577 $status->merge( $backend->doQuickOperations( $realOps ) );
583 $status->success = $masterStatus->success;
584 $status->successCount = $masterStatus->successCount;
585 $status->failCount = $masterStatus->failCount;
614 $realParams = $this->
substOpPaths( $params, $this->backends[$this->masterIndex] );
616 $status->merge( $masterStatus );
618 foreach ( $this->backends as $index => $backend ) {
619 if ( $index === $this->masterIndex ) {
624 if ( $this->asyncWrites ) {
625 DeferredUpdates::addCallableUpdate(
626 static function () use ( $backend, $method, $realParams ) {
627 $backend->$method( $realParams );
631 $status->merge( $backend->$method( $realParams ) );
642 $realParams = $this->
substOpPaths( $params, $this->backends[$index] );
644 $status->merge( $this->backends[$index]->
concatenate( $realParams ) );
651 $realParams = $this->
substOpPaths( $params, $this->backends[$index] );
653 return $this->backends[$index]->fileExists( $realParams );
658 $realParams = $this->
substOpPaths( $params, $this->backends[$index] );
660 return $this->backends[$index]->getFileTimestamp( $realParams );
665 $realParams = $this->
substOpPaths( $params, $this->backends[$index] );
667 return $this->backends[$index]->getFileSize( $realParams );
672 $realParams = $this->
substOpPaths( $params, $this->backends[$index] );
674 return $this->backends[$index]->getFileStat( $realParams );
679 $realParams = $this->
substOpPaths( $params, $this->backends[$index] );
681 return $this->backends[$index]->getFileXAttributes( $realParams );
686 $realParams = $this->
substOpPaths( $params, $this->backends[$index] );
688 $contentsM = $this->backends[$index]->getFileContentsMulti( $realParams );
691 foreach ( $contentsM as
$path => $data ) {
700 $realParams = $this->
substOpPaths( $params, $this->backends[$index] );
702 return $this->backends[$index]->getFileSha1Base36( $realParams );
707 $realParams = $this->
substOpPaths( $params, $this->backends[$index] );
709 return $this->backends[$index]->getFileProps( $realParams );
714 $realParams = $this->
substOpPaths( $params, $this->backends[$index] );
716 return $this->backends[$index]->streamFile( $realParams );
721 $realParams = $this->
substOpPaths( $params, $this->backends[$index] );
723 $fsFilesM = $this->backends[$index]->getLocalReferenceMulti( $realParams );
726 foreach ( $fsFilesM as
$path => $fsFile ) {
735 $realParams = $this->
substOpPaths( $params, $this->backends[$index] );
737 $tempFilesM = $this->backends[$index]->getLocalCopyMulti( $realParams );
740 foreach ( $tempFilesM as
$path => $tempFile ) {
749 $realParams = $this->
substOpPaths( $params, $this->backends[$index] );
751 return $this->backends[$index]->getFileHttpUrl( $realParams );
758 $realParams = $this->
substOpPaths( $params, $this->backends[$index] );
759 return $this->backends[$index]->addShellboxInputFile( $command, $boxedName, $realParams );
763 $realParams = $this->
substOpPaths( $params, $this->backends[$this->masterIndex] );
769 $realParams = $this->
substOpPaths( $params, $this->backends[$this->masterIndex] );
776 return $this->getFileListForWrite(
$params );
779 $realParams = $this->
substOpPaths( $params, $this->backends[$this->masterIndex] );
783 private function getFileListForWrite(
$params ) {
788 foreach ( $this->backends as $backend ) {
790 $iterator = $backend->getFileList( $realParams );
791 if ( $iterator !==
null ) {
792 foreach ( $iterator as $file ) {
798 return array_unique( $files );
806 foreach ( $this->backends as $backend ) {
807 $realPaths = is_array( $paths ) ? $this->
substPaths( $paths, $backend ) :
null;
808 $backend->clearCache( $realPaths );
813 $realPaths = $this->
substPaths( $paths, $this->backends[$this->readIndex] );
819 $realParams = $this->
substOpPaths( $params, $this->backends[$index] );
821 return $this->backends[$index]->preloadFileStat( $realParams );
825 $realOps = $this->
substOpBatchPaths( $ops, $this->backends[$this->masterIndex] );
828 $paths = $this->backends[
$this->masterIndex]->getPathsToLockForOpsInternal( $fileOps );
832 $paths[LockManager::LOCK_UW],
833 $this->backends[$this->masterIndex]
836 $paths[LockManager::LOCK_EX],
837 $this->backends[$this->masterIndex]
855class_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.