From b75ac3953e750fd6b1b29868a77dbebd7969fbdc Mon Sep 17 00:00:00 2001 From: "C. Scott Ananian" Date: Wed, 26 Aug 2020 17:37:38 -0400 Subject: [PATCH] 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 --- includes/Rest/EntryPoint.php | 21 +++++++++++++++++---- includes/db/MWLBFactory.php | 8 ++++++++ 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/includes/Rest/EntryPoint.php b/includes/Rest/EntryPoint.php index 6ff11daf20b..b8580d4f0ab 100644 --- a/includes/Rest/EntryPoint.php +++ b/includes/Rest/EntryPoint.php @@ -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( diff --git a/includes/db/MWLBFactory.php b/includes/db/MWLBFactory.php index f71124c970c..05f4cbb9c64 100644 --- a/includes/db/MWLBFactory.php +++ b/includes/db/MWLBFactory.php @@ -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'