Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
80.60% |
54 / 67 |
|
0.00% |
0 / 2 |
CRAP | |
0.00% |
0 / 1 |
| FileOpBatch | |
81.82% |
54 / 66 |
|
0.00% |
0 / 2 |
24.91 | |
0.00% |
0 / 1 |
| attempt | |
94.44% |
34 / 36 |
|
0.00% |
0 / 1 |
11.02 | |||
| runParallelBatches | |
66.67% |
20 / 30 |
|
0.00% |
0 / 1 |
15.48 | |||
| 1 | <?php |
| 2 | /** |
| 3 | * Helper class for representing batch file operations. |
| 4 | * |
| 5 | * @license GPL-2.0-or-later |
| 6 | * @file |
| 7 | * @ingroup FileBackend |
| 8 | */ |
| 9 | |
| 10 | namespace Wikimedia\FileBackend; |
| 11 | |
| 12 | use StatusValue; |
| 13 | use Wikimedia\FileBackend\FileOpHandle\FileBackendStoreOpHandle; |
| 14 | use Wikimedia\FileBackend\FileOps\FileOp; |
| 15 | use Wikimedia\FileBackend\FileOps\FileStatePredicates; |
| 16 | |
| 17 | /** |
| 18 | * Helper class for representing batch file operations. |
| 19 | * Do not use this class from places outside FileBackend. |
| 20 | * |
| 21 | * Methods should avoid throwing exceptions at all costs. |
| 22 | * |
| 23 | * @ingroup FileBackend |
| 24 | * @since 1.20 |
| 25 | */ |
| 26 | class FileOpBatch { |
| 27 | /* Timeout related parameters */ |
| 28 | private const MAX_BATCH_SIZE = 1000; // integer |
| 29 | |
| 30 | /** |
| 31 | * Attempt to perform a series of file operations. |
| 32 | * Callers are responsible for handling file locking. |
| 33 | * |
| 34 | * $opts is an array of options, including: |
| 35 | * - force : Errors that would normally cause a rollback do not. |
| 36 | * The remaining operations are still attempted if any fail. |
| 37 | * - concurrency : Try to do this many operations in parallel when possible. |
| 38 | * |
| 39 | * The resulting StatusValue will be "OK" unless: |
| 40 | * - a) unexpected operation errors occurred (network partitions, disk full...) |
| 41 | * - b) predicted operation errors occurred and 'force' was not set |
| 42 | * |
| 43 | * @param FileOp[] $performOps List of FileOp operations |
| 44 | * @param array $opts Batch operation options |
| 45 | * @return StatusValue |
| 46 | */ |
| 47 | public static function attempt( array $performOps, array $opts ) { |
| 48 | $status = StatusValue::newGood(); |
| 49 | |
| 50 | $n = count( $performOps ); |
| 51 | if ( $n > self::MAX_BATCH_SIZE ) { |
| 52 | $status->fatal( 'backend-fail-batchsize', $n, self::MAX_BATCH_SIZE ); |
| 53 | |
| 54 | return $status; |
| 55 | } |
| 56 | |
| 57 | $ignoreErrors = !empty( $opts['force'] ); |
| 58 | $maxConcurrency = $opts['concurrency'] ?? 1; |
| 59 | |
| 60 | $predicates = new FileStatePredicates(); // account for previous ops in prechecks |
| 61 | $curBatch = []; // concurrent FileOp sub-batch accumulation |
| 62 | $curBatchDeps = FileOp::newDependencies(); // paths used in FileOp sub-batch |
| 63 | $pPerformOps = []; // ordered list of concurrent FileOp sub-batches |
| 64 | $lastBackend = null; // last op backend name |
| 65 | // Do pre-checks for each operation; abort on failure... |
| 66 | foreach ( $performOps as $index => $fileOp ) { |
| 67 | $backendName = $fileOp->getBackend()->getName(); |
| 68 | // Decide if this op can be done concurrently within this sub-batch |
| 69 | // or if a new concurrent sub-batch must be started after this one... |
| 70 | if ( $fileOp->dependsOn( $curBatchDeps ) |
| 71 | || count( $curBatch ) >= $maxConcurrency |
| 72 | || ( $backendName !== $lastBackend && count( $curBatch ) ) |
| 73 | ) { |
| 74 | $pPerformOps[] = $curBatch; // push this batch |
| 75 | $curBatch = []; // start a new sub-batch |
| 76 | $curBatchDeps = FileOp::newDependencies(); |
| 77 | } |
| 78 | $lastBackend = $backendName; |
| 79 | $curBatch[$index] = $fileOp; // keep index |
| 80 | // Update list of affected paths in this batch |
| 81 | $curBatchDeps = $fileOp->applyDependencies( $curBatchDeps ); |
| 82 | // Simulate performing the operation... |
| 83 | $subStatus = $fileOp->precheck( $predicates ); // updates $predicates |
| 84 | $status->merge( $subStatus ); |
| 85 | if ( !$subStatus->isOK() ) { |
| 86 | // operation failed? |
| 87 | $status->success[$index] = false; |
| 88 | ++$status->failCount; |
| 89 | if ( !$ignoreErrors ) { |
| 90 | return $status; // abort |
| 91 | } |
| 92 | } |
| 93 | } |
| 94 | // Push the last sub-batch |
| 95 | if ( count( $curBatch ) ) { |
| 96 | $pPerformOps[] = $curBatch; |
| 97 | } |
| 98 | |
| 99 | if ( $ignoreErrors ) { // treat precheck() fatals as mere warnings |
| 100 | $status->setResult( true, $status->value ); |
| 101 | } |
| 102 | |
| 103 | // Attempt each operation (in parallel if allowed and possible)... |
| 104 | self::runParallelBatches( $pPerformOps, $status ); |
| 105 | |
| 106 | return $status; |
| 107 | } |
| 108 | |
| 109 | /** |
| 110 | * Attempt a list of file operations sub-batches in series. |
| 111 | * |
| 112 | * The operations *in* each sub-batch will be done in parallel. |
| 113 | * The caller is responsible for making sure the operations |
| 114 | * within any given sub-batch do not depend on each other. |
| 115 | * This will abort remaining ops on failure. |
| 116 | * |
| 117 | * @param FileOp[][] $pPerformOps Batches of file ops (batches use original indexes) |
| 118 | * @param StatusValue $status |
| 119 | */ |
| 120 | protected static function runParallelBatches( array $pPerformOps, StatusValue $status ) { |
| 121 | $aborted = false; // set to true on unexpected errors |
| 122 | foreach ( $pPerformOps as $performOpsBatch ) { |
| 123 | if ( $aborted ) { // check batch op abort flag... |
| 124 | // We can't continue (even with $ignoreErrors) as $predicates is wrong. |
| 125 | // Log the remaining ops as failed for recovery... |
| 126 | foreach ( $performOpsBatch as $i => $fileOp ) { |
| 127 | $status->success[$i] = false; |
| 128 | ++$status->failCount; |
| 129 | $fileOp->logFailure( 'attempt_aborted' ); |
| 130 | } |
| 131 | continue; |
| 132 | } |
| 133 | /** @var StatusValue[] $statuses */ |
| 134 | $statuses = []; |
| 135 | $opHandles = []; |
| 136 | // Get the backend; all sub-batch ops belong to a single backend |
| 137 | /** @var FileBackendStore $backend */ |
| 138 | $backend = reset( $performOpsBatch )->getBackend(); |
| 139 | // Get the operation handles or actually do it if there is just one. |
| 140 | // If attemptAsync() returns a StatusValue, it was either due to an error |
| 141 | // or the backend does not support async ops and did it synchronously. |
| 142 | foreach ( $performOpsBatch as $i => $fileOp ) { |
| 143 | if ( !isset( $status->success[$i] ) ) { // didn't already fail in precheck() |
| 144 | // Parallel ops may be disabled in config due to missing dependencies, |
| 145 | // (e.g. needing popen()). When they are, $performOpsBatch has size 1. |
| 146 | $subStatus = ( count( $performOpsBatch ) > 1 ) |
| 147 | ? $fileOp->attemptAsync() |
| 148 | : $fileOp->attempt(); |
| 149 | if ( $subStatus->value instanceof FileBackendStoreOpHandle ) { |
| 150 | $opHandles[$i] = $subStatus->value; // deferred |
| 151 | } else { |
| 152 | $statuses[$i] = $subStatus; // done already |
| 153 | } |
| 154 | } |
| 155 | } |
| 156 | // Try to do all the operations concurrently... |
| 157 | $statuses += $backend->executeOpHandlesInternal( $opHandles ); |
| 158 | // Marshall and merge all the responses (blocking)... |
| 159 | foreach ( $performOpsBatch as $i => $fileOp ) { |
| 160 | if ( !isset( $status->success[$i] ) ) { // didn't already fail in precheck() |
| 161 | $subStatus = $statuses[$i]; |
| 162 | $status->merge( $subStatus ); |
| 163 | if ( $subStatus->isOK() ) { |
| 164 | $status->success[$i] = true; |
| 165 | ++$status->successCount; |
| 166 | } else { |
| 167 | $status->success[$i] = false; |
| 168 | ++$status->failCount; |
| 169 | $aborted = true; // set abort flag; we can't continue |
| 170 | } |
| 171 | } |
| 172 | } |
| 173 | } |
| 174 | } |
| 175 | } |
| 176 | |
| 177 | /** @deprecated class alias since 1.43 */ |
| 178 | class_alias( FileOpBatch::class, 'FileOpBatch' ); |