https://www.mediawiki.org/wiki/Special:Code/MediaWiki/103877
Revision: 103877 Author: aaron Date: 2011-11-22 00:32:44 +0000 (Tue, 22 Nov 2011) Log Message: ----------- FileLockManager: * Simplified scheme for resource names used by locks * Added LOCK_SH and LOCK_EX options * Fixed FSFileLockManager::doLock() rollback logic * Use Status objects correctly Modified Paths: -------------- branches/FileBackend/phase3/includes/filerepo/backend/FSFileBackend.php branches/FileBackend/phase3/includes/filerepo/backend/FileBackend.php branches/FileBackend/phase3/includes/filerepo/backend/FileLockManager.php Modified: branches/FileBackend/phase3/includes/filerepo/backend/FSFileBackend.php =================================================================== --- branches/FileBackend/phase3/includes/filerepo/backend/FSFileBackend.php 2011-11-22 00:26:50 UTC (rev 103876) +++ branches/FileBackend/phase3/includes/filerepo/backend/FSFileBackend.php 2011-11-22 00:32:44 UTC (rev 103877) @@ -34,7 +34,7 @@ list( $c, $dest ) = $this->resolveVirtualPath( $params['dest'] ); if ( $dest === null ) { - $status->fatal( "Invalid storage path {$params['dest']}." ); + $status->fatal( 'backend-fail-invalidpath', $params['dest'] ); return $status; } @@ -42,16 +42,16 @@ if ( isset( $params['overwriteDest'] ) ) { $ok = unlink( $dest ); if ( !$ok ) { - $status->fatal( "Could not delete destination file." ); + $status->fatal( 'backend-fail-delete', $param['dest'] ); return $status; } } elseif ( isset( $params['overwriteSame'] ) ) { if ( !$this->filesAreSame( $params['source'], $dest ) ) { - $status->fatal( "Non-identical destination file already exists." ); + $status->fatal( 'backend-fail-notsame', $params['source'], $params['dest'] ); } return $status; // do nothing; either OK or bad status } else { - $status->fatal( "Destination file already exists." ); + $status->fatal( 'backend-fail-alreadyexists', $params['dest'] ); return $status; } } else { @@ -62,7 +62,7 @@ $ok = copy( $params['source'], $dest ); wfRestoreWarnings(); if ( !$ok ) { - $status->fatal( "Could not copy file to destination." ); + $status->fatal( 'backend-fail-copy', $params['source'], $params['dest'] ); return $status; } @@ -80,12 +80,12 @@ list( $c, $source ) = $this->resolveVirtualPath( $params['source'] ); if ( $source === null ) { - $status->fatal( "Invalid storage path {$params['source']}." ); + $status->fatal( 'backend-fail-invalidpath', $params['source'] ); return $status; } list( $c, $dest ) = $this->resolveVirtualPath( $params['dest'] ); if ( $dest === null ) { - $status->fatal( "Invalid storage path {$params['dest']}." ); + $status->fatal( 'backend-fail-invalidpath', $params['dest'] ); return $status; } @@ -93,16 +93,16 @@ if ( isset( $params['overwriteDest'] ) ) { $ok = unlink( $dest ); if ( !$ok ) { - $status->fatal( "Could not delete destination file." ); + $status->fatal( 'backend-fail-delete', $params['dest'] ); return $status; } } elseif ( isset( $params['overwriteSame'] ) ) { if ( !$this->filesAreSame( $source, $dest ) ) { - $status->fatal( "Non-identical destination file already exists." ); + $status->fatal( 'backend-fail-notsame', $params['source'], $params['dest'] ); } return $status; // do nothing; either OK or bad status } else { - $status->fatal( "Destination file already exists." ); + $status->fatal( 'backend-fail-alreadyexists', $params['dest'] ); return $status; } } else { @@ -113,7 +113,7 @@ $ok = rename( $source, $dest ); wfRestoreWarnings(); if ( !$ok ) { - $status->fatal( "Could not move file to destination." ); + $status->fatal( 'backend-fail-move', $params['source'], $params['dest'] ); return $status; } @@ -125,13 +125,13 @@ list( $c, $source ) = $this->resolveVirtualPath( $params['source'] ); if ( $source === null ) { - $status->fatal( "Invalid storage path {$params['source']}." ); + $status->fatal( 'backend-fail-invalidpath', $params['source'] ); return $status; } if ( !file_exists( $source ) ) { if ( !$params['ignoreMissingSource'] ) { - $status->fatal( "Could not delete source because it does not exist." ); + $status->fatal( 'backend-fail-delete', $params['source'] ); } return $status; // do nothing; either OK or bad status } @@ -140,7 +140,7 @@ $ok = unlink( $source ); wfRestoreWarnings(); if ( !$ok ) { - $status->fatal( "Could not delete source file." ); + $status->fatal( 'backend-fail-delete', $params['source'] ); return $status; } @@ -152,14 +152,14 @@ list( $c, $dest ) = $this->resolveVirtualPath( $params['dest'] ); if ( $dest === null ) { - $status->fatal( "Invalid storage path {$params['dest']}." ); + $status->fatal( 'backend-fail-invalidpath', $params['dest'] ); return $status; } // Check if the destination file exists and we can't handle that $destExists = file_exists( $dest ); if ( $destExists && !$params['overwriteDest'] && !$params['overwriteSame'] ) { - $status->fatal( "Destination file already exists." ); // short-circuit + $status->fatal( 'backend-fail-alreadyexists', $params['dest'] ); return $status; } @@ -168,7 +168,7 @@ $tmpPath = tempnam( wfTempDir(), 'file_concatenate' ); wfRestoreWarnings(); if ( $tmpPath === false ) { - $status->fatal( "Could not create temporary file $tmpPath." ); + $status->fatal( 'backend-fail-createtemp' ); return $status; } @@ -177,30 +177,30 @@ $tmpHandle = fopen( $tmpPath, 'a' ); wfRestoreWarnings(); if ( $tmpHandle === false ) { - $status->fatal( "Could not open temporary file $tmpPath." ); + $status->fatal( 'backend-fail-opentemp', $tmpPath ); return $status; } foreach ( $params['sources'] as $virtualSource ) { list( $c, $source ) = $this->resolveVirtualPath( $virtualSource ); if ( $source === null ) { - $status->fatal( "Invalid storage path {$virtualSource}." ); + $status->fatal( 'backend-fail-invalidpath', $virtualSource ); return $status; } // Load chunk into memory (it should be a small file) $chunk = file_get_contents( $source ); if ( $chunk === false ) { - $status->fatal( "Could not read source file $source." ); + $status->fatal( 'backend-fail-read', $virtualSource ); return $status; } // Append chunk to file (pass chunk size to avoid magic quotes) if ( !fwrite( $tmpHandle, $chunk, count( $chunk ) ) ) { - $status->fatal( "Could not write to temporary file $tmpPath." ); + $status->fatal( 'backend-fail-writetemp', $tmpPath ); return $status; } } wfSuppressWarnings(); if ( !fclose( $tmpHandle ) ) { - $status->fatal( "Could not close temporary file $tmpPath." ); + $status->fatal( 'backend-fail-closetemp', $tmpPath ); return $status; } wfRestoreWarnings(); @@ -213,12 +213,12 @@ $ok = unlink( $dest ); wfRestoreWarnings(); if ( !$ok ) { - $status->fatal( "Could not delete destination file." ); + $status->fatal( 'backend-fail-delete', $params['dest'] ); return $status; } } elseif ( isset( $params['overwriteSame'] ) ) { if ( !$this->filesAreSame( $tmpPath, $dest ) ) { - $status->fatal( "Non-identical destination file already exists." ); + $status->fatal( 'backend-fail-notsame', $tmpPath, $params['dest'] ); } return $status; // do nothing; either OK or bad status } @@ -232,7 +232,7 @@ $ok = rename( $tmpPath, $dest ); wfRestoreWarnings(); if ( !$ok ) { - $status->fatal( "Could not rename temporary file to destination." ); + $status->fatal( 'backend-fail-move', $tmpPath, $params['dest'] ); return $status; } @@ -271,13 +271,13 @@ list( $c, $source ) = $this->resolveVirtualPath( $params['source'] ); if ( $source === null ) { - $status->fatal( "Invalid storage path {$params['source']}." ); + $status->fatal( 'backend-fail-invalidpath', $params['source'] ); return $status; } $ok = StreamFile::stream( $source, array(), false ); if ( !$ok ) { - $status->fatal( "Unable to stream file {$source}." ); + $status->fatal( 'backend-fail-stream', $params['source'] ); return $status; } Modified: branches/FileBackend/phase3/includes/filerepo/backend/FileBackend.php =================================================================== --- branches/FileBackend/phase3/includes/filerepo/backend/FileBackend.php 2011-11-22 00:26:50 UTC (rev 103876) +++ branches/FileBackend/phase3/includes/filerepo/backend/FileBackend.php 2011-11-22 00:32:44 UTC (rev 103877) @@ -334,18 +334,27 @@ } final public function lockFiles( array $paths ) { - // Locks should be specific to this backend location - $backendKey = get_class( $this ) . '-' . $this->getName(); - return $this->lockManager->lock( $backendKey, $paths ); // not supported + return $this->lockManager->lock( $this->getLockResourcePaths( $paths ) ); } final public function unlockFiles( array $paths ) { - // Locks should be specific to this backend location - $backendKey = get_class( $this ) . '-' . $this->getName(); - return $this->lockManager->unlock( $backendKey, $paths ); // not supported + return $this->lockManager->unlock( $this->getLockResourcePaths( $paths ) ); } /** + * Get a prefix to use for locking keys + * @return string + */ + private function getLockResourcePaths( array $paths ) { + $backendKey = get_class( $this ) . ':' . $this->getName(); + $res = array(); + foreach( $paths as $path ) { + $res[] = "{$backendKey}:{$path}"; + } + return $res; + } + + /** * Split a storage path (e.g. "mwstore://container/path/to/object") * into a container name and a full object name within that container. * @@ -554,7 +563,7 @@ // Create a temporary backup copy... $this->tmpDestFile = $this->getLocalCopy( $this->params['dest'] ); if ( !$this->tmpDestFile ) { - $status->fatal( "Could not backup destination file." ); + $status->fatal( 'backend-fail-restore', $this->params['dest'] ); return $status; } } @@ -750,7 +759,7 @@ $status = Status::newGood(); if ( !$this->params['ignoreMissingSource'] ) { if ( !$this->backend->fileExists( $this->params['source'] ) ) { - $status->fatal( "Cannot delete file because it does not exist." ); + $status->fatal( 'backend-fail-notexists', $this->params['source'] ); return $status; } } Modified: branches/FileBackend/phase3/includes/filerepo/backend/FileLockManager.php =================================================================== --- branches/FileBackend/phase3/includes/filerepo/backend/FileLockManager.php 2011-11-22 00:26:50 UTC (rev 103876) +++ branches/FileBackend/phase3/includes/filerepo/backend/FileLockManager.php 2011-11-22 00:32:44 UTC (rev 103877) @@ -1,74 +1,66 @@ <?php /** * FileBackend helper class for handling file locking. - * Implemenations can rack of what locked in the process cache. + * Locks on resource keys can either be shared or exclusive. + * + * Implemenations can keep track of what is locked in the process cache. * This can reduce hits to external resources for lock()/unlock() calls. * * Subclasses should avoid throwing exceptions at all costs. */ abstract class FileLockManager { + /* Lock types; stronger locks have high values */ + const LOCK_SH = 1; // shared lock (for reads) + const LOCK_EX = 2; // exclusive lock (for writes) + /** * Construct a new instance from configuration + * * @param $config Array */ - abstract public function __construct( array $config ); + public function __construct( array $config ) {} /** - * Lock the files at a storage paths for a backend + * Lock the resources at the given abstract paths * - * @param $backendKey string - * @param $paths Array List of storage paths + * @param $paths Array List of resource names + * @param $type integer FileLockManager::LOCK_EX, FileLockManager::LOCK_SH * @return Status */ - final public function lock( $backendKey, array $paths ) { - $keys = array(); - foreach ( $paths as $path ) { - $keys[] = $this->getLockName( $backendKey, $path ); - } - return $this->doLock( $keys ); + final public function lock( array $paths, $type = self::LOCK_EX ) { + $keys = array_map( 'sha1', $paths ); + return $this->doLock( $keys, $type ); } /** - * Unlock the files at a storage paths for a backend + * Unlock the resources at the given abstract paths * - * @param $backendKey string * @param $paths Array List of storage paths * @return Status */ - final public function unlock( $backendKey, array $paths ) { - $keys = array(); - foreach ( $paths as $path ) { - $keys[] = $this->getLockName( $backendKey, $path ); - } - return $this->doUnlock( $keys ); + final public function unlock( array $paths ) { + $keys = array_map( 'sha1', $paths ); + return $this->doUnlock( $keys, 0 ); } /** - * Get the lock name given backend key (type/name) and a storage path - * - * @param $backendKey string - * @param $name string - * @return string - */ - private function getLockName( $backendKey, $name ) { - return urlencode( $backendKey ) . ':' . md5( $name ); - } - - /** * Lock a resource with the given key * - * @param $key Array List of keys + * @param $key Array List of keys to lock + * @param $type integer FileLockManager::LOCK_EX, FileLockManager::LOCK_SH * @return string */ - abstract protected function doLock( array $keys ); + abstract protected function doLock( array $keys, $type ); /** - * Unlock a resource with the given key + * Unlock a resource with the given key. + * If $type is given, then only locks of that type should be cleared. * - * @param $key Array List of keys + * @param $key Array List of keys to unlock + * @param $type integer FileLockManager::LOCK_EX, FileLockManager::LOCK_SH, or 0 * @return string */ - abstract protected function doUnlock( array $keys ); + abstract protected function doUnlock( array $keys, $type ); } /** @@ -79,24 +71,29 @@ */ class FSFileLockManager extends FileLockManager { protected $lockDir; // global dir for all servers - /** @var Array Map of lock key names to lock file handlers */ + /** @var Array Map of (locked key => lock type => lock file handle) */ protected $handles = array(); function __construct( array $config ) { $this->lockDir = $config['lockDir']; } - function doLock( array $keys ) { + function doLock( array $keys, $type ) { $status = Status::newGood(); $lockedKeys = array(); // files locked in this attempt foreach ( $keys as $key ) { - $status->merge( $this->doSingleLock( $key ) ); - if ( $status->isOk() ) { - $lockedKeys[] = $key; + $subStatus = $this->doSingleLock( $key, $type ); + $status->merge( $subStatus ); + if ( $status->isOK() ) { + // Don't append to $lockedKeys if $key is already locked. + // We do NOT want to unlock the key if we have to rollback. + if ( $subStatus->isGood() ) { // no warnings/fatals? + $lockedKeys[] = $key; + } } else { // Abort and unlock everything - $status->merge( $this->doUnlock( $lockedKeys ) ); + $status->merge( $this->doUnlock( $lockedKeys, $type ) ); return $status; } } @@ -104,33 +101,44 @@ return $status; } - function doUnlock( array $keys ) { + function doUnlock( array $keys, $type ) { $status = Status::newGood(); foreach ( $keys as $key ) { - $status->merge( $this->doSingleUnlock( $key ) ); + $status->merge( $this->doSingleUnlock( $key, $type ) ); } return $status; } - protected function doSingleLock( $key ) { + /** + * Lock a single resource key + * + * @param $key string + * @param $type integer + * @return Status + */ + protected function doSingleLock( $key, $type ) { $status = Status::newGood(); - if ( isset( $this->handles[$key] ) ) { - $status->warning( 'File already locked.' ); + if ( isset( $this->handles[$key][$type] ) ) { + $status->warning( 'lockmanager-alreadylocked', $key ); + } elseif ( isset( $this->handles[$key][self::LOCK_EX] ) ) { + $status->warning( 'lockmanager-alreadylocked', $key ); } else { wfSuppressWarnings(); - $handle = fopen( "{$this->lockDir}/{$key}", 'w' ); + $handle = fopen( "{$this->lockDir}/{$key}", 'c' ); if ( $handle ) { - if ( flock( $handle, LOCK_SH ) ) { - $this->handles[$key] = $handle; + // Either a shared or exclusive lock + $lock = ( $type == self::LOCK_SH ) ? LOCK_SH : LOCK_EX; + if ( flock( $handle, $lock ) ) { + $this->handles[$key][$type] = $handle; } else { fclose( $handle ); - $status->fatal( "Could not file acquire lock." ); + $status->fatal( 'lockmanager-fail-acquirelock', $key ); } } else { - $status->fatal( "Could not open lock file." ); + $status->fatal( 'lockmanager-fail-openlock', $key ); } wfRestoreWarnings(); } @@ -138,24 +146,43 @@ return $status; } - protected function doSingleUnlock( $key ) { + /** + * Unlock a single resource key + * + * @param $key string + * @param $type integer + * @return Status + */ + protected function doSingleUnlock( $key, $type ) { $status = Status::newGood(); - if ( isset( $this->handles[$key] ) ) { - wfSuppressWarnings(); - if ( !flock( $this->handles[$key], LOCK_UN ) ) { - $status->fatal( "Could not unlock file." ); + if ( !isset( $this->handles[$key] ) ) { + $status->warning( 'lockmanager-notlocked', $key ); + } elseif ( $type && !isset( $this->handles[$key][$type] ) ) { + $status->warning( 'lockmanager-notlocked', $key ); + } else { + foreach ( $this->handles[$key] as $lockType => $handle ) { + if ( $type && $lockType != $type ) { + continue; // only unlock locks of type $type + } + wfSuppressWarnings(); + if ( !flock( $this->handles[$key][$lockType], LOCK_UN ) ) { + $status->fatal( 'lockmanager-fail-releaselock', $key ); + } + if ( !fclose( $this->handles[$key][$lockType] ) ) { + $status->warning( 'lockmanager-fail-closelock', $key ); + } + wfRestoreWarnings(); + unset( $this->handles[$key][$lockType] ); } - if ( !fclose( $this->handles[$key] ) ) { - $status->warning( "Could not close lock file." ); + if ( !count( $this->handles[$key] ) ) { + wfSuppressWarnings(); + # No locks are held for the lock file anymore + if ( !unlink( "{$this->lockDir}/{$key}" ) ) { + $status->warning( 'lockmanager-fail-deletelock', $key ); + } + wfRestoreWarnings(); } - if ( !unlink( "{$this->lockDir}/{$key}" ) ) { - $status->warning( "Could not delete lock file." ); - } - wfRestoreWarnings(); - unset( $this->handles[$key] ); - } else { - $status->warning( "There is no file lock to unlock." ); } return $status; @@ -163,9 +190,11 @@ protected function __destruct() { // Make sure remaining files get cleared for sanity - foreach ( $this->handles as $key => $handler ) { - flock( $handler, LOCK_UN ); // PHP 5.3 will not do this automatically - fclose( $handler ); + foreach ( $this->handles as $key => $locks ) { + foreach ( $locks as $type => $handle ) { + flock( $handle, LOCK_UN ); // PHP 5.3 will not do this automatically + fclose( $handle ); + } unlink( "{$this->lockDir}/{$key}" ); } } @@ -191,8 +220,8 @@ class DBFileLockManager extends FileLockManager { /** @var Array Map of bucket indexes to server names */ protected $serverMap = array(); // (index => (server1,server2,...)) - /** @var Array List of active lock key names */ - protected $locksHeld = array(); // (key => 1) + /** @var Array Map of (locked key => lock type => 1) */ + protected $locksHeld = array(); /** $var Array Map Lock-active database connections (name => Database) */ protected $activeConns = array(); @@ -221,18 +250,20 @@ } } - function doLock( array $keys ) { + function doLock( array $keys, $type ) { $status = Status::newGood(); $keysToLock = array(); // Get locks that need to be acquired (buckets => locks)... foreach ( $keys as $key ) { - if ( isset( $this->locksHeld[$key] ) ) { - $status->warning( 'File already locked.' ); + if ( isset( $this->locksHeld[$key][$type] ) ) { + $status->warning( 'lockmanager-alreadylocked', $key ); + } elseif ( isset( $this->locksHeld[$key][self::LOCK_EX] ) ) { + $status->warning( 'lockmanager-alreadylocked', $key ); } else { $bucket = $this->getBucketFromKey( $key ); if ( $bucket === null ) { // config error? - $status->fatal( "Lock servers for key $key is not set." ); + $status->fatal( 'lockmanager-fail-config', $key ); return $status; } $keysToLock[$bucket][] = $key; @@ -249,25 +280,26 @@ // (b) Server is down but a fallback is up // (c) Server is down and no fallbacks are up (or none defined) try { - $this->lockingSelect( $server, $keys ); // SELECT FOR UPDATE + $this->lockingSelect( $server, $keys, $type ); } catch ( DBError $e ) { // Can we manage to lock on any of the fallback servers? - if ( !$this->lockingSelectFallbacks( $bucket, $keys ) ) { + if ( $this->lockingSelectFallbacks( $bucket, $keys, $type ) ) { + // Recovered; a fallback server is up + $propagateToFallbacks = false; // done already + } else { // Abort and unlock everything we just locked - $status->fatal( "Could not contact the lock server." ); - $status->merge( $this->doUnlock( $lockedKeys ) ); + $status->fatal( 'lockmanager-fail-db', $bucket ); + $status->merge( $this->doUnlock( $lockedKeys, $type ) ); return $status; - } else { // recovered using fallbacks - $propagateToFallbacks = false; // done already } } // Propagate any locks to the fallback servers (best effort) if ( $propagateToFallbacks ) { - $this->lockingSelectFallbacks( $bucket, $keys ); + $this->lockingSelectFallbacks( $bucket, $keys, $type ); } // Record locks as active foreach ( $keys as $key ) { - $this->locksHeld[$key] = 1; // locked + $this->locksHeld[$key][$type] = 1; // locked } // Keep track of what locks were made in this attempt $lockedKeys = array_merge( $lockedKeys, $keys ); @@ -276,21 +308,29 @@ return $status; } - function doUnlock( array $keys ) { + function doUnlock( array $keys, $type ) { $status = Status::newGood(); foreach ( $keys as $key ) { - if ( $this->locksHeld[$key] ) { - unset( $this->locksHeld[$key] ); - // Reference count the locks held and COMMIT when zero - if ( !count( $this->locksHeld ) ) { - $this->commitLockTransactions(); + if ( !isset( $this->locksHeld[$key] ) ) { + $status->warning( 'lockmanager-notlocked', $key ); + } elseif ( $type && !isset( $this->locksHeld[$key][$type] ) ) { + $status->warning( 'lockmanager-notlocked', $key ); + } else { + foreach ( $this->locksHeld[$key] as $lockType => $x ) { + if ( $type && $lockType != $type ) { + continue; // only unlock locks of type $type + } + unset( $this->locksHeld[$key][$lockType] ); } - } else { - $status->warning( "There is no file lock to unlock." ); } } + // Reference count the locks held and COMMIT when zero + if ( !count( $this->locksHeld ) ) { + $this->commitLockTransactions(); + } + return $status; } @@ -299,19 +339,23 @@ * * @param $server string * @param $keys Array + * @param $type integer FileLockManager::LOCK_EX or FileLockManager::LOCK_SH * @return void */ - protected function lockingSelect( $server, array $keys ) { + protected function lockingSelect( $server, array $keys, $type ) { if ( !isset( $this->activeConns[$server] ) ) { $this->activeConns[$server] = wfGetDB( DB_MASTER, array(), $server ); $this->activeConns[$server]->begin(); // start transaction } + $lockingClause = ( $type == self::LOCK_SH ) + ? 'LOCK IN SHARE MODE' // reader lock + : 'FOR UPDATE'; // writer lock $this->activeConns[$server]->select( 'file_locking', '1', array( 'fl_key' => $keys ), __METHOD__, - array( 'FOR UPDATE' ) + array( $lockingClause ) ); } @@ -321,15 +365,16 @@ * * @param $bucket integer * @param $keys Array + * @param $type integer FileLockManager::LOCK_EX or FileLockManager::LOCK_SH * @return bool Locks made on at least one fallback server */ - protected function lockingSelectFallbacks( $bucket, array $keys ) { + protected function lockingSelectFallbacks( $bucket, array $keys, $type ) { $locksMade = false; - $count = count( $this->serverMap[$bucket] ); - for ( $i=1; $i < $count; $i++ ) { // start at 1 to only include fallbacks + // Start at $i=1 to only include fallback servers + for ( $i=1; $i < count( $this->serverMap[$bucket] ); $i++ ) { $server = $this->serverMap[$bucket][$i]; try { - $this->doLockingSelect( $server, $keys ); // SELECT FOR UPDATE + $this->doLockingSelect( $server, $keys, $type ); $locksMade = true; // success for this fallback } catch ( DBError $e ) { // oh well; best effort (@TODO: logging?) @@ -380,11 +425,11 @@ class NullFileLockManager extends FileLockManager { function __construct( array $config ) {} - function doLock( array $keys ) { + function doLock( array $keys, $type ) { return Status::newGood(); } - function doUnlock( array $keys ) { + function doUnlock( array $keys, $type ) { return Status::newGood(); } } _______________________________________________ MediaWiki-CVS mailing list MediaWiki-CVS@lists.wikimedia.org https://lists.wikimedia.org/mailman/listinfo/mediawiki-cvs