Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PLATFORM-9062 | Allow Cargo to use replica databases #19

Merged
merged 4 commits into from
Mar 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions extension.json
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,8 @@
"CargoLinksUpdateHandler": "includes/hooks/CargoLinksUpdateHandler.php",
"CargoSearchMySQL": "includes/search/CargoSearchMySQL.php",
"CargoPageSchemas": "includes/CargoPageSchemas.php",
"CargoConnectionProvider": "includes/CargoConnectionProvider.php",
"CargoServices": "includes/CargoServices.php",
"CargoAppliedFilter": "drilldown/CargoAppliedFilter.php",
"CargoFilter": "drilldown/CargoFilter.php",
"CargoFilterValue": "drilldown/CargoFilterValue.php",
Expand Down Expand Up @@ -472,6 +474,7 @@
"class": "CargoLinksUpdateHandler"
}
},
"ServiceWiringFiles": ["includes/ServiceWiring.php"],
"config": {
"CargoDecimalMark": ".",
"CargoDigitGroupingCharacter": ",",
Expand All @@ -483,6 +486,7 @@
"CargoDBpassword": null,
"CargoDBprefix": null,
"CargoDBRowFormat": null,
"CargoDBCluster": null,
"CargoDBIndex": null,
"CargoDefaultStringBytes": 300,
"CargoDefaultQueryLimit": 100,
Expand Down
187 changes: 187 additions & 0 deletions includes/CargoConnectionProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
<?php

use MediaWiki\Config\ServiceOptions;
use Wikimedia\Rdbms\Database;
use Wikimedia\Rdbms\DatabaseFactory;
use Wikimedia\Rdbms\IDatabase;
use Wikimedia\Rdbms\ILBFactory;
use Wikimedia\Rdbms\ILoadBalancer;

/**
* Class to manage access to the Cargo database.
*
* By default, this class creates and manages a single connection to the local wiki DB.
* It can be configured to connect to a different DB using the 'wgCargoDB*' settings,
* or to eschew manual connection management in favor of MediaWiki's DBAL by setting the 'CargoDBCluster' option.
*/
class CargoConnectionProvider {
public const CONSTRUCTOR_OPTIONS = [
// MediaWiki DB setup variables.
'DBuser',
'DBpassword',
'DBport',
'DBprefix',
'DBservers',

// Optional Cargo-specific DB setup variables.
'CargoDBserver',
'CargoDBname',
'CargoDBuser',
'CargoDBpassword',
'CargoDBprefix',
'CargoDBtype',

// Fandom change: Optional DB index override to use for Cargo in a single-connection setup (PLATFORM-7466)
'CargoDBIndex',

// Optional external cluster name to use for Cargo.
// Supersedes all above configuration if present.
'CargoDBCluster'
];

private ILBFactory $lbFactory;

/**
* Connection factory to use for creating DB connections on MW 1.39 and newer.
* @var DatabaseFactory|null
*/
private ?DatabaseFactory $databaseFactory;

/**
* Configuration options used by this service.
* @var ServiceOptions
*/
private ServiceOptions $serviceOptions;

/**
* The database connection to use for accessing Cargo data, if 'CargoDBCluster' is not set.
* @var IDatabase|null
*/
private ?IDatabase $connection = null;

public function __construct(
ILBFactory $lbFactory,
?DatabaseFactory $databaseFactory,
ServiceOptions $serviceOptions
) {
$serviceOptions->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );

$this->lbFactory = $lbFactory;
$this->databaseFactory = $databaseFactory;
$this->serviceOptions = $serviceOptions;
}

/**
* Get a database connection for accessing Cargo data.
* @param int $dbType DB type to use (primary or replica)
* @return IDatabase
*/
public function getConnection( int $dbType ): IDatabase {
$cluster = $this->serviceOptions->get( 'CargoDBCluster' );

// If a cluster is specified, let MediaWiki's DBAL manage the lifecycle of Cargo-related connections.
if ( $cluster !== null ) {
$lb = $this->lbFactory->getExternalLB( $cluster );

// Fall back to the primary DB if there were recent writes, to ensure that Cargo sees its own changes.
$dbType = $lb->hasOrMadeRecentPrimaryChanges() ? ILoadBalancer::DB_PRIMARY : $dbType;
$conn = $lb->getConnection( $dbType );

// Fandom change: Ensure Cargo DB connections use 4-byte UTF-8 client character set (UGC-4625).
self::setClientCharacterSet( $conn );
return $conn;
}

if ( $this->connection === null ) {
$this->connection = $this->initConnection();

// Fandom change: Ensure Cargo DB connections use 4-byte UTF-8 client character set (UGC-4625).
self::setClientCharacterSet( $this->connection );
}

return $this->connection;
}

/**
* Get the DB type (e.g. 'postgres') of the Cargo database.
* This is mainly useful for code that needs to generate platform-specific SQL.
* @return string
*/
public function getDBType(): string {
return $this->serviceOptions->get( 'CargoDBtype' ) ?? $this->getConnection( DB_REPLICA )->getType();
}

/**
* Create a database connection for Cargo data managed entirely by this class.
* @return IDatabase
*/
private function initConnection(): IDatabase {
$lb = $this->lbFactory->getMainLB();
// Fandom change: Use the DB index specified in the CargoDBIndex option (PLATFORM-7466).
$index = $this->serviceOptions->get( 'CargoDBIndex' ) ?? $lb::DB_PRIMARY;
$dbr = $lb->getConnection( $index );

$dbServers = $this->serviceOptions->get( 'DBservers' );
$dbUser = $this->serviceOptions->get( 'DBuser' );
$dbPassword = $this->serviceOptions->get( 'DBpassword' );

$dbServer = $this->serviceOptions->get( 'CargoDBserver' ) ?? $dbr->getServer();
$dbName = $this->serviceOptions->get( 'CargoDBname' ) ?? $dbr->getDBname();
$dbType = $this->serviceOptions->get( 'CargoDBtype' ) ?? $dbr->getType();

// Server (host), db name, and db type can be retrieved from $dbr via
// public methods, but username and password cannot. If these values are
// not set for Cargo, get them from either $wgDBservers or wgDBuser and
// $wgDBpassword, depending on whether or not there are multiple DB servers.
$dbUsername = $this->serviceOptions->get( 'CargoDBuser' ) ?? $dbServers[0]['user'] ?? $dbUser;
$dbPassword = $this->serviceOptions->get( 'CargoDBpassword' ) ?? $dbServers[0]['password'] ?? $dbPassword;
$dbTablePrefix = $this->serviceOptions->get( 'CargoDBprefix' )
?? $this->serviceOptions->get( 'DBprefix' ) . 'cargo__';

$params = [
'host' => $dbServer,
'user' => $dbUsername,
'password' => $dbPassword,
'dbname' => $dbName,
'tablePrefix' => $dbTablePrefix,
];

if ( $dbType === 'sqlite' ) {
/** @var \Wikimedia\Rdbms\DatabaseSqlite $dbr */
$params['dbFilePath'] = $dbr->getDbFilePath();
} elseif ( $dbType === 'postgres' ) {
// @TODO - a $wgCargoDBport variable is still needed.
$params['port'] = $this->serviceOptions->get( 'DBport' );
}

if ( $this->databaseFactory !== null ) {
return $this->databaseFactory->create( $dbType, $params );
}

return Database::factory( $dbType, $params );
}

/**
* Set the client character set of a database connection handle to 4-byte UTF-8.
* This is necessary because Cargo utilizes functions such as REGEXP_LIKE(),
* which fail if the client character set is "binary".
*
* @param IDatabase $dbw Database connection handle.
*/
private static function setClientCharacterSet( IDatabase $dbw ): void {
if ( $dbw instanceof DatabaseMysqli ) {
// Force open the database connection so that we can obtain the underlying native connection handle.
$dbw->ping();

$ref = new ReflectionMethod( $dbw, 'getBindingHandle' );
$ref->setAccessible( true );

/** @var mysqli $mysqli */
$mysqli = $ref->invoke( $dbw );
if ( $mysqli->character_set_name() !== 'utf8mb4' ) {
$mysqli->set_charset( 'utf8mb4' );
}
}
}

}
3 changes: 2 additions & 1 deletion includes/CargoRecreateDataAction.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<?php

use MediaWiki\MediaWikiServices;
use MediaWiki\Permissions\PermissionManager;

/**
* Handles the 'recreatedata' action.
Expand Down Expand Up @@ -58,7 +59,7 @@ public static function displayTab( $obj, &$links ) {

$user = $obj->getUser();
$permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
if ( !$permissionManager->userCan( 'recreatecargodata', $user, $title ) ) {
if ( !$permissionManager->userCan( 'recreatecargodata', $user, $title, PermissionManager::RIGOR_QUICK ) ) {
return true;
}

Expand Down
3 changes: 1 addition & 2 deletions includes/CargoSQLQuery.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ class CargoSQLQuery {
public $mDateFieldPairs = [];

public function __construct() {
$this->mCargoDB = CargoUtils::getDB();
$this->mCargoDB = CargoServices::getCargoConnectionProvider()->getConnection( DB_REPLICA );
}

/**
Expand All @@ -56,7 +56,6 @@ public static function newFromValues( $tablesStr, $fieldsStr, $whereStr, $joinOn
$havingStr, $orderByStr, $limitStr, $offsetStr, $allowFieldEscaping );

$sqlQuery = new CargoSQLQuery();
$sqlQuery->mCargoDB = CargoUtils::getDB();
$sqlQuery->mTablesStr = $tablesStr;
$sqlQuery->setAliasedTableNames();
$sqlQuery->mFieldsStr = $fieldsStr;
Expand Down
12 changes: 12 additions & 0 deletions includes/CargoServices.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

use MediaWiki\MediaWikiServices;

/**
* Typed service locator class for Cargo service classes.
*/
class CargoServices {
public static function getCargoConnectionProvider(): CargoConnectionProvider {
return MediaWikiServices::getInstance()->getService( 'CargoConnectionProvider' );
}
}
Loading
Loading