phpunit.php: --regex and --keep-uploads. Instead of --regex, use --filter.
Instead of --keep-uploads, use the same option to parserTests.php, but you
must specify a directory with --upload-dir.
+* The 'jquery.arrowSteps' ResourceLoader module is now deprecated.
== Compatibility ==
'AtomFeed' => __DIR__ . '/includes/Feed.php',
'AtomicSectionUpdate' => __DIR__ . '/includes/deferred/AtomicSectionUpdate.php',
'AttachLatest' => __DIR__ . '/maintenance/attachLatest.php',
+ 'AugmentPageProps' => __DIR__ . '/includes/search/AugmentPageProps.php',
'AuthManagerSpecialPage' => __DIR__ . '/includes/specialpage/AuthManagerSpecialPage.php',
'AuthPlugin' => __DIR__ . '/includes/AuthPlugin.php',
'AuthPluginUser' => __DIR__ . '/includes/AuthPlugin.php',
'IExpiringStore' => __DIR__ . '/includes/libs/objectcache/IExpiringStore.php',
'IJobSpecification' => __DIR__ . '/includes/jobqueue/JobSpecification.php',
'ILoadBalancer' => __DIR__ . '/includes/libs/rdbms/loadbalancer/ILoadBalancer.php',
+ 'ILoadMonitor' => __DIR__ . '/includes/libs/rdbms/loadmonitor/ILoadMonitor.php',
'IP' => __DIR__ . '/includes/utils/IP.php',
'IPSet' => __DIR__ . '/includes/compat/IPSetCompat.php',
'IPTC' => __DIR__ . '/includes/media/IPTC.php',
'PatrolLog' => __DIR__ . '/includes/logging/PatrolLog.php',
'PatrolLogFormatter' => __DIR__ . '/includes/logging/PatrolLogFormatter.php',
'Pbkdf2Password' => __DIR__ . '/includes/password/Pbkdf2Password.php',
+ 'PerRowAugmentor' => __DIR__ . '/includes/search/PerRowAugmentor.php',
'PermissionsError' => __DIR__ . '/includes/exception/PermissionsError.php',
'PhpHttpRequest' => __DIR__ . '/includes/HttpFunctions.php',
'PhpXmlBugTester' => __DIR__ . '/includes/installer/PhpBugTests.php',
'ResourceLoaderUserTokensModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderUserTokensModule.php',
'ResourceLoaderWikiModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderWikiModule.php',
'RestbaseVirtualRESTService' => __DIR__ . '/includes/libs/virtualrest/RestbaseVirtualRESTService.php',
+ 'ResultAugmentor' => __DIR__ . '/includes/search/ResultAugmentor.php',
+ 'ResultSetAugmentor' => __DIR__ . '/includes/search/ResultSetAugmentor.php',
'ResultWrapper' => __DIR__ . '/includes/libs/rdbms/database/resultwrapper/ResultWrapper.php',
'RevDelArchiveItem' => __DIR__ . '/includes/revisiondelete/RevDelArchiveItem.php',
'RevDelArchiveList' => __DIR__ . '/includes/revisiondelete/RevDelArchiveList.php',
$output: ParserOutput that is produced from the page
$engine: SearchEngine for which the indexing is intended
+'SearchResultsAugment': Allows extension to add its code to the list of search
+result augmentors.
+&$setAugmentors: List of whole-set augmentor objects, must implement ResultSetAugmentor
+&$rowAugmentors: List of per-row augmentor objects, must implement ResultAugmentor.
+Note that lists should be in the format name => object and the names in both lists should
+be distinct.
+
'SecondaryDataUpdates': Allows modification of the list of DataUpdates to
perform when page content is modified. Currently called by
AbstractContent::getSecondaryDataUpdates.
* @defgroup Constants MediaWiki constants
*/
-/**@{
- * Database related constants
- */
-define( 'DBO_DEBUG', 1 );
-define( 'DBO_NOBUFFER', 2 );
-define( 'DBO_IGNORE', 4 );
-define( 'DBO_TRX', 8 ); // automatically start transaction on first query
-define( 'DBO_DEFAULT', 16 );
-define( 'DBO_PERSISTENT', 32 );
-define( 'DBO_SYSDBA', 64 ); // for oracle maintenance
-define( 'DBO_DDLMODE', 128 ); // when using schema files: mostly for Oracle
-define( 'DBO_SSL', 256 );
-define( 'DBO_COMPRESS', 512 );
-/**@}*/
-
-/**@{
- * Valid database indexes
- * Operation-based indexes
- */
-define( 'DB_REPLICA', -1 ); # Read from a replica (or only server)
-define( 'DB_MASTER', -2 ); # Write to master (or only server)
-/**@}*/
-
# Obsolete aliases
define( 'DB_SLAVE', -1 );
define( 'EDIT_INTERNAL', 128 );
/**@}*/
-/**@{
- * Flags for Database::makeList()
- * These are also available as Database class constants
+/**
+ * Database related
*/
-define( 'LIST_COMMA', 0 );
-define( 'LIST_AND', 1 );
-define( 'LIST_SET', 2 );
-define( 'LIST_NAMES', 3 );
-define( 'LIST_OR', 4 );
-/**@}*/
+require_once __DIR__ . '/libs/rdbms/defines.php';
/**
* Unicode and normalisation related
) {
$this->displayViewSourcePage(
$this->getContentObject(),
- wfMessage(
+ $this->context->msg(
'contentmodelediterror',
$revision->getContentModel(),
$this->contentModel
Hooks::run( 'EditPage::showReadOnlyForm:initial', [ $this, &$wgOut ] );
$wgOut->setRobotPolicy( 'noindex,nofollow' );
- $wgOut->setPageTitle( wfMessage(
+ $wgOut->setPageTitle( $this->context->msg(
'viewsource-title',
$this->getContextTitle()->getPrefixedText()
) );
if ( $firstrev && $firstrev->getId() == $undo ) {
$userText = $undorev->getUserText();
if ( $userText === '' ) {
- $undoSummary = wfMessage(
+ $undoSummary = $this->context->msg(
'undo-summary-username-hidden',
$undo
)->inContentLanguage()->text();
} else {
- $undoSummary = wfMessage(
+ $undoSummary = $this->context->msg(
'undo-summary',
$undo,
$userText
if ( $this->summary === '' ) {
$this->summary = $undoSummary;
} else {
- $this->summary = $undoSummary . wfMessage( 'colon-separator' )
+ $this->summary = $undoSummary . $this->context->msg( 'colon-separator' )
->inContentLanguage()->text() . $this->summary;
}
$this->undidRev = $undo;
// Messages: undo-success, undo-failure, undo-norev, undo-nochange
$class = ( $undoMsg == 'success' ? '' : 'error ' ) . "mw-undo-{$undoMsg}";
$this->editFormPageTop .= $wgOut->parse( "<div class=\"{$class}\">" .
- wfMessage( 'undo-' . $undoMsg )->plain() . '</div>', true, /* interface */true );
+ $this->context->msg( 'undo-' . $undoMsg )->plain() . '</div>', true, /* interface */true );
}
if ( $content === false ) {
// passed.
if ( $this->summary === '' ) {
$cleanSectionTitle = $wgParser->stripSectionName( $this->sectiontitle );
- return wfMessage( 'newsectionsummary' )
+ return $this->context->msg( 'newsectionsummary' )
->rawParams( $cleanSectionTitle )->inContentLanguage()->text();
}
} elseif ( $this->summary !== '' ) {
# This is a new section, so create a link to the new section
# in the revision summary.
$cleanSummary = $wgParser->stripSectionName( $this->summary );
- return wfMessage( 'newsectionsummary' )
+ return $this->context->msg( 'newsectionsummary' )
->rawParams( $cleanSummary )->inContentLanguage()->text();
}
return $this->summary;
if ( $displayTitle === false ) {
$displayTitle = $contextTitle->getPrefixedText();
}
- $wgOut->setPageTitle( wfMessage( $msg, $displayTitle ) );
+ $wgOut->setPageTitle( $this->context->msg( $msg, $displayTitle ) );
# Transmit the name of the message to JavaScript for live preview
# Keep Resources.php/mediawiki.action.edit.preview in sync with the possible keys
$wgOut->addJsConfigVars( [
# Try to add a custom edit intro, or use the standard one if this is not possible.
if ( !$this->showCustomIntro() && !$this->mTitle->exists() ) {
$helpLink = wfExpandUrl( Skin::makeInternalOrExternalUrl(
- wfMessage( 'helppage' )->inContentLanguage()->text()
+ $this->context->msg( 'helppage' )->inContentLanguage()->text()
) );
if ( $wgUser->isLoggedIn() ) {
$wgOut->wrapWikiMsg(
. Html::rawElement(
'label',
[ 'for' => 'wpAntispam' ],
- wfMessage( 'simpleantispam-label' )->parse()
+ $this->context->msg( 'simpleantispam-label' )->parse()
)
. Xml::element(
'input',
: 'confirmrecreate';
$wgOut->addHTML(
'<div class="mw-confirm-recreate">' .
- wfMessage( $key, $username, "<nowiki>$comment</nowiki>" )->parse() .
- Xml::checkLabel( wfMessage( 'recreate' )->text(), 'wpRecreate', 'wpRecreate', false,
+ $this->context->msg( $key, $username, "<nowiki>$comment</nowiki>" )->parse() .
+ Xml::checkLabel( $this->context->msg( 'recreate' )->text(), 'wpRecreate', 'wpRecreate', false,
[ 'title' => Linker::titleAttrib( 'recreate' ), 'tabindex' => 1, 'id' => 'wpRecreate' ]
) .
'</div>'
$this->showConflict();
} catch ( MWContentSerializationException $ex ) {
// this can't really happen, but be nice if it does.
- $msg = wfMessage(
+ $msg = $this->context->msg(
'content-failed-to-parse',
$this->contentModel,
$this->contentFormat,
if ( count( $editNotices ) ) {
$wgOut->addHTML( implode( "\n", $editNotices ) );
} else {
- $msg = wfMessage( 'editnotice-notext' );
+ $msg = $this->context->msg( 'editnotice-notext' );
if ( !$msg->isDisabled() ) {
$wgOut->addHTML(
'<div class="mw-editnotice-notext">'
]
);
} else {
- if ( !wfMessage( 'longpage-hint' )->isDisabled() ) {
+ if ( !$this->context->msg( 'longpage-hint' )->isDisabled() ) {
$wgOut->wrapWikiMsg( "<div id='mw-edit-longpage-hint'>\n$1\n</div>",
[
'longpage-hint',
return;
}
}
- $labelText = wfMessage( $isSubjectPreview ? 'subject' : 'summary' )->parse();
+ $labelText = $this->context->msg( $isSubjectPreview ? 'subject' : 'summary' )->parse();
list( $label, $input ) = $this->getSummaryInput(
$summary,
$labelText,
global $wgParser;
if ( $isSubjectPreview ) {
- $summary = wfMessage( 'newsectionsummary' )->rawParams( $wgParser->stripSectionName( $summary ) )
+ $summary = $this->context->msg( 'newsectionsummary' )
+ ->rawParams( $wgParser->stripSectionName( $summary ) )
->inContentLanguage()->text();
}
$message = $isSubjectPreview ? 'subject-preview' : 'summary-preview';
- $summary = wfMessage( $message )->parse()
+ $summary = $this->context->msg( $message )->parse()
. Linker::commentBlock( $summary, $this->mTitle, $isSubjectPreview );
return Xml::tags( 'div', [ 'class' => 'mw-summary-preview' ], $summary );
}
try {
$this->showDiff();
} catch ( MWContentSerializationException $ex ) {
- $msg = wfMessage(
+ $msg = $this->context->msg(
'content-failed-to-parse',
$this->contentModel,
$this->contentFormat,
}
if ( ( $oldContent && !$oldContent->isEmpty() ) || ( $newContent && !$newContent->isEmpty() ) ) {
- $oldtitle = wfMessage( $oldtitlemsg )->parse();
- $newtitle = wfMessage( 'yourtext' )->parse();
+ $oldtitle = $this->context->msg( $oldtitlemsg )->parse();
+ $newtitle = $this->context->msg( 'yourtext' )->parse();
if ( !$oldContent ) {
$oldContent = $newContent->getContentHandler()->makeEmptyContent();
*/
protected function showHeaderCopyrightWarning() {
$msg = 'editpage-head-copy-warn';
- if ( !wfMessage( $msg )->isDisabled() ) {
+ if ( !$this->context->msg( $msg )->isDisabled() ) {
global $wgOut;
$wgOut->wrapWikiMsg( "<div class='editpage-head-copywarn'>\n$1\n</div>",
'editpage-head-copy-warn' );
protected function showTosSummary() {
$msg = 'editpage-tos-summary';
Hooks::run( 'EditPageTosSummary', [ $this->mTitle, &$msg ] );
- if ( !wfMessage( $msg )->isDisabled() ) {
+ if ( !$this->context->msg( $msg )->isDisabled() ) {
global $wgOut;
$wgOut->addHTML( '<div class="mw-tos-summary">' );
$wgOut->addWikiMsg( $msg );
protected function showEditTools() {
global $wgOut;
$wgOut->addHTML( '<div class="mw-editTools">' .
- wfMessage( 'edittools' )->inContentLanguage()->parse() .
+ $this->context->msg( 'edittools' )->inContentLanguage()->parse() .
'</div>' );
}
Hooks::run( 'EditPageCopyrightWarning', [ $title, &$copywarnMsg ] );
return "<div id=\"editpage-copywarn\">\n" .
- call_user_func_array( 'wfMessage', $copywarnMsg )->$format() . "\n</div>";
+ call_user_func_array( 'wfMessage', $copywarnMsg )->title( $title )->$format() . "\n</div>";
}
/**
if ( $cancel !== '' ) {
$cancel .= Html::element( 'span',
[ 'class' => 'mw-editButtons-pipe-separator' ],
- wfMessage( 'pipe-separator' )->text() );
+ $this->context->msg( 'pipe-separator' )->text() );
}
- $message = wfMessage( 'edithelppage' )->inContentLanguage()->text();
+ $message = $this->context->msg( 'edithelppage' )->inContentLanguage()->text();
$edithelpurl = Skin::makeInternalOrExternalUrl( $message );
$attrs = [
'target' => 'helpwindow',
'href' => $edithelpurl,
];
- $edithelp = Html::linkButton( wfMessage( 'edithelp' )->text(),
+ $edithelp = Html::linkButton( $this->context->msg( 'edithelp' )->text(),
$attrs, [ 'mw-ui-quiet' ] ) .
- wfMessage( 'word-separator' )->escaped() .
- wfMessage( 'newwindow' )->parse();
+ $this->context->msg( 'word-separator' )->escaped() .
+ $this->context->msg( 'newwindow' )->parse();
$wgOut->addHTML( " <span class='cancelLink'>{$cancel}</span>\n" );
$wgOut->addHTML( " <span class='editHelp'>{$edithelp}</span>\n" );
$de = $handler->createDifferenceEngine( $this->mArticle->getContext() );
$de->setContent( $content2, $content1 );
$de->showDiff(
- wfMessage( 'yourtext' )->parse(),
- wfMessage( 'storedversion' )->text()
+ $this->context->msg( 'yourtext' )->parse(),
+ $this->context->msg( 'storedversion' )->text()
);
$wgOut->wrapWikiMsg( '<h2>$1</h2>', "yourtext" );
return Linker::linkKnown(
$this->getContextTitle(),
- wfMessage( 'cancel' )->parse(),
+ $this->context->msg( 'cancel' )->parse(),
Html::buttonAttributes( $attrs, [ 'mw-ui-quiet' ] ),
$cancelParams
);
// Quick paranoid permission checks...
if ( is_object( $data ) ) {
if ( $data->log_deleted & LogPage::DELETED_USER ) {
- $data->user_name = wfMessage( 'rev-deleted-user' )->escaped();
+ $data->user_name = $this->context->msg( 'rev-deleted-user' )->escaped();
}
if ( $data->log_deleted & LogPage::DELETED_COMMENT ) {
- $data->log_comment = wfMessage( 'rev-deleted-comment' )->escaped();
+ $data->log_comment = $this->context->msg( 'rev-deleted-comment' )->escaped();
}
}
// string, which happens when you initially edit
// a category page, due to automatic preview-on-open.
$parsedNote = $wgOut->parse( "<div class='previewnote'>" .
- wfMessage( 'session_fail_preview_html' )->text() . "</div>", true, /* interface */true );
+ $this->context->msg( 'session_fail_preview_html' )->text() . "</div>",
+ true, /* interface */true );
}
$stats->increment( 'edit.failures.session_loss' );
return $parsedNote;
# provide a anchor link to the editform
$continueEditing = '<span class="mw-continue-editing">' .
'[[#' . self::EDITFORM_ID . '|' . $wgLang->getArrow() . ' ' .
- wfMessage( 'continue-editing' )->text() . ']]</span>';
+ $this->context->msg( 'continue-editing' )->text() . ']]</span>';
if ( $this->mTriedSave && !$this->mTokenOk ) {
if ( $this->mTokenOkExceptSuffix ) {
- $note = wfMessage( 'token_suffix_mismatch' )->plain();
+ $note = $this->context->msg( 'token_suffix_mismatch' )->plain();
$stats->increment( 'edit.failures.bad_token' );
} else {
- $note = wfMessage( 'session_fail_preview' )->plain();
+ $note = $this->context->msg( 'session_fail_preview' )->plain();
$stats->increment( 'edit.failures.session_loss' );
}
} elseif ( $this->incompleteForm ) {
- $note = wfMessage( 'edit_form_incomplete' )->plain();
+ $note = $this->context->msg( 'edit_form_incomplete' )->plain();
if ( $this->mTriedSave ) {
$stats->increment( 'edit.failures.incomplete_form' );
}
} else {
- $note = wfMessage( 'previewnote' )->plain() . ' ' . $continueEditing;
+ $note = $this->context->msg( 'previewnote' )->plain() . ' ' . $continueEditing;
}
# don't parse non-wikitext pages, show message about preview
# Messages: usercsspreview, userjspreview, sitecsspreview, sitejspreview
if ( $level && $format ) {
$note = "<div id='mw-{$level}{$format}preview'>" .
- wfMessage( "{$level}{$format}preview" )->text() .
+ $this->context->msg( "{$level}{$format}preview" )->text() .
' ' . $continueEditing . "</div>";
}
}
}
} catch ( MWContentSerializationException $ex ) {
- $m = wfMessage(
+ $m = $this->context->msg(
'content-failed-to-parse',
$this->contentModel,
$this->contentFormat,
if ( $this->isConflict ) {
$conflict = '<h2 id="mw-previewconflict">'
- . wfMessage( 'previewconflict' )->escaped() . "</h2>\n";
+ . $this->context->msg( 'previewconflict' )->escaped() . "</h2>\n";
} else {
$conflict = '<hr />';
}
$previewhead = "<div class='previewnote'>\n" .
- '<h2 id="mw-previewheader">' . wfMessage( 'preview' )->escaped() . "</h2>" .
+ '<h2 id="mw-previewheader">' . $this->context->msg( 'preview' )->escaped() . "</h2>" .
$wgOut->parse( $note, true, /* interface */true ) . $conflict . "</div>\n";
$pageViewLang = $this->mTitle->getPageViewLanguage();
// don't show the minor edit checkbox if it's a new page or section
if ( !$this->isNew ) {
$checkboxes['minor'] = '';
- $minorLabel = wfMessage( 'minoredit' )->parse();
+ $minorLabel = $this->context->msg( 'minoredit' )->parse();
if ( $wgUser->isAllowed( 'minoredit' ) ) {
$attribs = [
'tabindex' => ++$tabindex,
- 'accesskey' => wfMessage( 'accesskey-minoredit' )->text(),
+ 'accesskey' => $this->context->msg( 'accesskey-minoredit' )->text(),
'id' => 'wpMinoredit',
];
$minorEditHtml =
}
}
- $watchLabel = wfMessage( 'watchthis' )->parse();
+ $watchLabel = $this->context->msg( 'watchthis' )->parse();
$checkboxes['watch'] = '';
if ( $wgUser->isLoggedIn() ) {
$attribs = [
'tabindex' => ++$tabindex,
- 'accesskey' => wfMessage( 'accesskey-watch' )->text(),
+ 'accesskey' => $this->context->msg( 'accesskey-watch' )->text(),
'id' => 'wpWatchthis',
];
$watchThisHtml =
} else {
$buttonLabelKey = !$this->mTitle->exists() ? 'savearticle' : 'savechanges';
}
- $buttonLabel = wfMessage( $buttonLabelKey )->text();
+ $buttonLabel = $this->context->msg( $buttonLabelKey )->text();
$attribs = [
'id' => 'wpSave',
'name' => 'wpSave',
'name' => 'wpPreview',
'tabindex' => $tabindex,
] + Linker::tooltipAndAccesskeyAttribs( 'preview' );
- $buttons['preview'] = Html::submitButton( wfMessage( 'showpreview' )->text(),
+ $buttons['preview'] = Html::submitButton( $this->context->msg( 'showpreview' )->text(),
$attribs );
$buttons['live'] = '';
'name' => 'wpDiff',
'tabindex' => ++$tabindex,
] + Linker::tooltipAndAccesskeyAttribs( 'diff' );
- $buttons['diff'] = Html::submitButton( wfMessage( 'showdiff' )->text(),
+ $buttons['diff'] = Html::submitButton( $this->context->msg( 'showdiff' )->text(),
$attribs );
Hooks::run( 'EditPageBeforeEditButtons', [ &$this, &$buttons, &$tabindex ] );
function noSuchSectionPage() {
global $wgOut;
- $wgOut->prepareErrorPage( wfMessage( 'nosuchsectiontitle' ) );
+ $wgOut->prepareErrorPage( $this->context->msg( 'nosuchsectiontitle' ) );
- $res = wfMessage( 'nosuchsectiontext', $this->section )->parseAsBlock();
+ $res = $this->context->msg( 'nosuchsectiontext', $this->section )->parseAsBlock();
Hooks::run( 'EditPageNoSuchSection', [ &$this, &$res ] );
$wgOut->addHTML( $res );
if ( is_array( $match ) ) {
$match = $wgLang->listToText( $match );
}
- $wgOut->prepareErrorPage( wfMessage( 'spamprotectiontitle' ) );
+ $wgOut->prepareErrorPage( $this->context->msg( 'spamprotectiontitle' ) );
$wgOut->addHTML( '<div id="spamprotected">' );
$wgOut->addWikiMsg( 'spamprotectiontext' );
*/
public function run() {
try {
+ $this->setDBProfilingAgent();
try {
$this->main();
} catch ( ErrorPageError $e ) {
$this->doPostOutputShutdown( 'normal' );
}
+ private function setDBProfilingAgent() {
+ $services = MediaWikiServices::getInstance();
+ // Add a comment for easy SHOW PROCESSLIST interpretation
+ $name = $this->context->getUser()->getName();
+ $services->getDBLoadBalancerFactory()->setAgentName(
+ mb_strlen( $name ) > 15 ? mb_substr( $name, 0, 15 ) . '...' : $name
+ );
+ }
+
/**
* @see MediaWiki::preOutputCommit()
* @param callable $postCommitWork [default: null]
* This clears some fields in this object, and clears any associated
* keys in the "bad links" section of the link cache.
*
- * - This is called from WikiPage::doEdit() and WikiPage::insertOn() to allow
+ * - This is called from WikiPage::doEditContent() and WikiPage::insertOn() to allow
* loading of the new page_id. It's also called from
* WikiPage::doDeleteArticleReal()
*
case EditPage::AS_END:
default:
- // $status came from WikiPage::doEdit()
+ // $status came from WikiPage::doEditContent()
$errors = $status->getErrorsArray();
$this->dieUsageMsg( $errors[0] ); // TODO: Add new errors to message map
break;
&& !$this->manager->getAuthenticationSessionData( 'no-email' )
) {
// TODO show 'confirmemail_oncreate'/'confirmemail_sendfailed' message
- wfGetDB( DB_MASTER )->onTransactionIdle( function () use ( $user ) {
- $user = $user->getInstanceForUpdate();
- $status = $user->sendConfirmationMail();
- $user->saveSettings();
- if ( !$status->isGood() ) {
- $this->logger->warning( 'Could not send confirmation email: ' .
- $status->getWikiText( false, false, 'en' ) );
- }
- } );
+ wfGetDB( DB_MASTER )->onTransactionIdle(
+ function () use ( $user ) {
+ $user = $user->getInstanceForUpdate();
+ $status = $user->sendConfirmationMail();
+ $user->saveSettings();
+ if ( !$status->isGood() ) {
+ $this->logger->warning( 'Could not send confirmation email: ' .
+ $status->getWikiText( false, false, 'en' ) );
+ }
+ },
+ __METHOD__
+ );
}
return AuthenticationResponse::newPass();
if ( $sendMail ) {
// Send email after DB commit
- $dbw->onTransactionIdle( function () use ( $req ) {
- /** @var TemporaryPasswordAuthenticationRequest $req */
- $this->sendPasswordResetEmail( $req );
- } );
+ $dbw->onTransactionIdle(
+ function () use ( $req ) {
+ /** @var TemporaryPasswordAuthenticationRequest $req */
+ $this->sendPasswordResetEmail( $req );
+ },
+ __METHOD__
+ );
}
}
if ( $mailpassword ) {
// Send email after DB commit
- wfGetDB( DB_MASTER )->onTransactionIdle( function () use ( $user, $creator, $req ) {
- $this->sendNewAccountEmail( $user, $creator, $req->password );
- } );
+ wfGetDB( DB_MASTER )->onTransactionIdle(
+ function () use ( $user, $creator, $req ) {
+ $this->sendNewAccountEmail( $user, $creator, $req->password );
+ },
+ __METHOD__
+ );
}
return $mailpassword ? 'byemail' : null;
if ( !$title || !$title instanceof Title ) {
global $wgTitle;
wfDebugLog( 'GlobalTitleFail', __METHOD__ . ' called by ' .
- wfGetAllCallers( 5 ) . ' with no title set.' );
+ wfGetAllCallers( 6 ) . ' with no title set.' );
$title = $wgTitle;
}
// Sometimes $wgTitle isn't set either...
) {
// @FIXME: This would be better as an extension hook
// Send emails or email jobs once this row is safely committed
- $dbw->onTransactionIdle( function () use ( $editor, $title ) {
- $enotif = new EmailNotification();
- $enotif->notifyOnPageChange(
- $editor,
- $title,
- $this->mAttribs['rc_timestamp'],
- $this->mAttribs['rc_comment'],
- $this->mAttribs['rc_minor'],
- $this->mAttribs['rc_last_oldid'],
- $this->mExtra['pageStatus']
- );
- } );
+ $dbw->onTransactionIdle(
+ function () use ( $editor, $title ) {
+ $enotif = new EmailNotification();
+ $enotif->notifyOnPageChange(
+ $editor,
+ $title,
+ $this->mAttribs['rc_timestamp'],
+ $this->mAttribs['rc_comment'],
+ $this->mAttribs['rc_minor'],
+ $this->mAttribs['rc_last_oldid'],
+ $this->mExtra['pageStatus']
+ );
+ },
+ __METHOD__
+ );
}
}
protected $mDBname;
/** @var array[] $aliases Map of (table => (dbname, schema, prefix) map) */
protected $tableAliases = [];
- /** @var bool */
+ /** @var bool Whether this PHP instance is for a CLI script */
protected $cliMode;
+ /** @var string Agent name for query profiling */
+ protected $agent;
/** @var BagOStuff APC cache */
protected $srvCache;
protected $mTrxPreCommitCallbacks = [];
/** @var array[] List of (callable, method name) */
protected $mTrxEndCallbacks = [];
- /** @var array[] Map of (name => (callable, method name)) */
+ /** @var callable[] Map of (name => callable) */
protected $mTrxRecurringCallbacks = [];
/** @var bool Whether to suppress triggering of transaction end callbacks */
protected $mTrxEndCallbacksSuppressed = false;
$this->cliMode = isset( $params['cliMode'] )
? $params['cliMode']
: ( PHP_SAPI === 'cli' );
+ $this->agent = isset( $params['agent'] )
+ ? str_replace( '/', '-', $params['agent'] ) // escape for comment
+ : '';
$this->mFlags = $flags;
if ( $this->mFlags & DBO_DEFAULT ) {
* @throws InvalidArgumentException If the database driver or extension cannot be found
*/
final public static function factory( $dbType, $p = [] ) {
- global $wgCommandLineMode;
-
$canonicalDBTypes = [
'mysql' => [ 'mysqli', 'mysql' ],
'postgres' => [],
$p['schema'] = isset( $defaultSchemas[$dbType] ) ? $defaultSchemas[$dbType] : null;
}
$p['foreign'] = isset( $p['foreign'] ) ? $p['foreign'] : false;
- $p['cliMode'] = $wgCommandLineMode;
$conn = new $class( $p );
if ( isset( $p['connLogger'] ) ) {
if ( isset( $p['errorLogger'] ) ) {
$conn->errorLogger = $p['errorLogger'];
} else {
- $conn->errorLogger = [ MWExceptionHandler::class, 'logException' ];
+ $conn->errorLogger = function ( Exception $e ) {
+ trigger_error( get_class( $e ) . ': ' . $e->getMessage(), E_WARNING );
+ };
}
} else {
$conn = null;
* - false to disable debugging
* - omitted or null to do nothing
*
- * @return bool|null Previous value of the flag
+ * @return bool Previous value of the flag
+ * @deprecated since 1.28; use setFlag()
*/
public function debug( $debug = null ) {
- return wfSetBit( $this->mFlags, DBO_DEBUG, $debug );
+ $res = $this->getFlag( DBO_DEBUG );
+ if ( $debug !== null ) {
+ $debug ? $this->setFlag( DBO_DEBUG ) : $this->clearFlag( DBO_DEBUG );
+ }
+
+ return $res;
}
public function bufferResults( $buffer = null ) {
- if ( is_null( $buffer ) ) {
- return !(bool)( $this->mFlags & DBO_NOBUFFER );
- } else {
- return !wfSetBit( $this->mFlags, DBO_NOBUFFER, !$buffer );
+ $res = !$this->getFlag( DBO_NOBUFFER );
+ if ( $buffer !== null ) {
+ $buffer ? $this->clearFlag( DBO_NOBUFFER ) : $this->setFlag( DBO_NOBUFFER );
}
+
+ return $res;
}
/**
* @return bool The previous value of the flag.
*/
protected function ignoreErrors( $ignoreErrors = null ) {
- return wfSetBit( $this->mFlags, DBO_IGNORE, $ignoreErrors );
+ $res = $this->getFlag( DBO_IGNORE );
+ if ( $ignoreErrors !== null ) {
+ $ignoreErrors ? $this->setFlag( DBO_IGNORE ) : $this->clearFlag( DBO_IGNORE );
+ }
+
+ return $res;
}
public function trxLevel() {
}
public function tablePrefix( $prefix = null ) {
- return wfSetVar( $this->mTablePrefix, $prefix );
+ $old = $this->mTablePrefix;
+ $this->mTablePrefix = $prefix;
+
+ return $old;
}
public function dbSchema( $schema = null ) {
- return wfSetVar( $this->mSchema, $schema );
+ $old = $this->mSchema;
+ $this->mSchema = $schema;
+
+ return $old;
}
/**
}
public function query( $sql, $fname = __METHOD__, $tempIgnore = false ) {
- global $wgUser;
-
$priorWritesPending = $this->writesOrCallbacksPending();
$this->mLastQuery = $sql;
$this->mDoneWrites = microtime( true );
}
- # Add a comment for easy SHOW PROCESSLIST interpretation
- if ( is_object( $wgUser ) && $wgUser->isItemLoaded( 'name' ) ) {
- $userName = $wgUser->getName();
- if ( mb_strlen( $userName ) > 15 ) {
- $userName = mb_substr( $userName, 0, 15 ) . '...';
- }
- $userName = str_replace( '/', '', $userName );
- } else {
- $userName = '';
- }
-
// Add trace comment to the begin of the sql string, right after the operator.
// Or, for one-word queries (like "BEGIN" or COMMIT") add it to the end (bug 42598)
- $commentedSql = preg_replace( '/\s|$/', " /* $fname $userName */ ", $sql, 1 );
+ $commentedSql = preg_replace( '/\s|$/', " /* $fname {$this->agent} */ ", $sql, 1 );
# Start implicit transactions that wrap the request if DBO_TRX is enabled
if ( !$this->mTrxLevel && $this->getFlag( DBO_TRX )
return false;
}
- final public function onTransactionResolution( callable $callback ) {
+ final public function onTransactionResolution( callable $callback, $fname = __METHOD__ ) {
if ( !$this->mTrxLevel ) {
throw new DBUnexpectedError( $this, "No transaction is active." );
}
- $this->mTrxEndCallbacks[] = [ $callback, wfGetCaller() ];
+ $this->mTrxEndCallbacks[] = [ $callback, $fname ];
}
- final public function onTransactionIdle( callable $callback ) {
- $this->mTrxIdleCallbacks[] = [ $callback, wfGetCaller() ];
+ final public function onTransactionIdle( callable $callback, $fname = __METHOD__ ) {
+ $this->mTrxIdleCallbacks[] = [ $callback, $fname ];
if ( !$this->mTrxLevel ) {
$this->runOnTransactionIdleCallbacks( self::TRIGGER_IDLE );
}
}
- final public function onTransactionPreCommitOrIdle( callable $callback ) {
+ final public function onTransactionPreCommitOrIdle( callable $callback, $fname = __METHOD__ ) {
if ( $this->mTrxLevel ) {
- $this->mTrxPreCommitCallbacks[] = [ $callback, wfGetCaller() ];
+ $this->mTrxPreCommitCallbacks[] = [ $callback, $fname ];
} else {
// If no transaction is active, then make one for this callback
$this->startAtomic( __METHOD__ );
final public function setTransactionListener( $name, callable $callback = null ) {
if ( $callback ) {
- $this->mTrxRecurringCallbacks[$name] = [ $callback, wfGetCaller() ];
+ $this->mTrxRecurringCallbacks[$name] = $callback;
} else {
unset( $this->mTrxRecurringCallbacks[$name] );
}
/** @var Exception $e */
$e = null; // first exception
- foreach ( $this->mTrxRecurringCallbacks as $callback ) {
+ foreach ( $this->mTrxRecurringCallbacks as $phpCallback ) {
try {
- list( $phpCallback ) = $callback;
$phpCallback( $trigger, $this );
} catch ( Exception $ex ) {
call_user_func( $this->errorLogger, $ex );
$this->mTrxAutomatic = ( $mode === self::TRANSACTION_INTERNAL );
$this->mTrxAutomaticAtomic = false;
$this->mTrxAtomicLevels = [];
- $this->mTrxShortId = wfRandomString( 12 );
+ $this->mTrxShortId = sprintf( '%06x', mt_rand( 0, 0xffffff ) );
$this->mTrxWriteDuration = 0.0;
$this->mTrxWriteQueryCount = 0;
$this->mTrxWriteAdjDuration = 0.0;
// There is a good chance an exception was thrown, causing any early return
// from the caller. Let any error handler get a chance to issue rollback().
// If there isn't one, let the error bubble up and trigger server-side rollback.
- $this->onTransactionResolution( function () use ( $lockKey, $fname ) {
- $this->unlock( $lockKey, $fname );
- } );
+ $this->onTransactionResolution(
+ function () use ( $lockKey, $fname ) {
+ $this->unlock( $lockKey, $fname );
+ },
+ $fname
+ );
} else {
$this->unlock( $lockKey, $fname );
}
} );
- $this->commit( __METHOD__, self::FLUSHING_INTERNAL );
+ $this->commit( $fname, self::FLUSHING_INTERNAL );
return $unlocker;
}
* @throws DBUnexpectedError
*/
protected function doQuery( $sql ) {
- if ( $this->debug() ) {
+ if ( $this->getFlag( DBO_DEBUG ) ) {
wfDebug( "SQL: [$sql]\n" );
}
$this->offset = 0;
* @TODO: inject objects via dependency framework
*/
public function __construct( array $conf ) {
+ global $wgCommandLineMode;
+
$defaults = [
'domain' => wfWikiID(),
'hostname' => wfHostname(),
$defaults['wanCache'] = $wCache;
}
+ $this->agent = isset( $params['agent'] ) ? $params['agent'] : '';
+ $this->cliMode = isset( $params['cliMode'] ) ? $params['cliMode'] : $wgCommandLineMode;
+
parent::__construct( $conf + $defaults );
}
$this->callback = $callback;
if ( $this->dbw->trxLevel() ) {
- $this->dbw->onTransactionResolution( [ $this, 'cancelOnRollback' ] );
+ $this->dbw->onTransactionResolution( [ $this, 'cancelOnRollback' ], $fname );
}
}
$this->callback = $callback;
if ( $this->dbw->trxLevel() ) {
- $this->dbw->onTransactionResolution( [ $this, 'cancelOnRollback' ] );
+ $this->dbw->onTransactionResolution( [ $this, 'cancelOnRollback' ], $fname );
}
}
// Commit and release the lock (if set)
ScopedCallback::consume( $scopedLock );
// Run post-commit hooks without DBO_TRX
- $this->getDB()->onTransactionIdle( function() {
- Hooks::run( 'LinksUpdateComplete', [ &$this ] );
- } );
+ $this->getDB()->onTransactionIdle(
+ function () {
+ Hooks::run( 'LinksUpdateComplete', [ &$this ] );
+ },
+ __METHOD__
+ );
}
/**
$this->fname = $fname;
if ( $dbw && $dbw->trxLevel() ) {
- $dbw->onTransactionResolution( [ $this, 'cancelOnRollback' ] );
+ $dbw->onTransactionResolution( [ $this, 'cancelOnRollback' ], $fname );
}
}
function invalidateImageRedirect( Title $title ) {
$key = $this->getSharedCacheKey( 'image_redirect', md5( $title->getDBkey() ) );
if ( $key ) {
- $this->getMasterDB()->onTransactionPreCommitOrIdle( function() use ( $key ) {
- ObjectCache::getMainWANInstance()->delete( $key );
- } );
+ $this->getMasterDB()->onTransactionPreCommitOrIdle(
+ function () use ( $key ) {
+ ObjectCache::getMainWANInstance()->delete( $key );
+ },
+ __METHOD__
+ );
}
}
return;
}
- $this->repo->getMasterDB()->onTransactionPreCommitOrIdle( function() use ( $key ) {
- ObjectCache::getMainWANInstance()->delete( $key );
- } );
+ $this->repo->getMasterDB()->onTransactionPreCommitOrIdle(
+ function () use ( $key ) {
+ ObjectCache::getMainWANInstance()->delete( $key );
+ },
+ __METHOD__
+ );
}
/**
}
// Release the lock *after* commit to avoid row-level contention.
// Make sure it triggers on rollback() as well as commit() (T132921).
- $dbw->onTransactionResolution( function () use ( $logger ) {
- $status = $this->releaseFileLock();
- if ( !$status->isGood() ) {
- $logger->error( "Failed to unlock '{file}'", [ 'file' => $this->name ] );
- }
- } );
+ $dbw->onTransactionResolution(
+ function () use ( $logger ) {
+ $status = $this->releaseFileLock();
+ if ( !$status->isGood() ) {
+ $logger->error( "Failed to unlock '{file}'", [ 'file' => $this->name ] );
+ }
+ },
+ __METHOD__
+ );
// Callers might care if the SELECT snapshot is safely fresh
$this->lockedOwnTrx = $makesTransaction;
}
$dbw->onTransactionIdle(
function () use ( $dbw, $jobs, $flags, $method ) {
$this->doBatchPushInternal( $dbw, $jobs, $flags, $method );
- }
+ },
+ __METHOD__
);
}
// jobs to become no-ops without any actual jobs that made them redundant.
$dbw = $this->getMasterDB();
$cache = $this->dupCache;
- $dbw->onTransactionIdle( function () use ( $cache, $params, $key, $dbw ) {
- $timestamp = $cache->get( $key ); // current last timestamp of this job
- if ( $timestamp && $timestamp >= $params['rootJobTimestamp'] ) {
- return true; // a newer version of this root job was enqueued
- }
+ $dbw->onTransactionIdle(
+ function () use ( $cache, $params, $key, $dbw ) {
+ $timestamp = $cache->get( $key ); // current last timestamp of this job
+ if ( $timestamp && $timestamp >= $params['rootJobTimestamp'] ) {
+ return true; // a newer version of this root job was enqueued
+ }
- // Update the timestamp of the last root job started at the location...
- return $cache->set( $key, $params['rootJobTimestamp'], JobQueueDB::ROOTJOB_TTL );
- } );
+ // Update the timestamp of the last root job started at the location...
+ return $cache->set( $key, $params['rootJobTimestamp'], JobQueueDB::ROOTJOB_TTL );
+ },
+ __METHOD__
+ );
return true;
}
* @author Aaron Schulz
* @ingroup JobQueue
*/
+use MediaWiki\MediaWikiServices;
/**
* Job for pruning recent changes
return; // already in progress
}
- $factory = wfGetLBFactory();
+ $factory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
$ticket = $factory->getEmptyTransactionTicket( __METHOD__ );
$cutoff = $dbw->timestamp( time() - $wgRCMaxAge );
do {
$dbw = wfGetDB( DB_MASTER );
// JobRunner uses DBO_TRX, but doesn't call begin/commit itself;
// onTransactionIdle() will run immediately since there is no trx.
- $dbw->onTransactionIdle( function() use ( $dbw, $days, $window ) {
- $factory = wfGetLBFactory();
- $ticket = $factory->getEmptyTransactionTicket( __METHOD__ );
- // Avoid disconnect/ping() cycle that makes locks fall off
- $dbw->setSessionOptions( [ 'connTimeout' => 900 ] );
-
- $lockKey = wfWikiID() . '-activeusers';
- if ( !$dbw->lock( $lockKey, __METHOD__, 1 ) ) {
- return; // exclusive update (avoids duplicate entries)
- }
-
- $nowUnix = time();
- // Get the last-updated timestamp for the cache
- $cTime = $dbw->selectField( 'querycache_info',
- 'qci_timestamp',
- [ 'qci_type' => 'activeusers' ]
- );
- $cTimeUnix = $cTime ? wfTimestamp( TS_UNIX, $cTime ) : 1;
-
- // Pick the date range to fetch from. This is normally from the last
- // update to till the present time, but has a limited window for sanity.
- // If the window is limited, multiple runs are need to fully populate it.
- $sTimestamp = max( $cTimeUnix, $nowUnix - $days * 86400 );
- $eTimestamp = min( $sTimestamp + $window, $nowUnix );
-
- // Get all the users active since the last update
- $res = $dbw->select(
- [ 'recentchanges' ],
- [ 'rc_user_text', 'lastedittime' => 'MAX(rc_timestamp)' ],
- [
- 'rc_user > 0', // actual accounts
- 'rc_type != ' . $dbw->addQuotes( RC_EXTERNAL ), // no wikidata
- 'rc_log_type IS NULL OR rc_log_type != ' . $dbw->addQuotes( 'newusers' ),
- 'rc_timestamp >= ' . $dbw->addQuotes( $dbw->timestamp( $sTimestamp ) ),
- 'rc_timestamp <= ' . $dbw->addQuotes( $dbw->timestamp( $eTimestamp ) )
- ],
- __METHOD__,
- [
- 'GROUP BY' => [ 'rc_user_text' ],
- 'ORDER BY' => 'NULL' // avoid filesort
- ]
- );
- $names = [];
- foreach ( $res as $row ) {
- $names[$row->rc_user_text] = $row->lastedittime;
- }
-
- // Rotate out users that have not edited in too long (according to old data set)
- $dbw->delete( 'querycachetwo',
- [
- 'qcc_type' => 'activeusers',
- 'qcc_value < ' . $dbw->addQuotes( $nowUnix - $days * 86400 ) // TS_UNIX
- ],
- __METHOD__
- );
+ $dbw->onTransactionIdle(
+ function () use ( $dbw, $days, $window ) {
+ $factory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
+ $ticket = $factory->getEmptyTransactionTicket( __METHOD__ );
+ // Avoid disconnect/ping() cycle that makes locks fall off
+ $dbw->setSessionOptions( [ 'connTimeout' => 900 ] );
+
+ $lockKey = wfWikiID() . '-activeusers';
+ if ( !$dbw->lock( $lockKey, __METHOD__, 1 ) ) {
+ return; // exclusive update (avoids duplicate entries)
+ }
- // Find which of the recently active users are already accounted for
- if ( count( $names ) ) {
- $res = $dbw->select( 'querycachetwo',
- [ 'user_name' => 'qcc_title' ],
+ $nowUnix = time();
+ // Get the last-updated timestamp for the cache
+ $cTime = $dbw->selectField( 'querycache_info',
+ 'qci_timestamp',
+ [ 'qci_type' => 'activeusers' ]
+ );
+ $cTimeUnix = $cTime ? wfTimestamp( TS_UNIX, $cTime ) : 1;
+
+ // Pick the date range to fetch from. This is normally from the last
+ // update to till the present time, but has a limited window for sanity.
+ // If the window is limited, multiple runs are need to fully populate it.
+ $sTimestamp = max( $cTimeUnix, $nowUnix - $days * 86400 );
+ $eTimestamp = min( $sTimestamp + $window, $nowUnix );
+
+ // Get all the users active since the last update
+ $res = $dbw->select(
+ [ 'recentchanges' ],
+ [ 'rc_user_text', 'lastedittime' => 'MAX(rc_timestamp)' ],
[
- 'qcc_type' => 'activeusers',
- 'qcc_namespace' => NS_USER,
- 'qcc_title' => array_keys( $names ) ],
- __METHOD__
+ 'rc_user > 0', // actual accounts
+ 'rc_type != ' . $dbw->addQuotes( RC_EXTERNAL ), // no wikidata
+ 'rc_log_type IS NULL OR rc_log_type != ' . $dbw->addQuotes( 'newusers' ),
+ 'rc_timestamp >= ' . $dbw->addQuotes( $dbw->timestamp( $sTimestamp ) ),
+ 'rc_timestamp <= ' . $dbw->addQuotes( $dbw->timestamp( $eTimestamp ) )
+ ],
+ __METHOD__,
+ [
+ 'GROUP BY' => [ 'rc_user_text' ],
+ 'ORDER BY' => 'NULL' // avoid filesort
+ ]
);
+ $names = [];
foreach ( $res as $row ) {
- unset( $names[$row->user_name] );
+ $names[$row->rc_user_text] = $row->lastedittime;
}
- }
- // Insert the users that need to be added to the list
- if ( count( $names ) ) {
- $newRows = [];
- foreach ( $names as $name => $lastEditTime ) {
- $newRows[] = [
+ // Rotate out users that have not edited in too long (according to old data set)
+ $dbw->delete( 'querycachetwo',
+ [
'qcc_type' => 'activeusers',
- 'qcc_namespace' => NS_USER,
- 'qcc_title' => $name,
- 'qcc_value' => wfTimestamp( TS_UNIX, $lastEditTime ),
- 'qcc_namespacetwo' => 0, // unused
- 'qcc_titletwo' => '' // unused
- ];
+ 'qcc_value < ' . $dbw->addQuotes( $nowUnix - $days * 86400 ) // TS_UNIX
+ ],
+ __METHOD__
+ );
+
+ // Find which of the recently active users are already accounted for
+ if ( count( $names ) ) {
+ $res = $dbw->select( 'querycachetwo',
+ [ 'user_name' => 'qcc_title' ],
+ [
+ 'qcc_type' => 'activeusers',
+ 'qcc_namespace' => NS_USER,
+ 'qcc_title' => array_keys( $names ) ],
+ __METHOD__
+ );
+ foreach ( $res as $row ) {
+ unset( $names[$row->user_name] );
+ }
}
- foreach ( array_chunk( $newRows, 500 ) as $rowBatch ) {
- $dbw->insert( 'querycachetwo', $rowBatch, __METHOD__ );
- $factory->commitAndWaitForReplication( __METHOD__, $ticket );
+
+ // Insert the users that need to be added to the list
+ if ( count( $names ) ) {
+ $newRows = [];
+ foreach ( $names as $name => $lastEditTime ) {
+ $newRows[] = [
+ 'qcc_type' => 'activeusers',
+ 'qcc_namespace' => NS_USER,
+ 'qcc_title' => $name,
+ 'qcc_value' => wfTimestamp( TS_UNIX, $lastEditTime ),
+ 'qcc_namespacetwo' => 0, // unused
+ 'qcc_titletwo' => '' // unused
+ ];
+ }
+ foreach ( array_chunk( $newRows, 500 ) as $rowBatch ) {
+ $dbw->insert( 'querycachetwo', $rowBatch, __METHOD__ );
+ $factory->commitAndWaitForReplication( __METHOD__, $ticket );
+ }
}
- }
- // If a transaction was already started, it might have an old
- // snapshot, so kludge the timestamp range back as needed.
- $asOfTimestamp = min( $eTimestamp, (int)$dbw->trxTimestamp() );
+ // If a transaction was already started, it might have an old
+ // snapshot, so kludge the timestamp range back as needed.
+ $asOfTimestamp = min( $eTimestamp, (int)$dbw->trxTimestamp() );
- // Touch the data freshness timestamp
- $dbw->replace( 'querycache_info',
- [ 'qci_type' ],
- [ 'qci_type' => 'activeusers',
- 'qci_timestamp' => $dbw->timestamp( $asOfTimestamp ) ], // not always $now
- __METHOD__
- );
+ // Touch the data freshness timestamp
+ $dbw->replace( 'querycache_info',
+ [ 'qci_type' ],
+ [ 'qci_type' => 'activeusers',
+ 'qci_timestamp' => $dbw->timestamp( $asOfTimestamp ) ], // not always $now
+ __METHOD__
+ );
- $dbw->unlock( $lockKey, __METHOD__ );
- } );
+ $dbw->unlock( $lockKey, __METHOD__ );
+ },
+ __METHOD__
+ );
}
}
return;
}
- $dbw->onTransactionIdle( function() use ( $dbw, $namespace, $dbkeys ) {
- $services = MediaWikiServices::getInstance();
- $lbFactory = $services->getDBLoadBalancerFactory();
- // Determine which pages need to be updated.
- // This is necessary to prevent the job queue from smashing the DB with
- // large numbers of concurrent invalidations of the same page.
- $now = $dbw->timestamp();
- $ids = $dbw->selectFieldValues(
- 'page',
- 'page_id',
- [
- 'page_namespace' => $namespace,
- 'page_title' => $dbkeys,
- 'page_touched < ' . $dbw->addQuotes( $now )
- ],
- __METHOD__
- );
-
- if ( !$ids ) {
- return;
- }
-
- $batchSize = $services->getMainConfig()->get( 'UpdateRowsPerQuery' );
- $ticket = $lbFactory->getEmptyTransactionTicket( __METHOD__ );
- foreach ( array_chunk( $ids, $batchSize ) as $idBatch ) {
- $dbw->update(
+ $dbw->onTransactionIdle(
+ function () use ( $dbw, $namespace, $dbkeys ) {
+ $services = MediaWikiServices::getInstance();
+ $lbFactory = $services->getDBLoadBalancerFactory();
+ // Determine which pages need to be updated.
+ // This is necessary to prevent the job queue from smashing the DB with
+ // large numbers of concurrent invalidations of the same page.
+ $now = $dbw->timestamp();
+ $ids = $dbw->selectFieldValues(
'page',
- [ 'page_touched' => $now ],
+ 'page_id',
[
- 'page_id' => $idBatch,
- 'page_touched < ' . $dbw->addQuotes( $now ) // handle races
+ 'page_namespace' => $namespace,
+ 'page_title' => $dbkeys,
+ 'page_touched < ' . $dbw->addQuotes( $now )
],
__METHOD__
);
- $lbFactory->commitAndWaitForReplication( __METHOD__, $ticket );
- }
- } );
+
+ if ( !$ids ) {
+ return;
+ }
+
+ $batchSize = $services->getMainConfig()->get( 'UpdateRowsPerQuery' );
+ $ticket = $lbFactory->getEmptyTransactionTicket( __METHOD__ );
+ foreach ( array_chunk( $ids, $batchSize ) as $idBatch ) {
+ $dbw->update(
+ 'page',
+ [ 'page_touched' => $now ],
+ [
+ 'page_id' => $idBatch,
+ 'page_touched < ' . $dbw->addQuotes( $now ) // handle races
+ ],
+ __METHOD__
+ );
+ $lbFactory->commitAndWaitForReplication( __METHOD__, $ticket );
+ }
+ },
+ __METHOD__
+ );
}
}
return $this->__call( __FUNCTION__, func_get_args() );
}
- public function onTransactionResolution( callable $callback ) {
+ public function onTransactionResolution( callable $callback, $fname = __METHOD__ ) {
return $this->__call( __FUNCTION__, func_get_args() );
}
- public function onTransactionIdle( callable $callback ) {
+ public function onTransactionIdle( callable $callback, $fname = __METHOD__ ) {
return $this->__call( __FUNCTION__, func_get_args() );
}
- public function onTransactionPreCommitOrIdle( callable $callback ) {
+ public function onTransactionPreCommitOrIdle( callable $callback, $fname = __METHOD__ ) {
return $this->__call( __FUNCTION__, func_get_args() );
}
* - How the transaction ended (IDatabase::TRIGGER_COMMIT or IDatabase::TRIGGER_ROLLBACK)
*
* @param callable $callback
+ * @param string $fname Caller name
* @return mixed
* @since 1.28
*/
- public function onTransactionResolution( callable $callback );
+ public function onTransactionResolution( callable $callback, $fname = __METHOD__ );
/**
* Run a callback as soon as there is no transaction pending.
* - How the transaction ended (IDatabase::TRIGGER_COMMIT or IDatabase::TRIGGER_IDLE)
*
* @param callable $callback
+ * @param string $fname Caller name
* @since 1.20
*/
- public function onTransactionIdle( callable $callback );
+ public function onTransactionIdle( callable $callback, $fname = __METHOD__ );
/**
* Run a callback before the current transaction commits or now if there is none.
* Updates will execute in the order they were enqueued.
*
* @param callable $callback
+ * @param string $fname Caller name
* @since 1.22
*/
- public function onTransactionPreCommitOrIdle( callable $callback );
+ public function onTransactionPreCommitOrIdle( callable $callback, $fname = __METHOD__ );
/**
* Run a callback each time any transaction commits or rolls back
--- /dev/null
+<?php
+
+/**@{
+ * Database related constants
+ */
+define( 'DBO_DEBUG', 1 );
+define( 'DBO_NOBUFFER', 2 );
+define( 'DBO_IGNORE', 4 );
+define( 'DBO_TRX', 8 ); // automatically start transaction on first query
+define( 'DBO_DEFAULT', 16 );
+define( 'DBO_PERSISTENT', 32 );
+define( 'DBO_SYSDBA', 64 ); // for oracle maintenance
+define( 'DBO_DDLMODE', 128 ); // when using schema files: mostly for Oracle
+define( 'DBO_SSL', 256 );
+define( 'DBO_COMPRESS', 512 );
+/**@}*/
+
+/**@{
+ * Valid database indexes
+ * Operation-based indexes
+ */
+define( 'DB_REPLICA', -1 ); # Read from a replica (or only server)
+define( 'DB_MASTER', -2 ); # Write to master (or only server)
+/**@}*/
+
+/**@{
+ * Flags for IDatabase::makeList()
+ * These are also available as Database class constants
+ */
+define( 'LIST_COMMA', 0 );
+define( 'LIST_AND', 1 );
+define( 'LIST_SET', 2 );
+define( 'LIST_NAMES', 3 );
+define( 'LIST_OR', 4 );
+/**@}*/
/** @var callable[] */
protected $replicationWaitCallbacks = [];
+ /** @var bool Whether this PHP instance is for a CLI script */
+ protected $cliMode;
+ /** @var string Agent name for query profiling */
+ protected $agent;
+
const SHUTDOWN_NO_CHRONPROT = 0; // don't save DB positions at all
const SHUTDOWN_CHRONPROT_ASYNC = 1; // save DB positions, but don't wait on remote DCs
const SHUTDOWN_CHRONPROT_SYNC = 2; // save DB positions, waiting on all DCs
*/
public function __construct( array $conf ) {
$this->domain = isset( $conf['domain'] ) ? $conf['domain'] : '';
+
if ( isset( $conf['readOnlyReason'] ) && is_string( $conf['readOnlyReason'] ) ) {
$this->readOnlyReason = $conf['readOnlyReason'];
}
$this->errorLogger = isset( $conf['errorLogger'] )
? $conf['errorLogger']
: function ( Exception $e ) {
- trigger_error( E_WARNING, $e->getMessage() );
+ trigger_error( E_WARNING, get_class( $e ) . ': ' . $e->getMessage() );
};
$this->hostname = isset( $conf['hostname'] )
? $conf['hostname']
: new TransactionProfiler();
$this->ticket = mt_rand();
+ $this->cliMode = isset( $params['cliMode'] ) ? $params['cliMode'] : PHP_SAPI === 'cli';
+ $this->agent = isset( $params['agent'] ) ? $params['agent'] : '';
}
/**
isset( $_GET['cpPosTime'] ) ? $_GET['cpPosTime'] : null
);
$chronProt->setLogger( $this->replLogger );
- if ( PHP_SAPI === 'cli' ) {
+ if ( $this->cliMode ) {
$chronProt->setEnabled( false );
}
'connLogger' => $this->connLogger,
'replLogger' => $this->replLogger,
'errorLogger' => $this->errorLogger,
- 'hostname' => $this->hostname
+ 'hostname' => $this->hostname,
+ 'cliMode' => $this->cliMode,
+ 'agent' => $this->agent
];
}
public function closeAll() {
$this->forEachLBCallMethod( 'closeAll', [] );
}
+
+ /**
+ * @param string $agent Agent name for query profiling
+ * @since 1.28
+ */
+ public function setAgentName( $agent ) {
+ $this->agent = $agent;
+ }
}
/** @var array[] $aliases Map of (table => (dbname, schema, prefix) map) */
private $tableAliases = [];
- /** @var LoadMonitor */
+ /** @var ILoadMonitor */
private $mLoadMonitor;
/** @var BagOStuff */
private $srvCache;
private $localDomain;
/** @var string Current server name */
private $host;
+ /** @var bool Whether this PHP instance is for a CLI script */
+ protected $cliMode;
+ /** @var string Agent name for query profiling */
+ protected $agent;
/** @var callable Exception logger */
private $errorLogger;
$this->errorLogger = isset( $params['errorLogger'] )
? $params['errorLogger']
: function ( Exception $e ) {
- trigger_error( E_WARNING, $e->getMessage() );
+ trigger_error( get_class( $e ) . ': ' . $e->getMessage(), E_WARNING );
};
foreach ( [ 'replLogger', 'connLogger', 'queryLogger', 'perfLogger' ] as $key ) {
$this->host = isset( $params['hostname'] )
? $params['hostname']
: ( gethostname() ?: 'unknown' );
+ $this->cliMode = isset( $params['cliMode'] ) ? $params['cliMode'] : PHP_SAPI === 'cli';
+ $this->agent = isset( $params['agent'] ) ? $params['agent'] : '';
}
/**
* Get a LoadMonitor instance
*
- * @return LoadMonitor
+ * @return ILoadMonitor
*/
private function getLoadMonitor() {
if ( !isset( $this->mLoadMonitor ) ) {
$server['connLogger'] = $this->connLogger;
$server['queryLogger'] = $this->queryLogger;
$server['trxProfiler'] = $this->trxProfiler;
+ $server['cliMode'] = $this->cliMode;
+ $server['errorLogger'] = $this->errorLogger;
+ $server['agent'] = $this->agent;
// Create a live connection object
try {
--- /dev/null
+<?php
+/**
+ * Database load monitoring interface
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Database
+ */
+use Psr\Log\LoggerAwareInterface;
+
+/**
+ * An interface for database load monitoring
+ *
+ * @ingroup Database
+ */
+interface ILoadMonitor extends LoggerAwareInterface {
+ /**
+ * Construct a new LoadMonitor with a given LoadBalancer parent
+ *
+ * @param ILoadBalancer $lb LoadBalancer this instance serves
+ * @param BagOStuff $sCache Local server memory cache
+ * @param BagOStuff $cCache Local cluster memory cache
+ */
+ public function __construct( ILoadBalancer $lb, BagOStuff $sCache, BagOStuff $cCache );
+
+ /**
+ * Perform pre-connection load ratio adjustment.
+ * @param int[] &$loads
+ * @param string|bool $group The selected query group. Default: false
+ * @param string|bool $domain Default: false
+ */
+ public function scaleLoads( &$loads, $group = false, $domain = false );
+
+ /**
+ * Get an estimate of replication lag (in seconds) for each server
+ *
+ * Values may be "false" if replication is too broken to estimate
+ *
+ * @param integer[] $serverIndexes
+ * @param string $domain
+ *
+ * @return array Map of (server index => float|int|bool)
+ */
+ public function getLagTimes( $serverIndexes, $domain );
+
+ /**
+ * Clear any process and persistent cache of lag times
+ * @since 1.27
+ */
+ public function clearCaches();
+}
<?php
/**
- * Database load monitoring.
- *
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* @file
* @ingroup Database
*/
-use Psr\Log\LoggerAwareInterface;
+
+use Psr\Log\LoggerInterface;
/**
- * An interface for database load monitoring
+ * Basic DB load monitor with no external dependencies
+ * Uses memcached to cache the replication lag for a short time
*
* @ingroup Database
*/
-interface LoadMonitor extends LoggerAwareInterface {
- /**
- * Construct a new LoadMonitor with a given LoadBalancer parent
- *
- * @param ILoadBalancer $lb LoadBalancer this instance serves
- * @param BagOStuff $sCache Local server memory cache
- * @param BagOStuff $cCache Local cluster memory cache
- */
- public function __construct( ILoadBalancer $lb, BagOStuff $sCache, BagOStuff $cCache );
-
- /**
- * Perform pre-connection load ratio adjustment.
- * @param int[] &$loads
- * @param string|bool $group The selected query group. Default: false
- * @param string|bool $domain Default: false
- */
- public function scaleLoads( &$loads, $group = false, $domain = false );
-
- /**
- * Get an estimate of replication lag (in seconds) for each server
- *
- * Values may be "false" if replication is too broken to estimate
- *
- * @param integer[] $serverIndexes
- * @param string $domain
- *
- * @return array Map of (server index => float|int|bool)
- */
- public function getLagTimes( $serverIndexes, $domain );
-
- /**
- * Clear any process and persistent cache of lag times
- * @since 1.27
- */
- public function clearCaches();
+class LoadMonitor implements ILoadMonitor {
+ /** @var ILoadBalancer */
+ protected $parent;
+ /** @var BagOStuff */
+ protected $srvCache;
+ /** @var BagOStuff */
+ protected $mainCache;
+ /** @var LoggerInterface */
+ protected $replLogger;
+
+ public function __construct( ILoadBalancer $lb, BagOStuff $srvCache, BagOStuff $cache ) {
+ $this->parent = $lb;
+ $this->srvCache = $srvCache;
+ $this->mainCache = $cache;
+ $this->replLogger = new \Psr\Log\NullLogger();
+ }
+
+ public function setLogger( LoggerInterface $logger ) {
+ $this->replLogger = $logger;
+ }
+
+ public function scaleLoads( &$loads, $group = false, $domain = false ) {
+ }
+
+ public function getLagTimes( $serverIndexes, $domain ) {
+ if ( count( $serverIndexes ) == 1 && reset( $serverIndexes ) == 0 ) {
+ # Single server only, just return zero without caching
+ return [ 0 => 0 ];
+ }
+
+ $key = $this->getLagTimeCacheKey();
+ # Randomize TTLs to reduce stampedes (4.0 - 5.0 sec)
+ $ttl = mt_rand( 4e6, 5e6 ) / 1e6;
+ # Keep keys around longer as fallbacks
+ $staleTTL = 60;
+
+ # (a) Check the local APC cache
+ $value = $this->srvCache->get( $key );
+ if ( $value && $value['timestamp'] > ( microtime( true ) - $ttl ) ) {
+ $this->replLogger->debug( __METHOD__ . ": got lag times ($key) from local cache" );
+ return $value['lagTimes']; // cache hit
+ }
+ $staleValue = $value ?: false;
+
+ # (b) Check the shared cache and backfill APC
+ $value = $this->mainCache->get( $key );
+ if ( $value && $value['timestamp'] > ( microtime( true ) - $ttl ) ) {
+ $this->srvCache->set( $key, $value, $staleTTL );
+ $this->replLogger->debug( __METHOD__ . ": got lag times ($key) from main cache" );
+
+ return $value['lagTimes']; // cache hit
+ }
+ $staleValue = $value ?: $staleValue;
+
+ # (c) Cache key missing or expired; regenerate and backfill
+ if ( $this->mainCache->lock( $key, 0, 10 ) ) {
+ # Let this process alone update the cache value
+ $cache = $this->mainCache;
+ /** @noinspection PhpUnusedLocalVariableInspection */
+ $unlocker = new ScopedCallback( function () use ( $cache, $key ) {
+ $cache->unlock( $key );
+ } );
+ } elseif ( $staleValue ) {
+ # Could not acquire lock but an old cache exists, so use it
+ return $staleValue['lagTimes'];
+ }
+
+ $lagTimes = [];
+ foreach ( $serverIndexes as $i ) {
+ if ( $i == $this->parent->getWriterIndex() ) {
+ $lagTimes[$i] = 0; // master always has no lag
+ continue;
+ }
+
+ $conn = $this->parent->getAnyOpenConnection( $i );
+ if ( $conn ) {
+ $close = false; // already open
+ } else {
+ $conn = $this->parent->openConnection( $i, $domain );
+ $close = true; // new connection
+ }
+
+ if ( !$conn ) {
+ $lagTimes[$i] = false;
+ $host = $this->parent->getServerName( $i );
+ $this->replLogger->error( __METHOD__ . ": host $host (#$i) is unreachable" );
+ continue;
+ }
+
+ $lagTimes[$i] = $conn->getLag();
+ if ( $lagTimes[$i] === false ) {
+ $host = $this->parent->getServerName( $i );
+ $this->replLogger->error( __METHOD__ . ": host $host (#$i) is not replicating?" );
+ }
+
+ if ( $close ) {
+ # Close the connection to avoid sleeper connections piling up.
+ # Note that the caller will pick one of these DBs and reconnect,
+ # which is slightly inefficient, but this only matters for the lag
+ # time cache miss cache, which is far less common that cache hits.
+ $this->parent->closeConnection( $conn );
+ }
+ }
+
+ # Add a timestamp key so we know when it was cached
+ $value = [ 'lagTimes' => $lagTimes, 'timestamp' => microtime( true ) ];
+ $this->mainCache->set( $key, $value, $staleTTL );
+ $this->srvCache->set( $key, $value, $staleTTL );
+ $this->replLogger->info( __METHOD__ . ": re-calculated lag times ($key)" );
+
+ return $value['lagTimes'];
+ }
+
+ public function clearCaches() {
+ $key = $this->getLagTimeCacheKey();
+ $this->srvCache->delete( $key );
+ $this->mainCache->delete( $key );
+ }
+
+ private function getLagTimeCacheKey() {
+ $writerIndex = $this->parent->getWriterIndex();
+ // Lag is per-server, not per-DB, so key on the master DB name
+ return $this->srvCache->makeGlobalKey(
+ 'lag-times',
+ $this->parent->getServerName( $writerIndex )
+ );
+ }
}
* @ingroup Database
*/
-use Psr\Log\LoggerInterface;
-
/**
* Basic MySQL load monitor with no external dependencies
* Uses memcached to cache the replication lag for a short time
*
* @ingroup Database
*/
-class LoadMonitorMySQL implements LoadMonitor {
- /** @var ILoadBalancer */
- protected $parent;
- /** @var BagOStuff */
- protected $srvCache;
- /** @var BagOStuff */
- protected $mainCache;
- /** @var LoggerInterface */
- protected $replLogger;
-
- public function __construct( ILoadBalancer $lb, BagOStuff $srvCache, BagOStuff $cache ) {
- $this->parent = $lb;
- $this->srvCache = $srvCache;
- $this->mainCache = $cache;
- $this->replLogger = new \Psr\Log\NullLogger();
- }
-
- public function setLogger( LoggerInterface $logger ) {
- $this->replLogger = $logger;
- }
-
- public function scaleLoads( &$loads, $group = false, $wiki = false ) {
- }
-
- public function getLagTimes( $serverIndexes, $wiki ) {
- if ( count( $serverIndexes ) == 1 && reset( $serverIndexes ) == 0 ) {
- # Single server only, just return zero without caching
- return [ 0 => 0 ];
- }
-
- $key = $this->getLagTimeCacheKey();
- # Randomize TTLs to reduce stampedes (4.0 - 5.0 sec)
- $ttl = mt_rand( 4e6, 5e6 ) / 1e6;
- # Keep keys around longer as fallbacks
- $staleTTL = 60;
-
- # (a) Check the local APC cache
- $value = $this->srvCache->get( $key );
- if ( $value && $value['timestamp'] > ( microtime( true ) - $ttl ) ) {
- $this->replLogger->debug( __METHOD__ . ": got lag times ($key) from local cache" );
- return $value['lagTimes']; // cache hit
- }
- $staleValue = $value ?: false;
-
- # (b) Check the shared cache and backfill APC
- $value = $this->mainCache->get( $key );
- if ( $value && $value['timestamp'] > ( microtime( true ) - $ttl ) ) {
- $this->srvCache->set( $key, $value, $staleTTL );
- $this->replLogger->debug( __METHOD__ . ": got lag times ($key) from main cache" );
-
- return $value['lagTimes']; // cache hit
- }
- $staleValue = $value ?: $staleValue;
-
- # (c) Cache key missing or expired; regenerate and backfill
- if ( $this->mainCache->lock( $key, 0, 10 ) ) {
- # Let this process alone update the cache value
- $cache = $this->mainCache;
- /** @noinspection PhpUnusedLocalVariableInspection */
- $unlocker = new ScopedCallback( function () use ( $cache, $key ) {
- $cache->unlock( $key );
- } );
- } elseif ( $staleValue ) {
- # Could not acquire lock but an old cache exists, so use it
- return $staleValue['lagTimes'];
- }
-
- $lagTimes = [];
- foreach ( $serverIndexes as $i ) {
- if ( $i == $this->parent->getWriterIndex() ) {
- $lagTimes[$i] = 0; // master always has no lag
- continue;
- }
-
- $conn = $this->parent->getAnyOpenConnection( $i );
- if ( $conn ) {
- $close = false; // already open
- } else {
- $conn = $this->parent->openConnection( $i, $wiki );
- $close = true; // new connection
- }
-
- if ( !$conn ) {
- $lagTimes[$i] = false;
- $host = $this->parent->getServerName( $i );
- $this->replLogger->error( __METHOD__ . ": host $host (#$i) is unreachable" );
- continue;
- }
-
- $lagTimes[$i] = $conn->getLag();
- if ( $lagTimes[$i] === false ) {
- $host = $this->parent->getServerName( $i );
- $this->replLogger->error( __METHOD__ . ": host $host (#$i) is not replicating?" );
- }
-
- if ( $close ) {
- # Close the connection to avoid sleeper connections piling up.
- # Note that the caller will pick one of these DBs and reconnect,
- # which is slightly inefficient, but this only matters for the lag
- # time cache miss cache, which is far less common that cache hits.
- $this->parent->closeConnection( $conn );
- }
- }
-
- # Add a timestamp key so we know when it was cached
- $value = [ 'lagTimes' => $lagTimes, 'timestamp' => microtime( true ) ];
- $this->mainCache->set( $key, $value, $staleTTL );
- $this->srvCache->set( $key, $value, $staleTTL );
- $this->replLogger->info( __METHOD__ . ": re-calculated lag times ($key)" );
-
- return $value['lagTimes'];
- }
-
- public function clearCaches() {
- $key = $this->getLagTimeCacheKey();
- $this->srvCache->delete( $key );
- $this->mainCache->delete( $key );
- }
-
- private function getLagTimeCacheKey() {
- $writerIndex = $this->parent->getWriterIndex();
- // Lag is per-server, not per-DB, so key on the master DB name
- return $this->srvCache->makeGlobalKey(
- 'lag-times',
- $this->parent->getServerName( $writerIndex )
- );
+class LoadMonitorMySQL extends LoadMonitor {
+ public function scaleLoads( &$loads, $group = false, $domain = false ) {
+ // @TODO: maybe use Threads_running/Threads_created ratio to guess load
+ // and Queries/Uptime to guess if a server is warming up the buffer pool
}
}
*/
use Psr\Log\LoggerInterface;
-class LoadMonitorNull implements LoadMonitor {
+class LoadMonitorNull implements ILoadMonitor {
public function __construct( ILoadBalancer $lb, BagOStuff $sCache, BagOStuff $cCache ) {
}
$logEntry->setComment( $reason );
$logid = $logEntry->insert();
- $dbw->onTransactionPreCommitOrIdle( function () use ( $dbw, $logEntry, $logid ) {
- // Bug 56776: avoid deadlocks (especially from FileDeleteForm)
- $logEntry->publish( $logid );
- } );
+ $dbw->onTransactionPreCommitOrIdle(
+ function () use ( $dbw, $logEntry, $logid ) {
+ // Bug 56776: avoid deadlocks (especially from FileDeleteForm)
+ $logEntry->publish( $logid );
+ },
+ __METHOD__
+ );
$dbw->endAtomic( __METHOD__ );
$cat->refreshCounts();
}
}
- }
+ },
+ __METHOD__
);
}
*/
public function msg() {
return call_user_func_array( 'wfMessage', func_get_args() )
- ->inLanguage( $this->getLanguage() );
+ ->inLanguage( $this->getLanguage() )
+ // Use a dummy title because there is no real title
+ // for this endpoint, and the cache won't vary on it
+ // anyways.
+ ->title( Title::newFromText( 'Dwimmerlaik' ) );
}
/**
);
if ( $dbw->trxLevel() ) {
- $dbw->onTransactionResolution( function () use ( &$scopeLock ) {
- ScopedCallback::consume( $scopeLock ); // release after commit
- } );
+ $dbw->onTransactionResolution(
+ function () use ( &$scopeLock ) {
+ ScopedCallback::consume( $scopeLock ); // release after commit
+ },
+ __METHOD__
+ );
}
}
} catch ( Exception $e ) {
}
$dbw->startAtomic( __METHOD__ );
- $dbw->onTransactionResolution( function () {
- // Release locks on commit or error
- $this->releaseItemLocks();
- } );
+ $dbw->onTransactionResolution(
+ function () {
+ // Release locks on commit or error
+ $this->releaseItemLocks();
+ },
+ __METHOD__
+ );
$missing = array_flip( $this->ids );
$this->clearFileOps();
--- /dev/null
+<?php
+
+/**
+ * Augment search result set with values of certain page props.
+ */
+class AugmentPageProps implements ResultSetAugmentor {
+ /**
+ * @var array List of properties.
+ */
+ private $propnames;
+
+ public function __construct( $propnames ) {
+ $this->propnames = $propnames;
+ }
+
+ public function augmentAll( SearchResultSet $resultSet ) {
+ $titles = $resultSet->extractTitles();
+ return PageProps::getInstance()->getProperties( $titles, $this->propnames );
+ }
+}
--- /dev/null
+<?php
+
+/**
+ * Perform augmentation of each row and return composite result,
+ * indexed by ID.
+ */
+class PerRowAugmentor implements ResultSetAugmentor {
+
+ /**
+ * @var ResultAugmentor
+ */
+ private $rowAugmentor;
+
+ /**
+ * PerRowAugmentor constructor.
+ * @param ResultAugmentor $augmentor Per-result augmentor to use.
+ */
+ public function __construct( ResultAugmentor $augmentor ) {
+ $this->rowAugmentor = $augmentor;
+ }
+
+ /**
+ * Produce data to augment search result set.
+ * @param SearchResultSet $resultSet
+ * @return array Data for all results
+ */
+ public function augmentAll( SearchResultSet $resultSet ) {
+ $data = [];
+ foreach ( $resultSet->extractResults() as $result ) {
+ $id = $result->getTitle()->getArticleID();
+ if ( !$id ) {
+ continue;
+ }
+ $data[$id] = $this->rowAugmentor->augment( $result );
+ }
+ return $data;
+ }
+}
--- /dev/null
+<?php
+
+/**
+ * Augment search results.
+ *
+ */
+interface ResultAugmentor {
+ /**
+ * Produce data to augment search result set.
+ * @param SearchResult $result
+ * @return mixed Data for this result
+ */
+ public function augment( SearchResult $result );
+}
--- /dev/null
+<?php
+
+/**
+ * Augment search results.
+ *
+ */
+interface ResultSetAugmentor {
+ /**
+ * Produce data to augment search result set.
+ * @param SearchResultSet $resultSet
+ * @return array Data for all results
+ */
+ public function augmentAll( SearchResultSet $resultSet );
+}
Hooks::run( 'SearchIndexFields', [ &$fields, $this ] );
return $fields;
}
+
+ /**
+ * Augment search results with extra data.
+ *
+ * @param SearchResultSet $resultSet
+ */
+ public function augmentSearchResults( SearchResultSet $resultSet ) {
+ $setAugmentors = [];
+ $rowAugmentors = [];
+ Hooks::run( "SearchResultsAugment", [ &$setAugmentors, &$rowAugmentors ] );
+
+ if ( !$setAugmentors && !$rowAugmentors ) {
+ // We're done here
+ return;
+ }
+
+ // Convert row augmentors to set augmentor
+ foreach ( $rowAugmentors as $name => $row ) {
+ if ( isset( $setAugmentors[$name] ) ) {
+ throw new InvalidArgumentException( "Both row and set augmentors are defined for $name" );
+ }
+ $setAugmentors[$name] = new PerRowAugmentor( $row );
+ }
+
+ foreach ( $setAugmentors as $name => $augmentor ) {
+ $data = $augmentor->augmentAll( $resultSet );
+ if ( $data ) {
+ $resultSet->setAugmentedData( $name, $data );
+ }
+ }
+ }
}
/**
return false;
}
$this->fetched = true;
- return SearchResult::newFromTitle( $this->result );
+ return SearchResult::newFromTitle( $this->result, $this );
}
public function rewind() {
*/
protected $searchEngine;
+ /**
+ * A set of extension data.
+ * @var array[]
+ */
+ protected $extensionData;
+
/**
* Return a new SearchResult and initializes it with a title.
*
- * @param Title $title
+ * @param Title $title
+ * @param SearchResultSet $parentSet
* @return SearchResult
*/
- public static function newFromTitle( $title ) {
+ public static function newFromTitle( $title, SearchResultSet $parentSet = null ) {
$result = new static();
$result->initFromTitle( $title );
+ if ( $parentSet ) {
+ $parentSet->augmentResult( $result );
+ }
return $result;
}
function isFileMatch() {
return false;
}
+
+ /**
+ * Get the extension data as:
+ * augmentor name => data
+ * @return array[]
+ */
+ public function getExtensionData() {
+ return $this->extensionData;
+ }
+
+ /**
+ * Set extension data for this result.
+ * The data is:
+ * augmentor name => data
+ * @param array[] $extensionData
+ */
+ public function setExtensionData( array $extensionData ) {
+ $this->extensionData = $extensionData;
+ }
+
}
protected $containedSyntax = false;
+ /**
+ * Cache of titles.
+ * Lists titles of the result set, in the same order as results.
+ * @var Title[]
+ */
+ private $titles;
+
+ /**
+ * Cache of results - serialization of the result iterator
+ * as an array.
+ * @var SearchResult[]
+ */
+ private $results;
+
+ /**
+ * Set of result's extra data, indexed per result id
+ * and then per data item name.
+ * The structure is:
+ * PAGE_ID => [ augmentor name => data, ... ]
+ * @var array[]
+ */
+ protected $extraData = [];
+
public function __construct( $containedSyntax = false ) {
$this->containedSyntax = $containedSyntax;
}
/**
* Fetches next search result, or false.
* STUB
- *
- * @return SearchResult
+ * FIXME: refactor as iterator, so we could use nicer interfaces.
+ * @return SearchResult|false
*/
function next() {
return false;
}
/**
- * Rewind result set back to begining
+ * Rewind result set back to beginning
*/
function rewind() {
}
public function searchContainedSyntax() {
return $this->containedSyntax;
}
+
+ /**
+ * Extract all the results in the result set as array.
+ * @return SearchResult[]
+ */
+ public function extractResults() {
+ if ( is_null( $this->results ) ) {
+ $this->results = [];
+ if ( $this->numRows() == 0 ) {
+ // Don't bother if we've got empty result
+ return $this->results;
+ }
+ $this->rewind();
+ while ( ( $result = $this->next() ) != false ) {
+ $this->results[] = $result;
+ }
+ $this->rewind();
+ }
+ return $this->results;
+ }
+
+ /**
+ * Extract all the titles in the result set.
+ * @return Title[]
+ */
+ public function extractTitles() {
+ if ( is_null( $this->titles ) ) {
+ if ( $this->numRows() == 0 ) {
+ // Don't bother if we've got empty result
+ $this->titles = [];
+ } else {
+ $this->titles = array_map(
+ function ( SearchResult $result ) {
+ return $result->getTitle();
+ },
+ $this->extractResults() );
+ }
+ }
+ return $this->titles;
+ }
+
+ /**
+ * Sets augmented data for result set.
+ * @param string $name Extra data item name
+ * @param array[] $data Extra data as PAGEID => data
+ */
+ public function setAugmentedData( $name, $data ) {
+ foreach ( $data as $id => $resultData ) {
+ $this->extraData[$id][$name] = $resultData;
+ }
+ }
+
+ /**
+ * Returns extra data for specific result and store it in SearchResult object.
+ * @param SearchResult $result
+ * @return array|null List of data as name => value or null if none present.
+ */
+ public function augmentResult( SearchResult $result ) {
+ $id = $result->getTitle()->getArticleID();
+ if ( !$id || !isset( $this->extraData[$id] ) ) {
+ return null;
+ }
+ $result->setExtensionData( $this->extraData[$id] );
+ return $this->extraData[$id];
+ }
}
}
return SearchResult::newFromTitle(
- Title::makeTitle( $row->page_namespace, $row->page_title )
+ Title::makeTitle( $row->page_namespace, $row->page_title ), $this
);
}
// show results
if ( $numTextMatches > 0 ) {
+ $search->augmentSearchResults( $textMatches );
$out->addHTML( $this->showMatches( $textMatches ) );
}
*
* @return string
*/
- protected function showMatches( &$matches, $interwiki = null ) {
+ protected function showMatches( $matches, $interwiki = null ) {
global $wgContLang;
$terms = $wgContLang->convertForSearchResult( $matches->termMatches() );
$pos = $this->offset;
if ( $result && $interwiki ) {
- $out .= $this->interwikiHeader( $interwiki, $result );
+ $out .= $this->interwikiHeader( $interwiki, $matches );
}
$out .= "<ul class='mw-search-results'>\n";
*
* @return string
*/
- protected function showHit( $result, $terms, $position ) {
+ protected function showHit( SearchResult $result, $terms, $position ) {
if ( $result->isBrokenTitle() ) {
return '';
wfGetDB( DB_MASTER )->onTransactionPreCommitOrIdle(
function() use ( $cache, $key ) {
$cache->delete( $key );
- }
+ },
+ __METHOD__
);
}
}
* Deferred version of incEditCountImmediate()
*/
public function incEditCount() {
- wfGetDB( DB_MASTER )->onTransactionPreCommitOrIdle( function() {
- $this->incEditCountImmediate();
- } );
+ wfGetDB( DB_MASTER )->onTransactionPreCommitOrIdle(
+ function () {
+ $this->incEditCountImmediate();
+ },
+ __METHOD__
+ );
}
/**
* Replaces User::touchUser()
*/
function invalidateCache() {
- $this->db->update( 'user',
+ $this->db->update(
+ 'user',
[ 'user_touched' => $this->db->timestamp() ],
[ 'user_id' => $this->id ],
- __METHOD__ );
+ __METHOD__
+ );
$wikiId = $this->db->getWikiID();
$userId = $this->id;
- $this->db->onTransactionPreCommitOrIdle( function() use ( $wikiId, $userId ) {
- User::purge( $wikiId, $userId );
- } );
+ $this->db->onTransactionPreCommitOrIdle(
+ function () use ( $wikiId, $userId ) {
+ User::purge( $wikiId, $userId );
+ },
+ __METHOD__
+ );
}
}
* Set triggers like when to try to run deferred updates
* @since 1.28
*/
- public function setTriggers() {
+ public function setAgentAndTriggers() {
+ if ( function_exists( 'posix_getpwuid' ) ) {
+ $agent = posix_getpwuid( posix_geteuid() )['name'];
+ } else {
+ $agent = 'sysadmin';
+ }
+ $agent .= '@' . wfHostname();
+
$lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
+ // Add a comment for easy SHOW PROCESSLIST interpretation
+ $lbFactory->setAgentName(
+ mb_strlen( $agent ) > 15 ? mb_substr( $agent, 0, 15 ) . '...' : $agent
+ );
self::setLBFactoryTriggers( $lbFactory );
}
// A good time when no DBs have writes pending is around lag checks.
// This avoids having long running scripts just OOM and lose all the updates.
-$maintenance->setTriggers();
+$maintenance->setAgentAndTriggers();
// Do the work
$maintenance->execute();
'scripts' => 'resources/lib/jquery/jquery.appear.js',
],
'jquery.arrowSteps' => [
+ 'deprecated' => true,
'scripts' => 'resources/src/jquery/jquery.arrowSteps.js',
'styles' => 'resources/src/jquery/jquery.arrowSteps.css',
'targets' => [ 'desktop', 'mobile' ],
public function testPrefixNormalizationSearchBug() {
$title = Title::newFromText( 'Category:Template:xyz' );
$page = WikiPage::factory( $title );
- $page->doEdit( 'Some text', 'inserting content' );
+
+ $page->doEditContent(
+ ContentHandler::makeContent( 'Some text', $page->getTitle() ),
+ 'inserting content'
+ );
$result = $this->doApiRequest( [
'action' => 'query',
$pageName = 'Help:' . __METHOD__;
$title = Title::newFromText( $pageName );
$page = WikiPage::factory( $title );
- $page->doEdit( 'Some text', 'inserting content' );
+
+ $page->doEditContent(
+ ContentHandler::makeContent( 'Some text', $page->getTitle() ),
+ 'inserting content'
+ );
$apiResult = $this->doApiRequest( [
'action' => 'query',
}
$this->db->restoreFlags( IDatabase::RESTORE_INITIAL );
}
+
/**
* @covers DatabaseBase::dropTable
*/
private function dropFunctions() {
$this->db->query( 'DROP FUNCTION IF EXISTS mw_test_function'
- . ( $this->db->getType() == 'postgres' ? '()' : '' )
+ . ( $this->db->getType() == 'postgres' ? '()' : '' )
);
}
$db->setFlag( DBO_TRX );
$called = false;
$flagSet = null;
- $db->onTransactionIdle( function() use ( $db, &$flagSet, &$called ) {
- $called = true;
- $flagSet = $db->getFlag( DBO_TRX );
- } );
+ $db->onTransactionIdle(
+ function () use ( $db, &$flagSet, &$called ) {
+ $called = true;
+ $flagSet = $db->getFlag( DBO_TRX );
+ },
+ __METHOD__
+ );
$this->assertFalse( $flagSet, 'DBO_TRX off in callback' );
$this->assertTrue( $db->getFlag( DBO_TRX ), 'DBO_TRX restored to default' );
$this->assertTrue( $called, 'Callback reached' );
$db->clearFlag( DBO_TRX );
$flagSet = null;
- $db->onTransactionIdle( function() use ( $db, &$flagSet ) {
- $flagSet = $db->getFlag( DBO_TRX );
- } );
+ $db->onTransactionIdle(
+ function () use ( $db, &$flagSet ) {
+ $flagSet = $db->getFlag( DBO_TRX );
+ },
+ __METHOD__
+ );
$this->assertFalse( $flagSet, 'DBO_TRX off in callback' );
$this->assertFalse( $db->getFlag( DBO_TRX ), 'DBO_TRX restored to default' );
$db->clearFlag( DBO_TRX );
- $db->onTransactionIdle( function() use ( $db ) {
- $db->setFlag( DBO_TRX );
- } );
+ $db->onTransactionIdle(
+ function () use ( $db ) {
+ $db->setFlag( DBO_TRX );
+ },
+ __METHOD__
+ );
$this->assertFalse( $db->getFlag( DBO_TRX ), 'DBO_TRX restored to default' );
}
$db->clearFlag( DBO_TRX );
$db->begin( __METHOD__ );
$called = false;
- $db->onTransactionResolution( function() use ( $db, &$called ) {
+ $db->onTransactionResolution( function () use ( $db, &$called ) {
$called = true;
$db->setFlag( DBO_TRX );
} );
$db->clearFlag( DBO_TRX );
$db->begin( __METHOD__ );
$called = false;
- $db->onTransactionResolution( function() use ( $db, &$called ) {
+ $db->onTransactionResolution( function () use ( $db, &$called ) {
$called = true;
$db->setFlag( DBO_TRX );
} );
public function testTransactionListener() {
$db = $this->db;
- $db->setTransactionListener( 'ping', function() use ( $db, &$called ) {
+ $db->setTransactionListener( 'ping', function () use ( $db, &$called ) {
$called = true;
} );
/**
* @covers WikiPage::doEdit
+ * @deprecated since 1.21. Should be removed when WikiPage::doEdit() gets removed
*/
public function testDoEdit() {
$this->hideDeprecated( "WikiPage::doEdit" );
$this->assertArrayHasKey( 'testData', $mapping );
$this->assertEquals( 'test', $mapping['testData'] );
}
+
+ public function hookSearchIndexFields( $mockFieldBuilder, &$fields, SearchEngine $engine ) {
+ $fields['testField'] = $mockFieldBuilder( "testField", SearchIndexField::INDEX_TYPE_TEXT );
+ return true;
+ }
+
+ public function testAugmentorSearch() {
+ $this->search->setNamespaces( [ 0, 1, 4 ] );
+ $resultSet = $this->search->searchText( 'smithee' );
+ // Not using mock since PHPUnit mocks do not work properly with references in params
+ $this->mergeMwGlobalArrayValue( 'wgHooks',
+ [ 'SearchResultsAugment' => [ [ $this, 'addAugmentors' ] ] ] );
+ $this->search->augmentSearchResults( $resultSet );
+ for ( $result = $resultSet->next(); $result; $result = $resultSet->next() ) {
+ $id = $result->getTitle()->getArticleID();
+ $augmentData = "Result:$id:" . $result->getTitle()->getText();
+ $augmentData2 = "Result2:$id:" . $result->getTitle()->getText();
+ $this->assertEquals( [ 'testSet' => $augmentData, 'testRow' => $augmentData2 ],
+ $result->getExtensionData() );
+ }
+ }
+
+ public function addAugmentors( &$setAugmentors, &$rowAugmentors ) {
+ $setAugmentor = $this->getMock( 'ResultSetAugmentor' );
+ $setAugmentor->expects( $this->once() )
+ ->method( 'augmentAll' )
+ ->willReturnCallback( function ( SearchResultSet $resultSet ) {
+ $data = [];
+ for ( $result = $resultSet->next(); $result; $result = $resultSet->next() ) {
+ $id = $result->getTitle()->getArticleID();
+ $data[$id] = "Result:$id:" . $result->getTitle()->getText();
+ }
+ $resultSet->rewind();
+ return $data;
+ } );
+ $setAugmentors['testSet'] = $setAugmentor;
+
+ $rowAugmentor = $this->getMock( 'ResultAugmentor' );
+ $rowAugmentor->expects( $this->exactly( 2 ) )
+ ->method( 'augment' )
+ ->willReturnCallback( function ( SearchResult $result ) {
+ $id = $result->getTitle()->getArticleID();
+ return "Result2:$id:" . $result->getTitle()->getText();
+ } );
+ $rowAugmentors['testRow'] = $rowAugmentor;
+ }
}
// let the user have a few (3) edits
$page = WikiPage::factory( Title::newFromText( 'Help:UserTest_EditCount' ) );
for ( $i = 0; $i < 3; $i++ ) {
- $page->doEdit( (string)$i, 'test', 0, false, $user );
+
+ $page->doEditContent(
+ ContentHandler::makeContent( (string)$i, $page->getTitle() ),
+ 'test',
+ 0,
+ false,
+ $user
+ );
}
$this->assertEquals(