$wgDBport = 5432;
/**
- * Name of the database
+ * Name of the database; this should be alphanumeric and not contain spaces nor hyphens
*/
$wgDBname = 'my_wiki';
$wgSearchTypeAlternatives = null;
/**
- * Table name prefix
+ * Table name prefix; this should be alphanumeric and not contain spaces nor hyphens
*/
$wgDBprefix = '';
$wgSQLMode = '';
/**
- * Mediawiki schema
+ * Mediawiki schema; this should be alphanumeric and not contain spaces nor hyphens
*/
$wgDBmwschema = null;
/**
* Other wikis on this site, can be administered from a single developer account.
*
- * Array numeric key => database name
+ * @var string[] List of wiki DB domain IDs; the format of each ID consist of 1-3 hyphen
+ * delimited alphanumeric components (each with no hyphens nor spaces) of any of the forms:
+ * - "<DB NAME>-<DB SCHEMA>-<TABLE PREFIX>"
+ * - "<DB NAME>-<TABLE PREFIX>"
+ * - "<DB NAME>"
+ * If hyphens appear in any of the components, then the domain ID parsing may not work
+ * in all cases and site functionality might be affected. If the schema ($wgDBmwschema)
+ * is left to the default "mediawiki" for all wikis, then the schema should be omitted
+ * from these IDs.
*/
$wgLocalDatabases = [];
/**
* Work out the site and language name from a database name
- * @param string $db
+ * @param string $wiki Wiki ID
*
* @return array
*/
- public function siteFromDB( $db ) {
+ public function siteFromDB( $wiki ) {
// Allow override
- $def = $this->getWikiParams( $db );
+ $def = $this->getWikiParams( $wiki );
if ( !is_null( $def['suffix'] ) && !is_null( $def['lang'] ) ) {
return [ $def['suffix'], $def['lang'] ];
}
foreach ( $this->suffixes as $altSite => $suffix ) {
if ( $suffix === '' ) {
$site = '';
- $lang = $db;
+ $lang = $wiki;
break;
- } elseif ( substr( $db, -strlen( $suffix ) ) == $suffix ) {
+ } elseif ( substr( $wiki, -strlen( $suffix ) ) == $suffix ) {
$site = is_numeric( $altSite ) ? $suffix : $altSite;
- $lang = substr( $db, 0, strlen( $db ) - strlen( $suffix ) );
+ $lang = substr( $wiki, 0, strlen( $wiki ) - strlen( $suffix ) );
break;
}
}
$lang = str_replace( '_', '-', $lang );
+
return [ $site, $lang ];
}
* Get the wiki ID of a database domain
*
* This is like DatabaseDomain::getId() without encoding (for legacy reasons)
+ * and without the schema if it merely set to the generic value "mediawiki"
*
* @param string|DatabaseDomain $domain
* @return string
public static function getWikiIdFromDomain( $domain ) {
$domain = DatabaseDomain::newFromId( $domain );
+ if ( !in_array( $domain->getSchema(), [ null, 'mediawiki' ], true ) ) {
+ // Include the schema if it is set and is not the default placeholder.
+ // This means a site admin may have specifically taylored the schemas.
+ // Domain IDs might use the form <DB>-<project>-<language>, meaning that
+ // the schema portion must be accounted for to disambiguate wikis.
+ return "{$domain->getDatabase()}-{$domain->getSchema()}-{$domain->getTablePrefix()}";
+ }
+
+ // Note that if this wiki ID is passed a a domain ID to LoadBalancer, then it can
+ // handle the schema by assuming the generic "mediawiki" schema if needed.
return strlen( $domain->getTablePrefix() )
? "{$domain->getDatabase()}-{$domain->getTablePrefix()}"
- : $domain->getDatabase();
+ : (string)$domain->getDatabase();
}
/**
$domain = DatabaseDomain::newFromId( $domain );
$curDomain = self::getCurrentWikiDomain();
+ if ( !in_array( $curDomain->getSchema(), [ null, 'mediawiki' ], true ) ) {
+ // Include the schema if it is set and is not the default placeholder.
+ // This means a site admin may have specifically taylored the schemas.
+ // Domain IDs might use the form <DB>-<project>-<language>, meaning that
+ // the schema portion must be accounted for to disambiguate wikis.
+ return $curDomain->equals( $domain );
+ }
+
return (
$curDomain->getDatabase() === $domain->getDatabase() &&
- // @TODO: check schema instead of assuming it's ""/"mediawiki" and never collides
$curDomain->getTablePrefix() === $domain->getTablePrefix()
);
}
"config-db-host-oracle": "Database TNS:",
"config-db-host-oracle-help": "Enter a valid [http://download.oracle.com/docs/cd/B28359_01/network.111/b28317/tnsnames.htm Local Connect Name]; a tnsnames.ora file must be visible to this installation.<br />If you are using client libraries 10g or newer you can also use the [http://download.oracle.com/docs/cd/E11882_01/network.112/e10836/naming.htm Easy Connect] naming method.",
"config-db-wiki-settings": "Identify this wiki",
- "config-db-name": "Database name:",
+ "config-db-name": "Database name (no hyphens):",
"config-db-name-help": "Choose a name that identifies your wiki.\nIt should not contain spaces.\n\nIf you are using shared web hosting, your hosting provider will either give you a specific database name to use or let you create databases via a control panel.",
"config-db-name-oracle": "Database schema:",
"config-db-account-oracle-warn": "There are three supported scenarios for installing Oracle as database backend:\n\nIf you wish to create database account as part of the installation process, please supply an account with SYSDBA role as database account for installation and specify the desired credentials for the web-access account, otherwise you can either create the web-access account manually and supply only that account (if it has required permissions to create the schema objects) or supply two different accounts, one with create privileges and a restricted one for web access.\n\nScript for creating an account with required privileges can be found in \"maintenance/oracle/\" directory of this installation. Keep in mind that using a restricted account will disable all maintenance capabilities with the default account.",
"config-db-account-lock": "Use the same username and password during normal operation",
"config-db-wiki-account": "User account for normal operation",
"config-db-wiki-help": "Enter the username and password that will be used to connect to the database during normal wiki operation.\nIf the account does not exist, and the installation account has sufficient privileges, this user account will be created with the minimum privileges required to operate the wiki.",
- "config-db-prefix": "Database table prefix:",
+ "config-db-prefix": "Database table prefix (no hyphens):",
"config-db-prefix-help": "If you need to share one database between multiple wikis, or between MediaWiki and another web application, you may choose to add a prefix to all the table names to avoid conflicts.\nDo not use spaces.\n\nThis field is usually left empty.",
"config-mysql-old": "MySQL $1 or later is required. You have $2.",
"config-db-port": "Database port:",
- "config-db-schema": "Schema for MediaWiki:",
+ "config-db-schema": "Schema for MediaWiki (no hyphens):",
"config-db-schema-help": "This schema will usually be fine.\nOnly change it if you know you need to.",
"config-pg-test-error": "Cannot connect to database <strong>$1</strong>: $2",
"config-sqlite-dir": "SQLite data directory:",
*/
abstract class JobQueue {
/** @var string Wiki ID */
- protected $wiki;
+ protected $domain;
/** @var string Job type */
protected $type;
/** @var string Job priority for pop() */
* @throws MWException
*/
protected function __construct( array $params ) {
- $this->wiki = $params['wiki'];
+ $this->domain = $params['domain'] ?? $params['wiki']; // b/c
$this->type = $params['type'];
$this->claimTTL = $params['claimTTL'] ?? 0;
$this->maxTries = $params['maxTries'] ?? 3;
/**
* @return string Wiki ID
*/
+ final public function getDomain() {
+ return $this->domain;
+ }
+
+ /**
+ * @return string Wiki ID
+ * @deprecated 1.33
+ */
final public function getWiki() {
- return $this->wiki;
+ return $this->domain;
}
/**
}
$this->doBatchPush( $jobs, $flags );
- $this->aggr->notifyQueueNonEmpty( $this->wiki, $this->type );
+ $this->aggr->notifyQueueNonEmpty( $this->domain, $this->type );
foreach ( $jobs as $job ) {
if ( $job->isRootJob() ) {
global $wgJobClasses;
$this->assertNotReadOnly();
- if ( !WikiMap::isCurrentWikiId( $this->wiki ) ) {
- throw new MWException( "Cannot pop '{$this->type}' job off foreign wiki queue." );
+ if ( !WikiMap::isCurrentWikiDomain( $this->domain ) ) {
+ throw new MWException(
+ "Cannot pop '{$this->type}' job off foreign '{$this->domain}' wiki queue." );
} elseif ( !isset( $wgJobClasses[$this->type] ) ) {
// Do not pop jobs if there is no class for the queue type
throw new MWException( "Unrecognized job type '{$this->type}'." );
$job = $this->doPop();
if ( !$job ) {
- $this->aggr->notifyQueueEmpty( $this->wiki, $this->type );
+ $this->aggr->notifyQueueEmpty( $this->domain, $this->type );
}
// Flag this job as an old duplicate based on its "root" job...
* @return string
*/
protected function getRootJobCacheKey( $signature ) {
- list( $db, $prefix ) = wfSplitWikiID( $this->wiki );
-
- return wfForeignMemcKey( $db, $prefix, 'jobqueue', $this->type, 'rootjob', $signature );
+ $this->dupCache->makeGlobalKey(
+ 'jobqueue',
+ $this->domain,
+ $this->type,
+ 'rootjob',
+ $signature
+ );
}
/**
*/
protected function doWaitForBackups() {
$lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
- $lbFactory->waitForReplication( [ 'wiki' => $this->wiki, 'cluster' => $this->cluster ] );
+ $lbFactory->waitForReplication(
+ [ 'domain' => $this->domain, 'cluster' => $this->cluster ] );
}
/**
public function getCoalesceLocationInternal() {
return $this->cluster
- ? "DBCluster:{$this->cluster}:{$this->wiki}"
- : "LBFactory:{$this->wiki}";
+ ? "DBCluster:{$this->cluster}:{$this->domain}"
+ : "LBFactory:{$this->domain}";
}
protected function doGetSiblingQueuesWithJobs( array $types ) {
$affected = $dbw->affectedRows();
$count += $affected;
JobQueue::incrStats( 'recycles', $this->type, $affected );
- $this->aggr->notifyQueueNonEmpty( $this->wiki, $this->type );
+ $this->aggr->notifyQueueNonEmpty( $this->domain, $this->type );
}
}
$lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
$lb = ( $this->cluster !== false )
? $lbFactory->getExternalLB( $this->cluster )
- : $lbFactory->getMainLB( $this->wiki );
+ : $lbFactory->getMainLB( $this->domain );
return ( $lb->getServerType( $lb->getWriterIndex() ) !== 'sqlite' )
// Keep a separate connection to avoid contention and deadlocks;
// However, SQLite has the opposite behavior due to DB-level locking.
- ? $lb->getConnectionRef( $index, [], $this->wiki, $lb::CONN_TRX_AUTOCOMMIT )
+ ? $lb->getConnectionRef( $index, [], $this->domain, $lb::CONN_TRX_AUTOCOMMIT )
// Jobs insertion will be defered until the PRESEND stage to reduce contention.
- : $lb->getConnectionRef( $index, [], $this->wiki );
+ : $lb->getConnectionRef( $index, [], $this->domain );
}
/**
$cluster = is_string( $this->cluster ) ? $this->cluster : 'main';
return $this->cache->makeGlobalKey(
- 'jobqueue', $this->wiki, $cluster, $this->type, $property );
+ 'jobqueue',
+ $this->domain,
+ $cluster,
+ $this->type,
+ $property
+ );
}
/**
*/
protected function __construct( array $params ) {
parent::__construct( $params );
- $section = $params['sectionsByWiki'][$this->wiki] ?? 'default';
+ $section = $params['sectionsByWiki'][$this->domain] ?? 'default';
if ( !isset( $params['partitionsBySection'][$section] ) ) {
throw new MWException( "No configuration for section '$section'." );
}
}
public function getCoalesceLocationInternal() {
- return "JobQueueFederated:wiki:{$this->wiki}" .
+ return "JobQueueFederated:wiki:{$this->domain}" .
sha1( serialize( array_keys( $this->partitionQueues ) ) );
}
*
* @file
*/
+use MediaWiki\MediaWikiServices;
/**
* Class to handle enqueueing of background jobs
/** @var ProcessCacheLRU */
protected $cache;
- /** @var string Wiki ID */
- protected $wiki;
+ /** @var string Wiki domain ID */
+ protected $domain;
/** @var string|bool Read only rationale (or false if r/w) */
protected $readOnlyReason;
/** @var bool Whether the wiki is not recognized in configuration */
- protected $invalidWiki = false;
+ protected $invalidDomain = false;
/** @var array Map of (bucket => (queue => JobQueue, types => list of types) */
protected $coalescedQueues;
const CACHE_VERSION = 1; // integer; cache version
/**
- * @param string $wiki Wiki ID
+ * @param string $domain Wiki domain ID
* @param string|bool $readOnlyReason Read-only reason or false
*/
- protected function __construct( $wiki, $readOnlyReason ) {
- $this->wiki = $wiki;
+ protected function __construct( $domain, $readOnlyReason ) {
+ $this->domain = $domain;
$this->readOnlyReason = $readOnlyReason;
$this->cache = new MapCacheLRU( 10 );
}
/**
- * @param bool|string $wiki Wiki ID
+ * @param bool|string $domain Wiki domain ID
* @return JobQueueGroup
*/
- public static function singleton( $wiki = false ) {
+ public static function singleton( $domain = false ) {
global $wgLocalDatabases;
- $wiki = ( $wiki === false ) ? wfWikiID() : $wiki;
+ if ( $domain === false ) {
+ $domain = WikiMap::getCurrentWikiDomain()->getId();
+ }
- if ( !isset( self::$instances[$wiki] ) ) {
- self::$instances[$wiki] = new self( $wiki, wfConfiguredReadOnlyReason() );
+ if ( !isset( self::$instances[$domain] ) ) {
+ self::$instances[$domain] = new self( $domain, wfConfiguredReadOnlyReason() );
// Make sure jobs are not getting pushed to bogus wikis. This can confuse
// the job runner system into spawning endless RPC requests that fail (T171371).
- if ( !WikiMap::isCurrentWikiId( $wiki ) && !in_array( $wiki, $wgLocalDatabases ) ) {
- self::$instances[$wiki]->invalidWiki = true;
+ $wikiId = WikiMap::getWikiIdFromDomain( $domain );
+ if (
+ !WikiMap::isCurrentWikiDomain( $domain ) &&
+ !in_array( $wikiId, $wgLocalDatabases )
+ ) {
+ self::$instances[$domain]->invalidDomain = true;
}
}
- return self::$instances[$wiki];
+ return self::$instances[$domain];
}
/**
public function get( $type ) {
global $wgJobTypeConf;
- $conf = [ 'wiki' => $this->wiki, 'type' => $type ];
- $conf += $wgJobTypeConf[$type] ?? $wgJobTypeConf['default'];
+ $conf = [ 'domain' => $this->domain, 'type' => $type ];
+ if ( isset( $wgJobTypeConf[$type] ) ) {
+ $conf = $conf + $wgJobTypeConf[$type];
+ } else {
+ $conf = $conf + $wgJobTypeConf['default'];
+ }
$conf['aggregator'] = JobQueueAggregator::singleton();
if ( !isset( $conf['readOnlyReason'] ) ) {
$conf['readOnlyReason'] = $this->readOnlyReason;
public function push( $jobs ) {
global $wgJobTypesExcludedFromDefaultQueue;
- if ( $this->invalidWiki ) {
+ if ( $this->invalidDomain ) {
// Do not enqueue job that cannot be run (T171371)
- $e = new LogicException( "Domain '{$this->wiki}' is not recognized." );
+ $e = new LogicException( "Domain '{$this->domain}' is not recognized." );
MWExceptionHandler::logException( $e );
return;
}
$cache = ObjectCache::getLocalClusterInstance();
$cache->set(
- $cache->makeGlobalKey( 'jobqueue', $this->wiki, 'hasjobs', self::TYPE_ANY ),
+ $cache->makeGlobalKey( 'jobqueue', $this->domain, 'hasjobs', self::TYPE_ANY ),
'true',
15
);
if ( array_diff( array_keys( $jobsByType ), $wgJobTypesExcludedFromDefaultQueue ) ) {
$cache->set(
- $cache->makeGlobalKey( 'jobqueue', $this->wiki, 'hasjobs', self::TYPE_DEFAULT ),
+ $cache->makeGlobalKey( 'jobqueue', $this->domain, 'hasjobs', self::TYPE_DEFAULT ),
'true',
15
);
* @since 1.26
*/
public function lazyPush( $jobs ) {
- if ( $this->invalidWiki ) {
+ if ( $this->invalidDomain ) {
// Do not enqueue job that cannot be run (T171371)
- throw new LogicException( "Domain '{$this->wiki}' is not recognized." );
+ throw new LogicException( "Domain '{$this->domain}' is not recognized." );
}
if ( PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg' ) {
// Throw errors now instead of on push(), when other jobs may be buffered
$this->assertValidJobs( $jobs );
- DeferredUpdates::addUpdate( new JobQueueEnqueueUpdate( $this->wiki, $jobs ) );
+ DeferredUpdates::addUpdate( new JobQueueEnqueueUpdate( $this->domain, $jobs ) );
}
/**
*/
public function queuesHaveJobs( $type = self::TYPE_ANY ) {
$cache = ObjectCache::getLocalClusterInstance();
- $key = $cache->makeGlobalKey( 'jobqueue', $this->wiki, 'hasjobs', $type );
+ $key = $cache->makeGlobalKey( 'jobqueue', $this->domain, 'hasjobs', $type );
$value = $cache->get( $key );
if ( $value === false ) {
$this->coalescedQueues = [];
foreach ( $wgJobTypeConf as $type => $conf ) {
$queue = JobQueue::factory(
- [ 'wiki' => $this->wiki, 'type' => 'null' ] + $conf );
+ [ 'wiki' => $this->domain, 'type' => 'null' ] + $conf );
$loc = $queue->getCoalesceLocationInternal();
if ( !isset( $this->coalescedQueues[$loc] ) ) {
$this->coalescedQueues[$loc]['queue'] = $queue;
*/
private function getCachedConfigVar( $name ) {
// @TODO: cleanup this whole method with a proper config system
- if ( WikiMap::isCurrentWikiId( $this->wiki ) ) {
+ if ( WikiMap::isCurrentWikiDomain( $this->domain ) ) {
return $GLOBALS[$name]; // common case
} else {
- $wiki = $this->wiki;
- $cache = ObjectCache::getMainWANInstance();
+ $wiki = WikiMap::getWikiIdFromDomain( $this->domain );
+ $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
$value = $cache->getWithSetCallback(
- $cache->makeGlobalKey( 'jobqueue', 'configvalue', $wiki, $name ),
+ $cache->makeGlobalKey( 'jobqueue', 'configvalue', $this->domain, $name ),
$cache::TTL_DAY + mt_rand( 0, $cache::TTL_DAY ),
function () use ( $wiki, $name ) {
global $wgConf;
-
+ // @TODO: use the full domain ID here
return [ 'v' => $wgConf->getConfig( $wiki, $name ) ];
},
[ 'pcTTL' => WANObjectCache::TTL_PROC_LONG ]
* @see JobQueue::doDelete
*/
protected function doDelete() {
- if ( isset( self::$data[$this->type][$this->wiki] ) ) {
- unset( self::$data[$this->type][$this->wiki] );
+ if ( isset( self::$data[$this->type][$this->domain] ) ) {
+ unset( self::$data[$this->type][$this->domain] );
if ( !self::$data[$this->type] ) {
unset( self::$data[$this->type] );
}
* @return mixed
*/
private function &getQueueData( $field, $init = null ) {
- if ( !isset( self::$data[$this->type][$this->wiki][$field] ) ) {
+ if ( !isset( self::$data[$this->type][$this->domain][$field] ) ) {
if ( $init !== null ) {
- self::$data[$this->type][$this->wiki][$field] = $init;
+ self::$data[$this->type][$this->domain][$field] = $init;
} else {
return $init;
}
}
- return self::$data[$this->type][$this->wiki][$field];
+ return self::$data[$this->type][$this->domain][$field];
}
}
* @return string JSON
*/
private function encodeQueueName() {
- return json_encode( [ $this->type, $this->wiki ] );
+ return json_encode( [ $this->type, $this->domain ] );
}
/**
*/
private function getQueueKey( $prop, $type = null ) {
$type = is_string( $type ) ? $type : $this->type;
- list( $db, $prefix ) = wfSplitWikiID( $this->wiki );
- $keyspace = $prefix ? "$db-$prefix" : $db;
+
+ // Use wiki ID for b/c
+ $keyspace = WikiMap::getWikiIdFromDomain( $this->domain );
$parts = [ $keyspace, 'jobqueue', $type, $prop ];
global $wgLocalDatabases;
$pendingDBs = []; // (job type => (db list))
- foreach ( $wgLocalDatabases as $db ) {
- foreach ( JobQueueGroup::singleton( $db )->getQueuesWithJobs() as $type ) {
- $pendingDBs[$type][] = $db;
+ foreach ( $wgLocalDatabases as $wikiId ) {
+ foreach ( JobQueueGroup::singleton( $wikiId )->getQueuesWithJobs() as $type ) {
+ $pendingDBs[$type][] = $wikiId;
}
}
private function repushAbandoned( JobQueue $queue ) {
$cache = ObjectCache::getInstance( CACHE_DB );
- $key = $cache->makeGlobalKey( 'last-job-repush', $queue->getWiki(), $queue->getType() );
+ $key = $cache->makeGlobalKey( 'last-job-repush', $queue->getDomain(), $queue->getType() );
$now = wfTimestampNow();
$lastRepushTime = $cache->get( $key );
return [
[ 'db-prefix', 'db-prefix' ],
[ wfWikiID(), wfWikiID() ],
- [ new DatabaseDomain( 'db-dash', null, 'prefix' ), 'db-dash-prefix' ]
+ [ new DatabaseDomain( 'db-dash', null, 'prefix' ), 'db-dash-prefix' ],
+ [ wfWikiID(), wfWikiID() ],
+ [ new DatabaseDomain( 'db-dash', null, 'prefix' ), 'db-dash-prefix' ],
+ [ new DatabaseDomain( 'db', 'mediawiki', 'prefix' ), 'db-prefix' ], // schema ignored
+ [ new DatabaseDomain( 'db', 'custom', 'prefix' ), 'db-custom-prefix' ],
];
}
public function provideIsCurrentWikiId() {
return [
[ 'db', 'db', null, '' ],
- [ 'db','db', 'schema', '' ],
+ [ 'db-schema-','db', 'schema', '' ],
+ [ 'db','db', 'mediawiki', '' ], // common b/c case
[ 'db-prefix', 'db', null, 'prefix' ],
- [ 'db-prefix', 'db', 'schema', 'prefix' ],
+ [ 'db-schema-prefix', 'db', 'schema', 'prefix' ],
+ [ 'db-prefix', 'db', 'mediawiki', 'prefix' ], // common b/c case
// Bad hyphen cases (best effort support)
[ 'db-stuff', 'db-stuff', null, '' ],
- [ 'db-stuff-prefix', 'db-stuff', null, 'prefix' ]
+ [ 'db-stuff-prefix', 'db-stuff', null, 'prefix' ],
+ [ 'db-stuff-schema-', 'db-stuff', 'schema', '' ],
+ [ 'db-stuff-schema-prefix', 'db-stuff', 'schema', 'prefix' ],
+ [ 'db-stuff-prefix', 'db-stuff', 'mediawiki', 'prefix' ] // common b/c case
];
}
$this->markTestSkipped( $desc );
}
$this->assertEquals( wfWikiID(), $queue->getWiki(), "Proper wiki ID ($desc)" );
+ $this->assertEquals( wfWikiID(), $queue->getDomain(), "Proper wiki ID ($desc)" );
}
/**
protected function getDB( $index ) {
$lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
// Override to not use CONN_TRX_AUTOCOMMIT so that we see the same temporary `job` table
- return $lb->getConnection( $index, [], $this->wiki );
+ return $lb->getConnection( $index, [], $this->domain );
}
}