diff options
author | Yaco <franco@reevo.org> | 2020-06-04 11:01:00 -0300 |
---|---|---|
committer | Yaco <franco@reevo.org> | 2020-06-04 11:01:00 -0300 |
commit | fc7369835258467bf97eb64f184b93691f9a9fd5 (patch) | |
tree | daabd60089d2dd76d9f5fb416b005fbe159c799d /www/wiki/includes/filebackend |
first commit
Diffstat (limited to 'www/wiki/includes/filebackend')
-rw-r--r-- | www/wiki/includes/filebackend/FileBackendGroup.php | 247 | ||||
-rw-r--r-- | www/wiki/includes/filebackend/README | 208 | ||||
-rw-r--r-- | www/wiki/includes/filebackend/filejournal/DBFileJournal.php | 193 | ||||
-rw-r--r-- | www/wiki/includes/filebackend/lockmanager/LockManagerGroup.php | 176 | ||||
-rw-r--r-- | www/wiki/includes/filebackend/lockmanager/MySqlLockManager.php | 141 |
5 files changed, 965 insertions, 0 deletions
diff --git a/www/wiki/includes/filebackend/FileBackendGroup.php b/www/wiki/includes/filebackend/FileBackendGroup.php new file mode 100644 index 00000000..454b6332 --- /dev/null +++ b/www/wiki/includes/filebackend/FileBackendGroup.php @@ -0,0 +1,247 @@ +<?php +/** + * File backend registration handling. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @ingroup FileBackend + */ + +use MediaWiki\Logger\LoggerFactory; +use MediaWiki\MediaWikiServices; + +/** + * Class to handle file backend registration + * + * @ingroup FileBackend + * @since 1.19 + */ +class FileBackendGroup { + /** @var FileBackendGroup */ + protected static $instance = null; + + /** @var array (name => ('class' => string, 'config' => array, 'instance' => object)) */ + protected $backends = []; + + protected function __construct() { + } + + /** + * @return FileBackendGroup + */ + public static function singleton() { + if ( self::$instance == null ) { + self::$instance = new self(); + self::$instance->initFromGlobals(); + } + + return self::$instance; + } + + /** + * Destroy the singleton instance + */ + public static function destroySingleton() { + self::$instance = null; + } + + /** + * Register file backends from the global variables + */ + protected function initFromGlobals() { + global $wgLocalFileRepo, $wgForeignFileRepos, $wgFileBackends, $wgDirectoryMode; + + // Register explicitly defined backends + $this->register( $wgFileBackends, wfConfiguredReadOnlyReason() ); + + $autoBackends = []; + // Automatically create b/c backends for file repos... + $repos = array_merge( $wgForeignFileRepos, [ $wgLocalFileRepo ] ); + foreach ( $repos as $info ) { + $backendName = $info['backend']; + if ( is_object( $backendName ) || isset( $this->backends[$backendName] ) ) { + continue; // already defined (or set to the object for some reason) + } + $repoName = $info['name']; + // Local vars that used to be FSRepo members... + $directory = $info['directory']; + $deletedDir = isset( $info['deletedDir'] ) + ? $info['deletedDir'] + : false; // deletion disabled + $thumbDir = isset( $info['thumbDir'] ) + ? $info['thumbDir'] + : "{$directory}/thumb"; + $transcodedDir = isset( $info['transcodedDir'] ) + ? $info['transcodedDir'] + : "{$directory}/transcoded"; + // Get the FS backend configuration + $autoBackends[] = [ + 'name' => $backendName, + 'class' => FSFileBackend::class, + 'lockManager' => 'fsLockManager', + 'containerPaths' => [ + "{$repoName}-public" => "{$directory}", + "{$repoName}-thumb" => $thumbDir, + "{$repoName}-transcoded" => $transcodedDir, + "{$repoName}-deleted" => $deletedDir, + "{$repoName}-temp" => "{$directory}/temp" + ], + 'fileMode' => isset( $info['fileMode'] ) ? $info['fileMode'] : 0644, + 'directoryMode' => $wgDirectoryMode, + ]; + } + + // Register implicitly defined backends + $this->register( $autoBackends, wfConfiguredReadOnlyReason() ); + } + + /** + * Register an array of file backend configurations + * + * @param array[] $configs + * @param string|null $readOnlyReason + * @throws InvalidArgumentException + */ + protected function register( array $configs, $readOnlyReason = null ) { + foreach ( $configs as $config ) { + if ( !isset( $config['name'] ) ) { + throw new InvalidArgumentException( "Cannot register a backend with no name." ); + } + $name = $config['name']; + if ( isset( $this->backends[$name] ) ) { + throw new LogicException( "Backend with name `{$name}` already registered." ); + } elseif ( !isset( $config['class'] ) ) { + throw new InvalidArgumentException( "Backend with name `{$name}` has no class." ); + } + $class = $config['class']; + + $config['readOnly'] = !empty( $config['readOnly'] ) + ? $config['readOnly'] + : $readOnlyReason; + + unset( $config['class'] ); // backend won't need this + $this->backends[$name] = [ + 'class' => $class, + 'config' => $config, + 'instance' => null + ]; + } + } + + /** + * Get the backend object with a given name + * + * @param string $name + * @return FileBackend + * @throws InvalidArgumentException + */ + public function get( $name ) { + // Lazy-load the actual backend instance + if ( !isset( $this->backends[$name]['instance'] ) ) { + $config = $this->config( $name ); + + $class = $config['class']; + if ( $class === FileBackendMultiWrite::class ) { + foreach ( $config['backends'] as $index => $beConfig ) { + if ( isset( $beConfig['template'] ) ) { + // Config is just a modified version of a registered backend's. + // This should only be used when that config is used only by this backend. + $config['backends'][$index] += $this->config( $beConfig['template'] ); + } + } + } + + $this->backends[$name]['instance'] = new $class( $config ); + } + + return $this->backends[$name]['instance']; + } + + /** + * Get the config array for a backend object with a given name + * + * @param string $name + * @return array Parameters to FileBackend::__construct() + * @throws InvalidArgumentException + */ + public function config( $name ) { + if ( !isset( $this->backends[$name] ) ) { + throw new InvalidArgumentException( "No backend defined with the name `$name`." ); + } + $class = $this->backends[$name]['class']; + + $config = $this->backends[$name]['config']; + $config['class'] = $class; + $config += [ // set defaults + 'wikiId' => wfWikiID(), // e.g. "my_wiki-en_" + 'mimeCallback' => [ $this, 'guessMimeInternal' ], + 'obResetFunc' => 'wfResetOutputBuffers', + 'streamMimeFunc' => [ StreamFile::class, 'contentTypeFromPath' ], + 'tmpDirectory' => wfTempDir(), + 'statusWrapper' => [ Status::class, 'wrap' ], + 'wanCache' => MediaWikiServices::getInstance()->getMainWANObjectCache(), + 'srvCache' => ObjectCache::getLocalServerInstance( 'hash' ), + 'logger' => LoggerFactory::getInstance( 'FileOperation' ), + 'profiler' => Profiler::instance() + ]; + $config['lockManager'] = + LockManagerGroup::singleton( $config['wikiId'] )->get( $config['lockManager'] ); + $config['fileJournal'] = isset( $config['fileJournal'] ) + ? FileJournal::factory( $config['fileJournal'], $name ) + : FileJournal::factory( [ 'class' => NullFileJournal::class ], $name ); + + return $config; + } + + /** + * Get an appropriate backend object from a storage path + * + * @param string $storagePath + * @return FileBackend|null Backend or null on failure + */ + public function backendFromPath( $storagePath ) { + list( $backend, , ) = FileBackend::splitStoragePath( $storagePath ); + if ( $backend !== null && isset( $this->backends[$backend] ) ) { + return $this->get( $backend ); + } + + return null; + } + + /** + * @param string $storagePath + * @param string|null $content + * @param string|null $fsPath + * @return string + * @since 1.27 + */ + public function guessMimeInternal( $storagePath, $content, $fsPath ) { + $magic = MediaWiki\MediaWikiServices::getInstance()->getMimeAnalyzer(); + // Trust the extension of the storage path (caller must validate) + $ext = FileBackend::extensionFromPath( $storagePath ); + $type = $magic->guessTypesForExtension( $ext ); + // For files without a valid extension (or one at all), inspect the contents + if ( !$type && $fsPath ) { + $type = $magic->guessMimeType( $fsPath, false ); + } elseif ( !$type && strlen( $content ) ) { + $tmpFile = TempFSFile::factory( 'mime_', '', wfTempDir() ); + file_put_contents( $tmpFile->getPath(), $content ); + $type = $magic->guessMimeType( $tmpFile->getPath(), false ); + } + return $type ?: 'unknown/unknown'; + } +} diff --git a/www/wiki/includes/filebackend/README b/www/wiki/includes/filebackend/README new file mode 100644 index 00000000..c06f6fc7 --- /dev/null +++ b/www/wiki/includes/filebackend/README @@ -0,0 +1,208 @@ +/*! +\ingroup FileBackend +\page file_backend_design File backend design + +Some notes on the FileBackend architecture. + +\section intro Introduction + +To abstract away the differences among different types of storage media, +MediaWiki is providing an interface known as FileBackend. Any MediaWiki +interaction with stored files should thus use a FileBackend object. + +Different types of backing storage media are supported (ranging from local +file system to distributed object stores). The types include: + +* FSFileBackend (used for mounted file systems) +* SwiftFileBackend (used for Swift or Ceph Rados+RGW object stores) +* FileBackendMultiWrite (useful for transitioning from one backend to another) + +Configuration documentation for each type of backend is to be found in their +__construct() inline documentation. + + +\section setup Setup + +File backends are registered in LocalSettings.php via the global variable +$wgFileBackends. To access one of those defined backends, one would use +FileBackendStore::get( <name> ) which will bring back a FileBackend object +handle. Such handles are reused for any subsequent get() call (via singleton). +The FileBackends objects are caching request calls such as file stats, +SHA1 requests or TCP connection handles. + +\par Note: +Some backends may require additional PHP extensions to be enabled or can rely on a +MediaWiki extension. This is often the case when a FileBackend subclass makes use of an +upstream client API for communicating with the backing store. + + +\section fileoperations File operations + +The MediaWiki FileBackend API supports various operations on either files or +directories. See FileBackend.php for full documentation for each function. + + +\subsection reading Reading + +The following basic operations are supported for reading from a backend: + +On files: +* stat a file for basic information (timestamp, size) +* read a file into a string or several files into a map of path names to strings +* download a file or set of files to a temporary file (on a mounted file system) +* get the SHA1 hash of a file +* get various properties of a file (stat information, content time, MIME information, ...) + +On directories: +* get a list of files directly under a directory +* get a recursive list of files under a directory +* get a list of directories directly under a directory +* get a recursive list of directories under a directory + +\par Note: +Backend handles should return directory listings as iterators, all though in some cases +they may just be simple arrays (which can still be iterated over). Iterators allow for +callers to traverse a large number of file listings without consuming excessive RAM in +the process. Either the memory consumed is flatly bounded (if the iterator does paging) +or it is proportional to the depth of the portion of the directory tree being traversed +(if the iterator works via recursion). + + +\subsection writing Writing + +The following basic operations are supported for writing or changing in the backend: + +On files: +* store (copying a mounted file system file into storage) +* create (creating a file within storage from a string) +* copy (within storage) +* move (within storage) +* delete (within storage) +* lock/unlock (lock or unlock a file in storage) + +The following operations are supported for writing directories in the backend: +* prepare (create parent container and directories for a path) +* secure (try to lock-down access to a container) +* publish (try to reverse the effects of secure) +* clean (remove empty containers or directories) + + +\subsection invokingoperation Invoking an operation + +Generally, callers should use doOperations() or doQuickOperations() when doing +batches of changes, rather than making a suite of single operation calls. This +makes the system tolerate high latency much better by pipelining operations +when possible. + +doOperations() should be used for working on important original data, i.e. when +consistency is important. The former will only pipeline operations that do not +depend on each other. It is best if the operations that do not depend on each +other occur in consecutive groups. This function can also log file changes to +a journal (see FileJournal), which can be used to sync two backend instances. +One might use this function for user uploads of file for example. + +doQuickOperations() is more geared toward ephemeral items that can be easily +regenerated from original data. It will always pipeline without checking for +dependencies within the operation batch. One might use this function for +creating and purging generated thumbnails of original files for example. + + +\section consistency Consistency + +Not all backing stores are sequentially consistent by default. Various FileBackend +functions offer a "latest" option that can be passed in to assure (or try to assure) +that the latest version of the file is read. Some backing stores are consistent by +default, but callers should always assume that without this option, stale data may +be read. This is actually true for stores that have eventual consistency. + +Note that file listing functions have no "latest" flag, and thus some systems may +return stale data. Thus callers should avoid assuming that listings contain changes +made my the current client or any other client from a very short time ago. For example, +creating a file under a directory and then immediately doing a file listing operation +on that directory may result in a listing that does not include that file. + + +\section locking Locking + +Locking is effective if and only if a proper lock manager is registered and is +actually being used by the backend. Lock managers can be registered in LocalSettings.php +using the $wgLockManagers global configuration variable. + +For object stores, locking is not generally useful for avoiding partially +written or read objects, since most stores use Multi Version Concurrency +Control (MVCC) to avoid this. However, locking can be important when: +* One or more operations must be done without objects changing in the meantime. +* It can also be useful when a file read is used to determine a file write or DB change. + For example, doOperations() first checks that there will be no "file already exists" + or "file does not exist" type errors before attempting an operation batch. This works + by stating the files first, and is only safe if the files are locked in the meantime. + +When locking, callers should use the latest available file data for reads. +Also, one should always lock the file *before* reading it, not after. If stale data is +used to determine a write, there will be some data corruption, even when reads of the +original file finally start returning the updated data without needing the "latest" +option (eventual consistency). The "scoped" lock functions are preferable since +there is not the problem of forgetting to unlock due to early returns or exceptions. + +Since acquiring locks can fail, and lock managers can be non-blocking, callers should: +* Acquire all required locks up font +* Be prepared for the case where locks fail to be acquired +* Possible retry acquiring certain locks + +MVCC is also a useful pattern to use on top of the backend interface, because operations +are not atomic, even with doOperations(), so doing complex batch file changes or changing +files and updating a database row can result in partially written "transactions". Thus one +should avoid changing files once they have been stored, except perhaps with ephemeral data +that are tolerant of some degree of inconsistency. + +Callers can use their own locking (e.g. SELECT FOR UPDATE) if it is more convenient, but +note that all callers that change any of the files should then go through functions that +acquire these locks. For example, if a caller just directly uses the file backend store() +function, it will ignore any custom "FOR UPDATE" locks, which can cause problems. + +\section objectstore Object stores + +Support for object stores (like Amazon S3/Swift) drive much of the API and design +decisions of FileBackend, but using any POSIX compliant file systems works fine. +The system essentially stores "files" in "containers". For a mounted file system +as a backing store, "files" will just be files under directories. For an object store +as a backing store, the "files" will be objects stored in actual containers. + + +\section file_obj_diffs File system and Object store differences + +An advantage of object stores is the reduced Round-Trip Times. This is +achieved by avoiding the need to create each parent directory before placing a +file somewhere. It gets worse the deeper the directory hierarchy is. Another +advantage of object stores is that object listings tend to use databases, which +scale better than the linked list directories that file sytems sometimes use. +File systems like btrfs and xfs use tree structures, which scale better. +For both object stores and file systems, using "/" in filenames will allow for the +intuitive use of directory functions. For example, creating a file in Swift +called "container/a/b/file1" will mean that: +- a "directory listing" of "container/a" will contain "b", +- and a "file listing" of "b" will contain "file1" + +This means that switching from an object store to a file system and vise versa +using the FileBackend interface will generally be harmless. However, one must be +aware of some important differences: + +* In a file system, you cannot have a file and a directory within the same path + whereas it is possible in an object stores. Calling code should avoid any layouts + which allow files and directories at the same path. +* Some file systems have file name length restrictions or overall path length + restrictions that others do not. The same goes with object stores which might + have a maximum object length or a limitation regarding the number of files + under a container or volume. +* Latency varies among systems, certain access patterns may not be tolerable for + certain backends but may hold up for others. Some backend subclasses use + MediaWiki's object caching for serving stat requests, which can greatly + reduce latency. Making sure that the backend has pipelining (see the + "parallelize" and "concurrency" settings) enabled can also mask latency in + batch operation scenarios. +* File systems may implement directories as linked-lists or other structures + with poor scalability, so calling code should use layouts that shard the data. + Instead of storing files like "container/file.txt", one can store files like + "container/<x>/<y>/file.txt". It is best if "sharding" optional or configurable. + +*/ diff --git a/www/wiki/includes/filebackend/filejournal/DBFileJournal.php b/www/wiki/includes/filebackend/filejournal/DBFileJournal.php new file mode 100644 index 00000000..3dc9f18e --- /dev/null +++ b/www/wiki/includes/filebackend/filejournal/DBFileJournal.php @@ -0,0 +1,193 @@ +<?php +/** + * Version of FileJournal that logs to a DB table. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @ingroup FileJournal + */ + +use MediaWiki\MediaWikiServices; +use Wikimedia\Rdbms\IDatabase; +use Wikimedia\Rdbms\DBError; + +/** + * Version of FileJournal that logs to a DB table + * @since 1.20 + */ +class DBFileJournal extends FileJournal { + /** @var IDatabase */ + protected $dbw; + + protected $wiki = false; // string; wiki DB name + + /** + * Construct a new instance from configuration. + * + * @param array $config Includes: + * 'wiki' : wiki name to use for LoadBalancer + */ + protected function __construct( array $config ) { + parent::__construct( $config ); + + $this->wiki = $config['wiki']; + } + + /** + * @see FileJournal::logChangeBatch() + * @param array $entries + * @param string $batchId + * @return StatusValue + */ + protected function doLogChangeBatch( array $entries, $batchId ) { + $status = StatusValue::newGood(); + + try { + $dbw = $this->getMasterDB(); + } catch ( DBError $e ) { + $status->fatal( 'filejournal-fail-dbconnect', $this->backend ); + + return $status; + } + + $now = wfTimestamp( TS_UNIX ); + + $data = []; + foreach ( $entries as $entry ) { + $data[] = [ + 'fj_batch_uuid' => $batchId, + 'fj_backend' => $this->backend, + 'fj_op' => $entry['op'], + 'fj_path' => $entry['path'], + 'fj_new_sha1' => $entry['newSha1'], + 'fj_timestamp' => $dbw->timestamp( $now ) + ]; + } + + try { + $dbw->insert( 'filejournal', $data, __METHOD__ ); + if ( mt_rand( 0, 99 ) == 0 ) { + $this->purgeOldLogs(); // occasionally delete old logs + } + } catch ( DBError $e ) { + $status->fatal( 'filejournal-fail-dbquery', $this->backend ); + + return $status; + } + + return $status; + } + + /** + * @see FileJournal::doGetCurrentPosition() + * @return bool|mixed The value from the field, or false on failure. + */ + protected function doGetCurrentPosition() { + $dbw = $this->getMasterDB(); + + return $dbw->selectField( 'filejournal', 'MAX(fj_id)', + [ 'fj_backend' => $this->backend ], + __METHOD__ + ); + } + + /** + * @see FileJournal::doGetPositionAtTime() + * @param int|string $time Timestamp + * @return bool|mixed The value from the field, or false on failure. + */ + protected function doGetPositionAtTime( $time ) { + $dbw = $this->getMasterDB(); + + $encTimestamp = $dbw->addQuotes( $dbw->timestamp( $time ) ); + + return $dbw->selectField( 'filejournal', 'fj_id', + [ 'fj_backend' => $this->backend, "fj_timestamp <= $encTimestamp" ], + __METHOD__, + [ 'ORDER BY' => 'fj_timestamp DESC' ] + ); + } + + /** + * @see FileJournal::doGetChangeEntries() + * @param int|null $start + * @param int $limit + * @return array[] + */ + protected function doGetChangeEntries( $start, $limit ) { + $dbw = $this->getMasterDB(); + + $res = $dbw->select( 'filejournal', '*', + [ + 'fj_backend' => $this->backend, + 'fj_id >= ' . $dbw->addQuotes( (int)$start ) ], // $start may be 0 + __METHOD__, + array_merge( [ 'ORDER BY' => 'fj_id ASC' ], + $limit ? [ 'LIMIT' => $limit ] : [] ) + ); + + $entries = []; + foreach ( $res as $row ) { + $item = []; + foreach ( (array)$row as $key => $value ) { + $item[substr( $key, 3 )] = $value; // "fj_op" => "op" + } + $entries[] = $item; + } + + return $entries; + } + + /** + * @see FileJournal::purgeOldLogs() + * @return StatusValue + * @throws DBError + */ + protected function doPurgeOldLogs() { + $status = StatusValue::newGood(); + if ( $this->ttlDays <= 0 ) { + return $status; // nothing to do + } + + $dbw = $this->getMasterDB(); + $dbCutoff = $dbw->timestamp( time() - 86400 * $this->ttlDays ); + + $dbw->delete( 'filejournal', + [ 'fj_timestamp < ' . $dbw->addQuotes( $dbCutoff ) ], + __METHOD__ + ); + + return $status; + } + + /** + * Get a master connection to the logging DB + * + * @return IDatabase + * @throws DBError + */ + protected function getMasterDB() { + if ( !$this->dbw ) { + // Get a separate connection in autocommit mode + $lb = MediaWikiServices::getInstance()->getDBLoadBalancerFactory()->newMainLB(); + $this->dbw = $lb->getConnection( DB_MASTER, [], $this->wiki ); + $this->dbw->clearFlag( DBO_TRX ); + } + + return $this->dbw; + } +} diff --git a/www/wiki/includes/filebackend/lockmanager/LockManagerGroup.php b/www/wiki/includes/filebackend/lockmanager/LockManagerGroup.php new file mode 100644 index 00000000..5d79dac0 --- /dev/null +++ b/www/wiki/includes/filebackend/lockmanager/LockManagerGroup.php @@ -0,0 +1,176 @@ +<?php +/** + * Lock manager registration handling. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @ingroup LockManager + */ +use MediaWiki\MediaWikiServices; +use MediaWiki\Logger\LoggerFactory; + +/** + * Class to handle file lock manager registration + * + * @ingroup LockManager + * @since 1.19 + */ +class LockManagerGroup { + /** @var LockManagerGroup[] (domain => LockManagerGroup) */ + protected static $instances = []; + + protected $domain; // string; domain (usually wiki ID) + + /** @var array Array of (name => ('class' => ..., 'config' => ..., 'instance' => ...)) */ + protected $managers = []; + + /** + * @param string $domain Domain (usually wiki ID) + */ + protected function __construct( $domain ) { + $this->domain = $domain; + } + + /** + * @param bool|string $domain Domain (usually wiki ID). Default: false. + * @return LockManagerGroup + */ + public static function singleton( $domain = false ) { + $domain = ( $domain === false ) ? wfWikiID() : $domain; + if ( !isset( self::$instances[$domain] ) ) { + self::$instances[$domain] = new self( $domain ); + self::$instances[$domain]->initFromGlobals(); + } + + return self::$instances[$domain]; + } + + /** + * Destroy the singleton instances + */ + public static function destroySingletons() { + self::$instances = []; + } + + /** + * Register lock managers from the global variables + */ + protected function initFromGlobals() { + global $wgLockManagers; + + $this->register( $wgLockManagers ); + } + + /** + * Register an array of file lock manager configurations + * + * @param array $configs + * @throws Exception + */ + protected function register( array $configs ) { + foreach ( $configs as $config ) { + $config['domain'] = $this->domain; + if ( !isset( $config['name'] ) ) { + throw new Exception( "Cannot register a lock manager with no name." ); + } + $name = $config['name']; + if ( !isset( $config['class'] ) ) { + throw new Exception( "Cannot register lock manager `{$name}` with no class." ); + } + $class = $config['class']; + unset( $config['class'] ); // lock manager won't need this + $this->managers[$name] = [ + 'class' => $class, + 'config' => $config, + 'instance' => null + ]; + } + } + + /** + * Get the lock manager object with a given name + * + * @param string $name + * @return LockManager + * @throws Exception + */ + public function get( $name ) { + if ( !isset( $this->managers[$name] ) ) { + throw new Exception( "No lock manager defined with the name `$name`." ); + } + // Lazy-load the actual lock manager instance + if ( !isset( $this->managers[$name]['instance'] ) ) { + $class = $this->managers[$name]['class']; + $config = $this->managers[$name]['config']; + if ( $class === DBLockManager::class ) { + $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory(); + $lb = $lbFactory->newMainLB( $config['domain'] ); + $dbw = $lb->getLazyConnectionRef( DB_MASTER, [], $config['domain'] ); + + $config['dbServers']['localDBMaster'] = $dbw; + $config['srvCache'] = ObjectCache::getLocalServerInstance( 'hash' ); + } + $config['logger'] = LoggerFactory::getInstance( 'LockManager' ); + + $this->managers[$name]['instance'] = new $class( $config ); + } + + return $this->managers[$name]['instance']; + } + + /** + * Get the config array for a lock manager object with a given name + * + * @param string $name + * @return array + * @throws Exception + */ + public function config( $name ) { + if ( !isset( $this->managers[$name] ) ) { + throw new Exception( "No lock manager defined with the name `$name`." ); + } + $class = $this->managers[$name]['class']; + + return [ 'class' => $class ] + $this->managers[$name]['config']; + } + + /** + * Get the default lock manager configured for the site. + * Returns NullLockManager if no lock manager could be found. + * + * @return LockManager + */ + public function getDefault() { + return isset( $this->managers['default'] ) + ? $this->get( 'default' ) + : new NullLockManager( [] ); + } + + /** + * Get the default lock manager configured for the site + * or at least some other effective configured lock manager. + * Throws an exception if no lock manager could be found. + * + * @return LockManager + * @throws Exception + */ + public function getAny() { + return isset( $this->managers['default'] ) + ? $this->get( 'default' ) + : $this->get( 'fsLockManager' ); + } +} diff --git a/www/wiki/includes/filebackend/lockmanager/MySqlLockManager.php b/www/wiki/includes/filebackend/lockmanager/MySqlLockManager.php new file mode 100644 index 00000000..2108aed4 --- /dev/null +++ b/www/wiki/includes/filebackend/lockmanager/MySqlLockManager.php @@ -0,0 +1,141 @@ +<?php + +use Wikimedia\Rdbms\IDatabase; +use Wikimedia\Rdbms\DBError; + +/** + * MySQL version of DBLockManager that supports shared locks. + * + * Do NOT use this on connection handles that are also being used for anything + * else as the transaction isolation will be wrong and all the other changes will + * get rolled back when the locks release! + * + * All lock servers must have the innodb table defined in maintenance/locking/filelocks.sql. + * All locks are non-blocking, which avoids deadlocks. + * + * @ingroup LockManager + */ +class MySqlLockManager extends DBLockManager { + /** @var array Mapping of lock types to the type actually used */ + protected $lockTypeMap = [ + self::LOCK_SH => self::LOCK_SH, + self::LOCK_UW => self::LOCK_SH, + self::LOCK_EX => self::LOCK_EX + ]; + + public function __construct( array $config ) { + parent::__construct( $config ); + + $this->session = substr( $this->session, 0, 31 ); // fit to field + } + + protected function initConnection( $lockDb, IDatabase $db ) { + # Let this transaction see lock rows from other transactions + $db->query( "SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;" ); + # Do everything in a transaction as it all gets rolled back eventually + $db->startAtomic( __CLASS__ ); + } + + /** + * Get a connection to a lock DB and acquire locks on $paths. + * This does not use GET_LOCK() per https://bugs.mysql.com/bug.php?id=1118. + * + * @see DBLockManager::getLocksOnServer() + * @param string $lockSrv + * @param array $paths + * @param string $type + * @return StatusValue + */ + protected function doGetLocksOnServer( $lockSrv, array $paths, $type ) { + $status = StatusValue::newGood(); + + $db = $this->getConnection( $lockSrv ); // checked in isServerUp() + + $keys = []; // list of hash keys for the paths + $data = []; // list of rows to insert + $checkEXKeys = []; // list of hash keys that this has no EX lock on + # Build up values for INSERT clause + foreach ( $paths as $path ) { + $key = $this->sha1Base36Absolute( $path ); + $keys[] = $key; + $data[] = [ 'fls_key' => $key, 'fls_session' => $this->session ]; + if ( !isset( $this->locksHeld[$path][self::LOCK_EX] ) ) { + $checkEXKeys[] = $key; // this has no EX lock on $key itself + } + } + + # Block new writers (both EX and SH locks leave entries here)... + $db->insert( 'filelocks_shared', $data, __METHOD__, [ 'IGNORE' ] ); + # Actually do the locking queries... + if ( $type == self::LOCK_SH ) { // reader locks + # Bail if there are any existing writers... + if ( count( $checkEXKeys ) ) { + $blocked = $db->selectField( + 'filelocks_exclusive', + '1', + [ 'fle_key' => $checkEXKeys ], + __METHOD__ + ); + } else { + $blocked = false; + } + # Other prospective writers that haven't yet updated filelocks_exclusive + # will recheck filelocks_shared after doing so and bail due to this entry. + } else { // writer locks + $encSession = $db->addQuotes( $this->session ); + # Bail if there are any existing writers... + # This may detect readers, but the safe check for them is below. + # Note: if two writers come at the same time, both bail :) + $blocked = $db->selectField( + 'filelocks_shared', + '1', + [ 'fls_key' => $keys, "fls_session != $encSession" ], + __METHOD__ + ); + if ( !$blocked ) { + # Build up values for INSERT clause + $data = []; + foreach ( $keys as $key ) { + $data[] = [ 'fle_key' => $key ]; + } + # Block new readers/writers... + $db->insert( 'filelocks_exclusive', $data, __METHOD__ ); + # Bail if there are any existing readers... + $blocked = $db->selectField( + 'filelocks_shared', + '1', + [ 'fls_key' => $keys, "fls_session != $encSession" ], + __METHOD__ + ); + } + } + + if ( $blocked ) { + foreach ( $paths as $path ) { + $status->fatal( 'lockmanager-fail-acquirelock', $path ); + } + } + + return $status; + } + + /** + * @see QuorumLockManager::releaseAllLocks() + * @return StatusValue + */ + protected function releaseAllLocks() { + $status = StatusValue::newGood(); + + foreach ( $this->conns as $lockDb => $db ) { + if ( $db->trxLevel() ) { // in transaction + try { + $db->rollback( __METHOD__ ); // finish transaction and kill any rows + } catch ( DBError $e ) { + $status->fatal( 'lockmanager-fail-db-release', $lockDb ); + } + } + } + + return $status; + } +} |