summaryrefslogtreecommitdiff
path: root/www/wiki/includes/filebackend
diff options
context:
space:
mode:
authorYaco <franco@reevo.org>2020-06-04 11:01:00 -0300
committerYaco <franco@reevo.org>2020-06-04 11:01:00 -0300
commitfc7369835258467bf97eb64f184b93691f9a9fd5 (patch)
treedaabd60089d2dd76d9f5fb416b005fbe159c799d /www/wiki/includes/filebackend
first commit
Diffstat (limited to 'www/wiki/includes/filebackend')
-rw-r--r--www/wiki/includes/filebackend/FileBackendGroup.php247
-rw-r--r--www/wiki/includes/filebackend/README208
-rw-r--r--www/wiki/includes/filebackend/filejournal/DBFileJournal.php193
-rw-r--r--www/wiki/includes/filebackend/lockmanager/LockManagerGroup.php176
-rw-r--r--www/wiki/includes/filebackend/lockmanager/MySqlLockManager.php141
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;
+ }
+}