Allow REST API POST handlers to opt out of mandatory sqlite locking

This is a follow up to T93097, which worked around a limitation of
SQLite 3.8+ which prevents it from upgrading a read transaction to a write
transaction.  Our heuristic is that HTTP POST requests are going to
eventually become DB writes, and so we take the SQLite lock early.

However, Parsoid POST requests don't have any side effects on the DB,
and taking the write lock causes deadlocks during VE saves: the POST
to action=visualeditoredit conflicts with the POST to the recursive
request to the Parsoid REST API.  We can't use a GET request for
these requests without hitting query-length limits.

This patch allows REST API calls to set the `X-MediaWiki-Read` header
to opt-out of the SQLite obligatory lock.  This avoids the deadlock,
while still allowing the API call to use a POST and avoid query length
limits.

Bug: T259685
Change-Id: If37dc890a24a45c3a914e310b5b5bf625965e9e6
This commit is contained in:
C. Scott Ananian 2020-08-26 17:37:38 -04:00 committed by C. Scott Ananian
parent 631d7e45ef
commit b75ac3953e
2 changed files with 25 additions and 4 deletions

View file

@ -26,6 +26,8 @@ class EntryPoint {
private $context;
/** @var CorsUtils */
private $cors;
/** @var ?RequestInterface */
private static $mainRequest;
/**
* @param IContextSource $context
@ -77,6 +79,19 @@ class EntryPoint {
) )->setCors( $cors );
}
/**
* @return ?RequestInterface The RequestInterface object used by this entry point.
*/
public static function getMainRequest(): ?RequestInterface {
if ( self::$mainRequest === null ) {
$conf = MediaWikiServices::getInstance()->getMainConfig();
self::$mainRequest = new RequestFromGlobals( [
'cookiePrefix' => $conf->get( 'CookiePrefix' )
] );
}
return self::$mainRequest;
}
public static function main() {
// URL safety checks
global $wgRequest;
@ -91,10 +106,6 @@ class EntryPoint {
$services = MediaWikiServices::getInstance();
$conf = $services->getMainConfig();
$request = new RequestFromGlobals( [
'cookiePrefix' => $conf->get( 'CookiePrefix' )
] );
$responseFactory = new ResponseFactory( self::getTextFormatters( $services ) );
$cors = new CorsUtils(
@ -105,6 +116,8 @@ class EntryPoint {
$context->getUser()
);
$request = self::getMainRequest();
$router = self::createRouter( $context, $request, $responseFactory, $cors );
$entryPoint = new self(

View file

@ -182,6 +182,14 @@ abstract class MWLBFactory {
// See https://www.sqlite.org/lang_transaction.html
// See https://www.sqlite.org/lockingv3.html#shared_lock
$isHttpRead = in_array( $httpMethod, [ 'GET', 'HEAD', 'OPTIONS', 'TRACE' ] );
if ( MW_ENTRY_POINT === 'rest' && !$isHttpRead ) {
// Hack to support some re-entrant invocations using sqlite
// See: T259685
$request = \MediaWiki\Rest\EntryPoint::getMainRequest();
if ( $request->hasHeader( 'X-MediaWiki-Read' ) ) {
$isHttpRead = true;
}
}
$server += [
'dbDirectory' => $options->get( 'SQLiteDataDir' ),
'trxMode' => $isHttpRead ? 'DEFERRED' : 'IMMEDIATE'