diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php index c25807d0090..8791a941453 100644 --- a/includes/DefaultSettings.php +++ b/includes/DefaultSettings.php @@ -2370,6 +2370,13 @@ $wgDatabaseReplicaLagWarning = 10; */ $wgDatabaseReplicaLagCritical = 30; +/** + * Max execution time for queries of several expensive special pages such as RecentChanges + * in milliseconds. + * @since 1.38 + */ +$wgMaxExecutionTimeForExpensiveQueries = 0; + /** * RevisionStore table schema migration stage (content, slots, content_models & slot_roles tables). * Use the SCHEMA_COMPAT_XXX flags. Supported values: diff --git a/includes/ServiceWiring.php b/includes/ServiceWiring.php index 4f86fda33de..765f6533b25 100644 --- a/includes/ServiceWiring.php +++ b/includes/ServiceWiring.php @@ -1877,7 +1877,8 @@ return [ $services->getCommentStore(), $services->getWatchedItemStore(), $services->getHookContainer(), - $services->getMainConfig()->get( 'WatchlistExpiry' ) + $services->getMainConfig()->get( 'WatchlistExpiry' ), + $services->getMainConfig()->get( 'MaxExecutionTimeForExpensiveQueries' ) ); }, diff --git a/includes/api/ApiQueryLogEvents.php b/includes/api/ApiQueryLogEvents.php index eaeecc08139..a2f6d15ed36 100644 --- a/includes/api/ApiQueryLogEvents.php +++ b/includes/api/ApiQueryLogEvents.php @@ -259,6 +259,11 @@ class ApiQueryLogEvents extends ApiQueryBase { $this->addOption( 'STRAIGHT_JOIN' ); } + $this->addOption( + 'MAX_EXECUTION_TIME', + $this->getConfig()->get( 'MaxExecutionTimeForExpensiveQueries' ) + ); + $count = 0; $res = $this->select( __METHOD__ ); diff --git a/includes/api/ApiQueryRecentChanges.php b/includes/api/ApiQueryRecentChanges.php index 3e34ef70a6e..52199ae8db4 100644 --- a/includes/api/ApiQueryRecentChanges.php +++ b/includes/api/ApiQueryRecentChanges.php @@ -416,6 +416,10 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase { } $this->addOption( 'LIMIT', $params['limit'] + 1 ); + $this->addOption( + 'MAX_EXECUTION_TIME', + $this->getConfig()->get( 'MaxExecutionTimeForExpensiveQueries' ) + ); $hookData = []; $count = 0; diff --git a/includes/api/ApiQueryUserContribs.php b/includes/api/ApiQueryUserContribs.php index 1080b8e5758..673092316b5 100644 --- a/includes/api/ApiQueryUserContribs.php +++ b/includes/api/ApiQueryUserContribs.php @@ -486,6 +486,10 @@ class ApiQueryUserContribs extends ApiQueryBase { $this->addWhere( '1=0' ); } } + $this->addOption( + 'MAX_EXECUTION_TIME', + $this->getConfig()->get( 'MaxExecutionTimeForExpensiveQueries' ) + ); } /** diff --git a/includes/libs/rdbms/querybuilder/SelectQueryBuilder.php b/includes/libs/rdbms/querybuilder/SelectQueryBuilder.php index d1b5095c654..10cbd770613 100644 --- a/includes/libs/rdbms/querybuilder/SelectQueryBuilder.php +++ b/includes/libs/rdbms/querybuilder/SelectQueryBuilder.php @@ -388,6 +388,17 @@ class SelectQueryBuilder extends JoinGroupBase { return $this; } + /** + * Set MAX_EXECUTION_TIME for queries. + * + * @param int $time maximum allowed time in milliseconds + * @return $this + */ + public function setMaxExecutionTime( int $time ) { + $this->options['MAX_EXECUTION_TIME'] = $time; + return $this; + } + /** * Add a GROUP BY clause. May be either an SQL fragment string naming a * field or expression to group by, or an array of such SQL fragments. diff --git a/includes/logging/LogPager.php b/includes/logging/LogPager.php index ed6f524ba12..e9a9a5c56f1 100644 --- a/includes/logging/LogPager.php +++ b/includes/logging/LogPager.php @@ -380,6 +380,8 @@ class LogPager extends ReverseChronologicalPager { $options[] = 'STRAIGHT_JOIN'; } + $options['MAX_EXECUTION_TIME'] = $this->getConfig()->get( 'MaxExecutionTimeForExpensiveQueries' ); + $info = [ 'tables' => $tables, 'fields' => $fields, diff --git a/includes/specials/SpecialRecentChanges.php b/includes/specials/SpecialRecentChanges.php index ea81503c8d4..4cc25ee9191 100644 --- a/includes/specials/SpecialRecentChanges.php +++ b/includes/specials/SpecialRecentChanges.php @@ -398,6 +398,7 @@ class SpecialRecentChanges extends ChangesListSpecialPage { // MediaWiki 1.26 this used to use the plus operator instead, which meant // that extensions weren't able to change these conditions $query_options = array_merge( $orderByAndLimit, $query_options ); + $query_options['MAX_EXECUTION_TIME'] = $this->getConfig()->get( 'MaxExecutionTimeForExpensiveQueries' ); $rows = $dbr->select( $tables, $fields, diff --git a/includes/specials/SpecialRecentChangesLinked.php b/includes/specials/SpecialRecentChangesLinked.php index 5911fa05b21..94ce9e0a4eb 100644 --- a/includes/specials/SpecialRecentChangesLinked.php +++ b/includes/specials/SpecialRecentChangesLinked.php @@ -249,7 +249,6 @@ class SpecialRecentChangesLinked extends SpecialRecentChanges { ' ORDER BY rc_timestamp DESC'; $sql = $dbr->limitResult( $sql, $limit, false ); } - return $dbr->query( $sql, __METHOD__ ); } diff --git a/includes/specials/SpecialWatchlist.php b/includes/specials/SpecialWatchlist.php index 7485a6e127d..2e48b94a6f5 100644 --- a/includes/specials/SpecialWatchlist.php +++ b/includes/specials/SpecialWatchlist.php @@ -459,6 +459,7 @@ class SpecialWatchlist extends ChangesListSpecialPage { // array_merge() is used intentionally here so that hooks can, should // they so desire, override the ORDER BY / LIMIT condition(s) $query_options = array_merge( $orderByAndLimit, $query_options ); + $query_options['MAX_EXECUTION_TIME'] = $this->getConfig()->get( 'MaxExecutionTimeForExpensiveQueries' ); return $dbr->select( $tables, diff --git a/includes/specials/pagers/ContribsPager.php b/includes/specials/pagers/ContribsPager.php index 0a07dd47b68..70c04168f7d 100644 --- a/includes/specials/pagers/ContribsPager.php +++ b/includes/specials/pagers/ContribsPager.php @@ -276,6 +276,7 @@ class ContribsPager extends RangeChronologicalPager { $order ); + $options['MAX_EXECUTION_TIME'] = $this->getConfig()->get( 'MaxExecutionTimeForExpensiveQueries' ); /* * This hook will allow extensions to add in additional queries, so they can get their data * in My Contributions as well. Extensions should append their results to the $data array. diff --git a/includes/watcheditem/WatchedItemQueryService.php b/includes/watcheditem/WatchedItemQueryService.php index d7979c81546..d7442e1a6eb 100644 --- a/includes/watcheditem/WatchedItemQueryService.php +++ b/includes/watcheditem/WatchedItemQueryService.php @@ -78,18 +78,25 @@ class WatchedItemQueryService { */ private $expiryEnabled; + /** + * @var int Max query execution time + */ + private $maxQueryExecutionTime; + public function __construct( ILoadBalancer $loadBalancer, CommentStore $commentStore, WatchedItemStoreInterface $watchedItemStore, HookContainer $hookContainer, - bool $expiryEnabled = false + bool $expiryEnabled = false, + int $maxQueryExecutionTime = 0 ) { $this->loadBalancer = $loadBalancer; $this->commentStore = $commentStore; $this->watchedItemStore = $watchedItemStore; $this->hookRunner = new HookRunner( $hookContainer ); $this->expiryEnabled = $expiryEnabled; + $this->maxQueryExecutionTime = $maxQueryExecutionTime; } /** @@ -712,7 +719,9 @@ class WatchedItemQueryService { if ( array_key_exists( 'limit', $options ) ) { $dbOptions['LIMIT'] = (int)$options['limit'] + 1; } - + if ( $this->maxQueryExecutionTime ) { + $dbOptions['MAX_EXECUTION_TIME'] = $this->maxQueryExecutionTime; + } return $dbOptions; } @@ -730,6 +739,9 @@ class WatchedItemQueryService { if ( array_key_exists( 'limit', $options ) ) { $dbOptions['LIMIT'] = (int)$options['limit']; } + if ( $this->maxQueryExecutionTime ) { + $dbOptions['MAX_EXECUTION_TIME'] = $this->maxQueryExecutionTime; + } return $dbOptions; }