r103632 MediaWiki - Code Review archive

Repository:MediaWiki
Revision:r103631‎ | r103632 | r103633 >
Date:01:55, 19 November 2011
Author:aaron
Status:deferred
Tags:
Comment:
* Refactored locking classes (mostly supported batch locking)
* Added FSFileLockManager and DBFileLockManager classes
* More work on FSFileBackend functions
* Various minor cleanups
Modified paths:
  • /branches/FileBackend/phase3/includes/filerepo/backend/FSFileBackend.php (modified) (history)
  • /branches/FileBackend/phase3/includes/filerepo/backend/FileBackend.php (modified) (history)
  • /branches/FileBackend/phase3/includes/filerepo/backend/FileBackendMultiWrite.php (modified) (history)
  • /branches/FileBackend/phase3/includes/filerepo/backend/FileLockManager.php (modified) (history)

Diff [purge]

Index: branches/FileBackend/phase3/includes/filerepo/backend/FileBackendMultiWrite.php
@@ -74,11 +74,9 @@
7575 */
7676 private function revertOperations( array $ops, $index = false ) {
7777 $status = Status::newGood();
78 - if ( $index === false ) {
79 - $pos = count( $ops ) - 1; // last element (or -1)
80 - } else {
81 - $pos = $index;
82 - }
 78+ $pos = ( $index !== false )
 79+ ? $index // use provided index
 80+ : $pos = count( $ops ) - 1; // last element (or -1)
8381 while ( $pos >= 0 ) {
8482 $tStatus = $ops[$pos]->revert();
8583 // merge $tStatus with $status
Index: branches/FileBackend/phase3/includes/filerepo/backend/FSFileBackend.php
@@ -11,6 +11,12 @@
1212 * child classes if the target store supports the operation.
1313 */
1414 class FSFileBackend extends FileBackend {
 15+ protected $fileMode;
 16+
 17+ function __construct( array $config ) {
 18+ $this->fileMode = isset( $config['fileMode'] ) ? $config['fileMode'] : 0644;
 19+ }
 20+
1521 public function store( array $params ) {
1622 $status = Status::newGood();
1723
@@ -18,41 +24,48 @@
1925
2026 if ( file_exists( $params['dest'] ) ) {
2127 if ( $params['overwriteDest'] ) {
22 - unlink( $params['dest'] );
23 - } elseif( $params['overwriteSame'] ) {
 28+ $ok = unlink( $params['dest'] );
 29+ if ( !$ok ) {
 30+ $status->fatal( "Could not delete destination file." );
 31+ return $status;
 32+ }
 33+ } elseif ( $params['overwriteSame'] ) {
2434 if ( // check size first since it's faster
2535 filesize( $params['dest'] ) != filesize( $params['source'] ) ||
2636 sha1_file( $params['dest'] ) != sha1_file( $params['source'] )
2737 ) {
28 - // error out
 38+ $status->fatal( "Non-identical destination file already exists." );
 39+ return $status;
2940 }
 41+ } else {
 42+ $status->fatal( "Destination file already exists." );
 43+ return $status;
3044 }
3145 }
 46+
3247 wfSuppressWarnings();
33 - copy( $params['source'], $params['dest'] );
 48+ $ok = copy( $params['source'], $params['dest'] );
3449 wfRestoreWarnings();
 50+ if ( !$ok ) {
 51+ $status->fatal( "Could not copy file to destination." );
 52+ }
3553
3654 return $status;
3755 }
3856
3957 public function copy( array $params ) {
40 - $status = Status::newGood();
41 -
42 - wfMakeDirParents( $params['dest'] );
43 -
44 - wfSuppressWarnings();
45 - copy( $params['source'], $params['dest'] );
46 - wfRestoreWarnings();
47 -
48 - return $status;
 58+ return $this->store( $params ); // both source and dest are on FS
4959 }
5060
5161 public function delete( array $params ) {
5262 $status = Status::newGood();
5363
5464 wfSuppressWarnings();
55 - unlink( $params['dest'] );
 65+ $ok = unlink( $params['dest'] );
5666 wfRestoreWarnings();
 67+ if ( !$ok ) {
 68+ $status->fatal( "Could not delete source file." );
 69+ }
5770
5871 return $status;
5972 }
@@ -60,18 +73,89 @@
6174 public function concatenate( array $params ) {
6275 $status = Status::newGood();
6376
64 - wfMakeDirParents( $params['dest'] );
 77+ // Check if the destination file exists and we can't handle that
 78+ $destExists = file_exists( $params['dest'] );
 79+ if ( $destExists && !$params['overwriteDest'] && !$params['overwriteSame'] ) {
 80+ $status->fatal( "Destination file already exists." ); // short-circuit
 81+ return $status;
 82+ }
6583
 84+ // Create a new temporary file...
6685 wfSuppressWarnings();
67 - $destHandle = fopen( $params['dest'], 'a' );
68 -
 86+ $tmpPath = tempnam( wfTempDir(), 'file_concatenate' );
6987 wfRestoreWarnings();
70 -
 88+ if ( $tmpPath === false ) {
 89+ $status->fatal( "Could not create temporary file $tmpPath." );
 90+ return $status;
 91+ }
 92+
 93+ // Build up that file using the source chunks (in order)...
 94+ wfSuppressWarnings();
 95+ $tmpHandle = fopen( $tmpPath, 'a' );
 96+ wfRestoreWarnings();
 97+ if ( $tmpHandle === false ) {
 98+ $status->fatal( "Could not open temporary file $tmpPath." );
 99+ return $status;
 100+ }
 101+ foreach ( $params['sources'] as $source ) {
 102+ // Load chunk into memory (it should be a small file)
 103+ $chunk = file_get_contents( $source );
 104+ if ( $chunk === false ) {
 105+ $status->fatal( "Could not read source file $source." );
 106+ return $status;
 107+ }
 108+ // Append chunk to file (pass chunk size to avoid magic quotes)
 109+ if ( !fwrite( $tmpHandle, $chunk, count( $chunk ) ) ) {
 110+ $status->fatal( "Could not write to temporary file $tmpPath." );
 111+ return $status;
 112+ }
 113+ }
 114+ wfSuppressWarnings();
 115+ if ( !fclose( $tmpHandle ) ) {
 116+ $status->fatal( "Could not close temporary file $tmpPath." );
 117+ return $status;
 118+ }
 119+ wfRestoreWarnings();
 120+
 121+ // Handle overwrite behavior of file destination if applicable.
 122+ // Note that we already checked if no overwrite params were set above.
 123+ if ( $destExists ) {
 124+ if ( $params['overwriteDest'] ) {
 125+ wfSuppressWarnings();
 126+ $ok = unlink( $params['dest'] );
 127+ wfRestoreWarnings();
 128+ if ( !$ok ) {
 129+ $status->fatal( "Could not delete destination file." );
 130+ return $status;
 131+ }
 132+ } elseif ( $params['overwriteSame'] ) {
 133+ if ( // check size first since it's faster
 134+ filesize( $params['dest'] ) != filesize( $params['source'] ) ||
 135+ sha1_file( $params['dest'] ) != sha1_file( $params['source'] )
 136+ ) {
 137+ $status->fatal( "Non-identical destination file already exists." );
 138+ return $status;
 139+ }
 140+ }
 141+ } else {
 142+ // Make sure destination directory exists
 143+ wfMakeDirParents( $params['dest'] );
 144+ }
 145+
 146+ // Rename the temporary file to the destination path
 147+ wfSuppressWarnings();
 148+ $ok = rename( $tmpPath, $params['dest'] );
 149+ wfRestoreWarnings();
 150+ if ( !$ok ) {
 151+ $status->fatal( "Could not rename temporary file to destination." );
 152+ return $status;
 153+ }
 154+
71155 return $status;
72156 }
73157
74158 public function fileExists( array $params ) {
75 -
 159+ return file_exists( $params['source'] );
76160 }
77161
78162 public function getFileProps( array $params ) {
@@ -79,13 +163,33 @@
80164 }
81165
82166 public function getLocalCopy( array $params ) {
83 -
84 - }
 167+ // Create a new temporary file...
 168+ wfSuppressWarnings();
 169+ $tmpPath = tempnam( wfTempDir(), 'file_localcopy' );
 170+ wfRestoreWarnings();
 171+ if ( $tmpPath === false ) {
 172+ return null;
 173+ }
85174
86 - public function lockFile( $source ) {
 175+ // Copy the source file over the temp file
 176+ wfSuppressWarnings();
 177+ $ok = copy( $params['source'], $tmpPath );
 178+ wfRestoreWarnings();
 179+ if ( !$ok ) {
 180+ return null;
 181+ }
 182+
 183+ return $tmpPath;
87184 }
88185
89 - public function unlockFile( $source ) {
90 -
 186+ /**
 187+ * Chmod a file, supressing the warnings.
 188+ * @param $path String: the path to change
 189+ * @return void
 190+ */
 191+ protected function chmod( $path ) {
 192+ wfSuppressWarnings();
 193+ chmod( $path, $this->fileMode );
 194+ wfRestoreWarnings();
91195 }
92196 }
Index: branches/FileBackend/phase3/includes/filerepo/backend/FileLockManager.php
@@ -3,33 +3,320 @@
44 * FileBackend helper class for handling file locking.
55 * Implemenations can rack of what locked in the process cache.
66 * This can reduce hits to external resources for lock()/unlock() calls.
 7+ *
 8+ * Subclasses should avoid throwing exceptions at all costs.
79 */
8 -interface IFileLockManager {
 10+abstract class FileLockManager {
911 /**
10 - * Lock the file at a storage path for a backend
 12+ * Construct a new instance from configuration
 13+ * @param $config Array
 14+ */
 15+ abstract public function __construct( array $config );
 16+
 17+ /**
 18+ * Lock the files at a storage paths for a backend
1119 *
1220 * @param $backendKey string
13 - * @param $name string
 21+ * @param $paths Array List of storage paths
1422 * @return Status
1523 */
16 - public function lock( $backendKey, $name );
 24+ final public function lock( $backendKey, array $paths ) {
 25+ $keys = array();
 26+ foreach ( $paths as $path ) {
 27+ $keys[] = $this->getLockName( $backendKey, $path );
 28+ }
 29+ return $this->doLock( $keys );
 30+ }
1731
1832 /**
19 - * Unlock the file at a storage path for a backend
 33+ * Unlock the files at a storage paths for a backend
2034 *
2135 * @param $backendKey string
22 - * @param $name string
 36+ * @param $paths Array List of storage paths
2337 * @return Status
2438 */
25 - public function unlock( $backendKey, $name );
 39+ final public function unlock( $backendKey, array $paths ) {
 40+ $keys = array();
 41+ foreach ( $paths as $path ) {
 42+ $keys[] = $this->getLockName( $backendKey, $path );
 43+ }
 44+ return $this->doUnlock( $keys );
 45+ }
 46+
 47+ /**
 48+ * Get the lock name given backend key (type/name) and a storage path
 49+ *
 50+ * @param $backendKey string
 51+ * @param $name string
 52+ * @return string
 53+ */
 54+ private function getLockName( $backendKey, $name ) {
 55+ return urlencode( $backendKey ) . ':' . md5( $name );
 56+ }
 57+
 58+ /**
 59+ * Lock a resource with the given key
 60+ *
 61+ * @param $key Array List of keys
 62+ * @return string
 63+ */
 64+ abstract protected function doLock( array $keys );
 65+
 66+ /**
 67+ * Unlock a resource with the given key
 68+ *
 69+ * @param $key Array List of keys
 70+ * @return string
 71+ */
 72+ abstract protected function doUnlock( array $keys );
2673 }
2774
28 -class NullFileLockManager implements IFileLockManager {
29 - public function lock( $backendKey, $name ) {
 75+/**
 76+ * Simple version of FileLockManager based on using FS lock files
 77+ */
 78+class FSFileLockManager extends FileLockManager {
 79+ protected $lockDir; // global dir for all servers
 80+ /** @var Array Map of lock key names to lock file handlers */
 81+ protected $handles = array();
 82+
 83+ function __construct( array $config ) {
 84+ $this->lockDir = $config['lockDir'];
 85+ }
 86+
 87+ function doLock( array $keys ) {
 88+ $status = Status::newGood();
 89+
 90+ $lockedKeys = array(); // files locked in this attempt
 91+ foreach ( $keys as $key ) {
 92+ $lockStatus = $this->doSingleLock( $key );
 93+ if ( $lockStatus->isOk() ) {
 94+ $lockedKeys[] = $key;
 95+ } else {
 96+ // Abort and unlock everything
 97+ $this->doUnlock( $lockedKeys );
 98+ return $lockStatus;
 99+ }
 100+ }
 101+
 102+ return $status;
 103+ }
 104+
 105+ function doUnlock( array $keys ) {
 106+ $status = Status::newGood();
 107+
 108+ foreach ( $keys as $key ) {
 109+ $lockStatus = $this->doSingleUnlock( $key );
 110+ if ( !$lockStatus->isOk() ) {
 111+ // append $lockStatus to $status
 112+ }
 113+ }
 114+
 115+ return $status;
 116+ }
 117+
 118+ protected function doSingleLock( $key ) {
 119+ $status = Status::newGood();
 120+
 121+ if ( isset( $this->handles[$key] ) ) {
 122+ $status->warning( 'File already locked.' );
 123+ } else {
 124+ wfSuppressWarnings();
 125+ $handle = fopen( "{$this->lockDir}/{$key}", 'w' );
 126+ if ( $handle ) {
 127+ if ( flock( $handle, LOCK_SH ) ) {
 128+ $this->handles[$key] = $handle;
 129+ } else {
 130+ fclose( $handle );
 131+ $status->fatal( "Could not file acquire lock." );
 132+ }
 133+ } else {
 134+ $status->fatal( "Could not open lock file." );
 135+ }
 136+ wfRestoreWarnings();
 137+ }
 138+
 139+ return $status;
 140+ }
 141+
 142+ protected function doSingleUnlock( $key ) {
 143+ $status = Status::newGood();
 144+
 145+ if ( isset( $this->handles[$key] ) ) {
 146+ wfSuppressWarnings();
 147+ if ( !flock( $this->handles[$key], LOCK_UN ) ) {
 148+ $status->fatal( "Could not unlock file." );
 149+ }
 150+ if ( !fclose( $this->handles[$key] ) ) {
 151+ $status->warning( "Could not close lock file." );
 152+ }
 153+ if ( !unlink( "{$this->lockDir}/{$key}" ) ) {
 154+ $status->warning( "Could not delete lock file." );
 155+ }
 156+ wfRestoreWarnings();
 157+ unset( $this->handles[$key] );
 158+ } else {
 159+ $status->warning( "There is no file lock to unlock." );
 160+ }
 161+
 162+ return $status;
 163+ }
 164+
 165+ function __destruct() {
 166+ // Make sure remaining files get cleared for sanity
 167+ foreach ( $this->handles as $key => $handler ) {
 168+ flock( $handler, LOCK_UN ); // PHP 5.3 will not do this automatically
 169+ fclose( $handler );
 170+ unlink( "{$this->lockDir}/{$key}" );
 171+ }
 172+ }
 173+}
 174+
 175+/**
 176+ * Version of FileLockManager based on using per-row DB locks
 177+ */
 178+class DBFileLockManager extends FileLockManager {
 179+ /** @var Array Map of bucket indexes to server names */
 180+ protected $serverMap = array(); // (index => server name)
 181+ protected $shards; // number of severs to shard to
 182+
 183+ /** @var Array List of active lock key names */
 184+ protected $locksHeld = array(); // (key => 1)
 185+ /** $var Array Map active database connections (name => Database) */
 186+ protected $activeConns = array();
 187+
 188+ /**
 189+ * Construct a new instance from configuration.
 190+ * The 'serverMap' param of $config has is an array of consecutive
 191+ * integer keys, starting from 0, with server name strings as values.
 192+ * It should have no more than 16 items in the array.
 193+ *
 194+ * The `file_locking` table could be a MEMORY or innoDB table.
 195+ *
 196+ * @param array $config
 197+ */
 198+ function __construct( array $config ) {
 199+ $this->serverMap = $config['serverMap'];
 200+ $this->shards = count( $this->serverMap );
 201+ }
 202+
 203+ function doLock( array $keys ) {
 204+ $status = Status::newGood();
 205+
 206+ $keysToLock = array();
 207+ // Get locks that need to be acquired...
 208+ foreach ( $keys as $key ) {
 209+ if ( isset( $this->locksHeld[$key] ) ) {
 210+ $status->warning( 'File already locked.' );
 211+ } else {
 212+ $server = $this->getDBServerFromKey( $key );
 213+ if ( $server === null ) {
 214+ $status->fatal( "Lock server for $key is not set." );
 215+ }
 216+ $keysToLock[$server][] = $key;
 217+ }
 218+ }
 219+
 220+ $lockedKeys = array(); // files locked in this attempt
 221+ // Attempt to acquire these locks...
 222+ try {
 223+ foreach ( $keysToLock as $server => $keys ) {
 224+ $db = $this->getDB( $server );
 225+ $db->select( 'file_locking',
 226+ '1',
 227+ array( 'fl_key' => $keys ),
 228+ __METHOD__,
 229+ array( 'FOR UPDATE' )
 230+ );
 231+ // Record locks as active
 232+ foreach ( $keys as $key ) {
 233+ $this->locksHeld[$key] = 1; // locked
 234+ }
 235+ // Keep track of what locks where made in this attempt
 236+ $lockedKeys = array_merge( $lockedKeys, $keys );
 237+ }
 238+ } catch ( DBConnectionError $e ) {
 239+ // Abort and unlock everything
 240+ $status->fatal( "Could not contact the lock database." );
 241+ $this->doUnlock( $lockedKeys );
 242+ }
 243+
 244+ return $status;
 245+ }
 246+
 247+ function doUnlock( array $keys ) {
 248+ $status = Status::newGood();
 249+
 250+ foreach ( $keys as $key ) {
 251+ if ( $this->locksHeld[$key] ) {
 252+ unset( $this->locksHeld[$key] );
 253+ // Reference count the locks held and COMMIT when zero
 254+ if ( !count( $this->locksHeld ) ) {
 255+ $this->commitOpenTransactions();
 256+ }
 257+ } else {
 258+ // append warning to $status
 259+ }
 260+ }
 261+
 262+ return $status;
 263+ }
 264+
 265+ /**
 266+ * Get a database connection for $server
 267+ * @param $server string
 268+ * @return Database
 269+ */
 270+ protected function getDB( $server ) {
 271+ if ( !isset( $this->activeConns[$server] ) ) {
 272+ $this->activeConns[$server] = wfGetDB( DB_MASTER, array(), $server );
 273+ $this->activeConns[$server]->begin(); // start transaction
 274+ }
 275+ return $this->activeConns[$server];
 276+ }
 277+
 278+ /**
 279+ * Commit all changes to active databases
 280+ * @return void
 281+ */
 282+ protected function commitOpenTransactions() {
 283+ try {
 284+ foreach ( $this->activeConns as $server => $db ) {
 285+ $db->commit(); // finish transaction
 286+ unset( $this->activeConns[$server] );
 287+ }
 288+ } catch ( DBConnectionError $e ) {
 289+ // oh well
 290+ }
 291+ }
 292+
 293+ /**
 294+ * Get the server name for lock key
 295+ * @param $key string
 296+ * @return string|null
 297+ */
 298+ protected function getDBServerFromKey( $key ) {
 299+ $hash = str_pad( md5( $key ), 32, '0', STR_PAD_LEFT ); // 32 char hash
 300+ $prefix = substr( $hash, 0, 2 ); // first 2 hex chars (8 bits)
 301+ $bucket = intval( base_convert( $prefix, 16, 10 ) ) % $this->shards;
 302+
 303+ if ( isset( $this->serverMap[$bucket] ) ) {
 304+ return $this->serverMap[$bucket];
 305+ } else {
 306+ wfWarn( "No key for bucket $bucket in serverMap." );
 307+ return null; // disabled? bad config?
 308+ }
 309+ }
 310+}
 311+
 312+/**
 313+ * Simple version of FileLockManager that does nothing
 314+ */
 315+class NullFileLockManager extends FileLockManager {
 316+ public function doLock( array $keys ) {
30317 return Status::newGood();
31318 }
32319
33 - public function unlock( $backendKey, $name ) {
 320+ public function doUnlock( array $keys ) {
34321 return Status::newGood();
35322 }
36323 }
Index: branches/FileBackend/phase3/includes/filerepo/backend/FileBackend.php
@@ -142,22 +142,22 @@
143143 public function getLocalCopy( array $params );
144144
145145 /**
146 - * Lock the file at a storage path in the backend.
 146+ * Lock the files at the given storage paths in the backend.
147147 * Do not call these from places other than FileOp.
148148 *
149 - * @param $source string Source storage path
 149+ * @param $sources Array Source storage paths
150150 * @return Status
151151 */
152 - public function lockFile( $source );
 152+ public function lockFiles( array $sources );
153153
154154 /**
155 - * Unlock the file at a storage path in the backend.
 155+ * Unlock the files at the given storage paths in the backend.
156156 * Do not call these from places other than FileOp.
157157 *
158 - * @param $source string Source storage path
 158+ * @param $sources Array Source storage paths
159159 * @return Status
160160 */
161 - public function unlockFile( $source );
 161+ public function unlockFiles( array $sources );
162162 }
163163
164164 /**
@@ -169,7 +169,7 @@
170170 /** @var FileLockManager */
171171 protected $lockManager;
172172
173 - public function getName() {
 173+ final public function getName() {
174174 return $this->name;
175175 }
176176
@@ -255,11 +255,9 @@
256256 */
257257 private function revertOperations( array $ops, $index = false ) {
258258 $status = Status::newGood();
259 - if ( $index === false ) {
260 - $pos = count( $ops ) - 1; // last element (or -1)
261 - } else {
262 - $pos = $index;
263 - }
 259+ $pos = ( $index !== false )
 260+ ? $index // use provided index
 261+ : $pos = count( $ops ) - 1; // last element (or -1)
264262 while ( $pos >= 0 ) {
265263 $tStatus = $ops[$pos]->revert();
266264 // merge $tStatus with $status
@@ -268,16 +266,16 @@
269267 return $status;
270268 }
271269
272 - final public function lockFile( array $path ) {
 270+ final public function lockFiles( array $paths ) {
273271 // Locks should be specific to this backend location
274272 $backendKey = get_class( $this ) . '-' . $this->getName();
275 - return $this->lockManager->lockFile( $backendKey, $path ); // not supported
 273+ return $this->lockManager->lock( $backendKey, $paths ); // not supported
276274 }
277275
278 - final public function unlockFile( array $path ) {
 276+ final public function unlockFiles( array $paths ) {
279277 // Locks should be specific to this backend location
280278 $backendKey = get_class( $this ) . '-' . $this->getName();
281 - return $this->lockManager->unlockFile( $backendKey, $path ); // not supported
 279+ return $this->lockManager->unlock( $backendKey, $paths ); // not supported
282280 }
283281 }
284282
@@ -375,6 +373,9 @@
376374 */
377375 private function setLocks() {
378376 $status = Status::newGood();
 377+ return $this->backend->lockFiles( $this->storagePathsToLock() );
 378+
 379+
379380 $lockedFiles = array(); // files actually locked
380381 foreach ( $this->storagePathsToLock() as $file ) {
381382 $lockStatus = $this->backend->lockFile( $file );
@@ -396,6 +397,8 @@
397398 */
398399 private function unsetLocks() {
399400 $status = Status::newGood();
 401+ return $this->backend->unlockFiles( $this->storagePathsToLock() );
 402+
400403 foreach ( $this->storagePathsToLock() as $file ) {
401404 $lockStatus = $this->backend->unlockFile( $file );
402405 if ( !$lockStatus->isOk() ) {
@@ -553,7 +556,7 @@
554557 /**
555558 * Combines files from severals storage paths into a new file in the backend.
556559 * $params include:
557 - * source : source storage path
 560+ * sources : ordered source storage paths (e.g. chunk1,chunk2,...)
558561 * dest : destination storage path
559562 * overwriteDest : do nothing and pass if an identical file exists at destination
560563 * overwriteSame : override any existing file at destination

Status & tagging log