jenkins-bot has submitted this change and it was merged.

Change subject: Gracefully handle outdated echo_unread_wikis rows
......................................................................


Gracefully handle outdated echo_unread_wikis rows

Adds $wgEchoSectionTransition and $wgEchoBundleTransition.
If either of these settings is enabled, we will disbelieve
the alert/message counts in the euw table and obtain them
using server-side cross-wiki API queries instead.

This affects both ApiEchoNotifications (for generating the cross-wiki
summary entry) and the count and timestamp computation in NotifUser.

In bundle transition mode, we trust that notifications are classified
correctly between alerts and messages, but we don't trust the
counts in the table. In section transition mode, we trust that
the sum of the alert and message counts is the correct count,
but we don't trust the alert and message counts individually.
If both modes are enabled, we mistrust anything that's mistrusted
by either mode and only trust what's trusted by both modes.
In any event, we do trust that only the wikis with rows in the
euw table have unread notifications.

Bug: T132954
Change-Id: Ibcc8ac102dac3cf06916d67427b42457fdb93db6
---
M Echo.php
M includes/NotifUser.php
M includes/api/ApiCrossWikiBase.php
M includes/api/ApiEchoNotifications.php
4 files changed, 218 insertions(+), 21 deletions(-)

Approvals:
  Sbisson: Looks good to me, approved
  jenkins-bot: Verified



diff --git a/Echo.php b/Echo.php
index 735ce17..9eeeaf4 100644
--- a/Echo.php
+++ b/Echo.php
@@ -151,6 +151,17 @@
 // main one. Must be a key defined in $wgExternalServers
 $wgEchoSharedTrackingCluster = false;
 
+// Enable this when you've changed the section (alert vs message) of a 
notification
+// type, but haven't yet finished running backfillUnreadWikis.php. This setting
+// reduces performance but prevents glitchy and inaccurate information from 
being
+// show to users while the unread_wikis table is being rebuilt.
+$wgEchoSectionTransition = false;
+
+// Enable this when you've changed the way bundled notifications are counted,
+// but haven't yet finished running backfillUnreadWikis.php. Like 
$wgEchoSectionTransition,
+// this setting reduces performance but prevents glitches.
+$wgEchoBundleTransition = false;
+
 // The max number of notifications allowed for a user to do a live update,
 // this is also the number of max notifications allowed for a user to have
 // @FIXME - the name is not intuitive, probably change it when the deleteJob 
patch
diff --git a/includes/NotifUser.php b/includes/NotifUser.php
index 1755c90..546efaa 100644
--- a/includes/NotifUser.php
+++ b/includes/NotifUser.php
@@ -45,6 +45,11 @@
         */
        private $cached;
 
+       /**
+        * @var array|null
+        */
+       private $mForeignData = null;
+
        // The max notification count shown in badge
 
        // The max number shown in bundled message, eg, <user> and 99+ others 
<action>.
@@ -230,7 +235,7 @@
                $count = (int) 
$this->userNotifGateway->getCappedNotificationCount( $dbSource, 
$eventTypesToLoad, MWEchoNotifUser::MAX_BADGE_COUNT + 1 );
 
                if ( $global ) {
-                       $count += $this->getForeignNotifications()->getCount( 
$section );
+                       $count += $this->getForeignCount( $section );
                }
 
                $this->setInCache( $memcKey, $count, 86400 );
@@ -316,7 +321,8 @@
 
                // Use timestamp of most recent foreign notification, if it's 
more recent
                if ( $global ) {
-                       $foreignTime = 
$this->getForeignNotifications()->getTimestamp( $section );
+                       $foreignTime = $this->getForeignTimestamp( $section );
+
                        if (
                                $foreignTime !== false &&
                                // $foreignTime < $timestamp = invert 0
@@ -478,16 +484,16 @@
 
                if ( $wgEchoCrossWikiNotifications ) {
                        // For performance, compute the global counts by adding 
foreign counts to the above
-                       $globalAlertCount = $alertCount + 
$this->getForeignNotifications()->getCount( EchoAttributeManager::ALERT );
-                       $globalMsgCount = $msgCount + 
$this->getForeignNotifications()->getCount( EchoAttributeManager::MESSAGE );
+                       $globalAlertCount = $alertCount + 
$this->getForeignCount( EchoAttributeManager::ALERT );
+                       $globalMsgCount = $msgCount + $this->getForeignCount( 
EchoAttributeManager::MESSAGE );
                        $globalAllCount = $globalAlertCount + $globalMsgCount;
 
                        // For performance, compute the global timestamps as 
max( localTimestamp, foreignTimestamp )
-                       $foreignAlertUnread = 
$this->getForeignNotifications()->getTimestamp( EchoAttributeManager::ALERT );
+                       $foreignAlertUnread = $this->getForeignTimestamp( 
EchoAttributeManager::ALERT );
                        $globalAlertUnread = $alertUnread !== false &&
                                ( $foreignAlertUnread === false || 
$alertUnread->diff( $foreignAlertUnread )->invert === 1 ) ?
                                $alertUnread : $foreignAlertUnread;
-                       $foreignMsgUnread = 
$this->getForeignNotifications()->getTimestamp( EchoAttributeManager::MESSAGE );
+                       $foreignMsgUnread = $this->getForeignTimestamp( 
EchoAttributeManager::MESSAGE );
                        $globalMsgUnread = $msgUnread !== false &&
                                ( $foreignMsgUnread === false || 
$msgUnread->diff( $foreignMsgUnread )->invert === 1 ) ?
                                $msgUnread : $foreignMsgUnread;
@@ -670,4 +676,118 @@
                }
                return $this->foreignNotifications;
        }
+
+       /**
+        * Get data about foreign notifications from the foreign wikis' APIs.
+        *
+        * This is used when $wgEchoSectionTransition or 
$wgEchoBundleTransition is enabled,
+        * to deal with untrustworthy echo_unread_wikis entries. This method 
fetches the list of
+        * wikis that have any unread notifications at all from the 
echo_unread_wikis table, then
+        * queries their APIs to find the per-section counts and timestamps for 
those wikis.
+        *
+        * The results of this function are cached in the NotifUser object.
+        * @return array [ (str) wiki => [ (str) section => [ 'count' => (int) 
count, 'timestamp' => (str) ts ] ] ]
+        */
+       protected function getForeignData() {
+               if ( $this->mForeignData ) {
+                       return $this->mForeignData;
+               }
+
+               $potentialWikis = $this->getForeignNotifications()->getWikis( 
EchoAttributeManager::ALL );
+               $foreignReq = new EchoForeignWikiRequest(
+                       $this->mUser,
+                       array(
+                               'action' => 'query',
+                               'meta' => 'notifications',
+                               'notprop' => 'count|list',
+                               'notgroupbysection' => '1',
+                               'notunreadfirst' => '1',
+                       ),
+                       $potentialWikis,
+                       'notwikis'
+               );
+               $foreignResults = $foreignReq->execute();
+
+               $this->mForeignData = array();
+               foreach ( $foreignResults as $wiki => $result ) {
+                       if ( !isset( $result['query']['notifications'] ) ) {
+                               continue;
+                       }
+                       $data = $result['query']['notifications'];
+                       foreach ( EchoAttributeManager::$sections as $section ) 
{
+                               if ( isset( $data[$section]['rawcount'] ) ) {
+                                       
$this->mForeignData[$wiki][$section]['count'] = $data[$section]['rawcount'];
+                               }
+                               if ( isset( $data[$section]['list'][0] ) ) {
+                                       
$this->mForeignData[$wiki][$section]['timestamp'] = 
$data[$section]['list'][0]['timestamp']['mw'];
+                               }
+                       }
+               }
+               return $this->mForeignData;
+       }
+
+       protected function getForeignCount( $section = 
EchoAttributeManager::ALL ) {
+               global $wgEchoSectionTransition, $wgEchoBundleTransition;
+               $count = 0;
+               if (
+                       // In section transition mode, we don't trust the 
individual echo_unread_wikis rows
+                       // but we do trust that alert+message=all. In bundle 
transition mode, we don't trust
+                       // that either, but we do trust that wikis with rows in 
the table have unread notifications
+                       // and wikis without rows in the table don't.
+                       ( $wgEchoSectionTransition && $section !== 
EchoAttributeManager::ALL ) ||
+                       $wgEchoBundleTransition
+               ) {
+                       $foreignData = $this->getForeignData();
+                       foreach ( $foreignData as $data ) {
+                               if ( $section === EchoAttributeManager::ALL ) {
+                                       foreach ( $data as $subData ) {
+                                               if ( isset( $subData['count'] ) 
) {
+                                                       $count += 
$subData['count'];
+                                               }
+                                       }
+                               } elseif ( isset( $data[$section]['count'] ) ) {
+                                       $count += $data[$section]['count'];
+                               }
+                       }
+               } else {
+                       $count += $this->getForeignNotifications()->getCount( 
$section );
+               }
+               return $count;
+       }
+
+       protected function getForeignTimestamp( $section = 
EchoAttributeManager::ALL ) {
+               if (
+                       // In section transition mode, we don't trust the 
individual echo_unread_wikis rows
+                       // but we do trust that alert+message=all. In bundle 
transition mode, we don't trust
+                       // that either, but we do trust that wikis with rows in 
the table have unread notifications
+                       // and wikis without rows in the table don't.
+                       ( $wgEchoSectionTransition && $section !== 
EchoAttributeManager::ALL ) ||
+                       $wgEchoBundleTransition
+               ) {
+                       $foreignTime = false;
+                       $foreignData = $this->getForeignData();
+                       foreach ( $foreignData as $data ) {
+                               if ( $section === EchoAttributeManager::ALL ) {
+                                       foreach ( $data as $subData ) {
+                                               if ( isset( 
$subData['timestamp'] ) ) {
+                                                       $wikiTime = new 
MWTimestamp( $data[$section]['timestamp'] );
+                                                       // $wikiTime > 
$foreignTime = invert 1
+                                                       if ( $foreignTime === 
false || $wikiTime->diff( $foreignTime )->invert === 1 ) {
+                                                               $foreignTime = 
$wikiTime;
+                                                       }
+                                               }
+                                       }
+                               } elseif ( isset( $data[$section]['timestamp'] 
) ) {
+                                       $wikiTime = new MWTimestamp( 
$data[$section]['timestamp'] );
+                                       // $wikiTime > $foreignTime = invert 1
+                                       if ( $foreignTime === false || 
$wikiTime->diff( $foreignTime )->invert === 1 ) {
+                                               $foreignTime = $wikiTime;
+                                       }
+                               }
+                       }
+               } else {
+                       $foreignTime = 
$this->getForeignNotifications()->getTimestamp( $section );
+               }
+               return $foreignTime;
+       }
 }
diff --git a/includes/api/ApiCrossWikiBase.php 
b/includes/api/ApiCrossWikiBase.php
index 84f0ef6..e5d9eeb 100644
--- a/includes/api/ApiCrossWikiBase.php
+++ b/includes/api/ApiCrossWikiBase.php
@@ -23,14 +23,16 @@
         * This will turn the current API call (with all of it's params) and 
execute
         * it on all foreign wikis, returning an array of results per wiki.
         *
+        * @param array $wikis List of wikis to query. Defaults to the result 
of getRequestedForeignWikis().
+        * @param array $paramOverrides Request parameter overrides
         * @return array
         * @throws Exception
         */
-       protected function getFromForeign() {
+       protected function getFromForeign( $wikis = null, array $paramOverrides 
= array() ) {
                $foreignReq = new EchoForeignWikiRequest(
                        $this->getUser(),
-                       $this->getForeignQueryParams(),
-                       $this->getRequestedForeignWikis(),
+                       $paramOverrides + $this->getForeignQueryParams(),
+                       $wikis !== null ? $wikis : 
$this->getRequestedForeignWikis(),
                        $this->getModulePrefix() . 'wikis'
                );
                return $foreignReq->execute();
diff --git a/includes/api/ApiEchoNotifications.php 
b/includes/api/ApiEchoNotifications.php
index 896fc68..3fc66d1 100644
--- a/includes/api/ApiEchoNotifications.php
+++ b/includes/api/ApiEchoNotifications.php
@@ -91,9 +91,12 @@
                                                $titles, $params[$section . 
'unreadfirst']
                                        );
 
-                                       if ( $this->crossWikiSummary && 
$this->foreignNotifications->getCount( $section ) > 0 ) {
+                                       if ( $this->crossWikiSummary ) {
                                                // insert fake notification for 
foreign notifications
-                                               array_unshift( 
$result[$section]['list'], $this->makeForeignNotification( $user, 
$params['format'], $section ) );
+                                               $foreignNotification = 
$this->makeForeignNotification( $user, $params['format'], $section );
+                                               if ( $foreignNotification ) {
+                                                       array_unshift( 
$result[$section]['list'], $foreignNotification );
+                                               }
                                        }
                                }
                        } else {
@@ -108,8 +111,11 @@
                                // if exactly 1 section is specified, we 
consider only that section, otherwise
                                // we pass ALL to consider all foreign 
notifications
                                $section = count( $params['sections'] ) === 1 ? 
reset( $params['sections'] ) : EchoAttributeManager::ALL;
-                               if ( $this->crossWikiSummary && 
$this->foreignNotifications->getCount( $section ) > 0 ) {
-                                       array_unshift( $result['list'], 
$this->makeForeignNotification( $user, $params['format'], $section ) );
+                               if ( $this->crossWikiSummary ) {
+                                       $foreignNotification = 
$this->makeForeignNotification( $user, $params['format'], $section );
+                                       if ( $foreignNotification ) {
+                                               array_unshift( $result['list'], 
$foreignNotification );
+                                       }
                                }
                        }
                }
@@ -283,15 +289,73 @@
                return $result;
        }
 
+       /**
+        * Build and format a "fake" notification to represent foreign 
notifications.
+        * @param User $user
+        * @param string $format
+        * @param string $section
+        * @return array|false A formatted notification, or false if there are 
no foreign notifications
+        */
        protected function makeForeignNotification( User $user, $format, 
$section = EchoAttributeManager::ALL ) {
-               $wikis = $this->foreignNotifications->getWikis( $section );
-               $count = $this->foreignNotifications->getCount( $section );
+               global $wgEchoSectionTransition, $wgEchoBundleTransition;
+               if (
+                       ( $wgEchoSectionTransition && $section !== 
EchoAttributeManager::ALL ) ||
+                       $wgEchoBundleTransition
+               ) {
+                       // In section transition mode we trust that 
echo_unread_wikis is accurate for the total of alerts+messages,
+                       // but not for each section individually (i.e. we don't 
trust that notifications won't be misclassified).
+                       // We get all wikis that have any notifications at all 
according to the euw table,
+                       // and query them to find out what's really there.
+                       // In bundle transition mode, we trust that 
notifications are classified correctly, but we don't
+                       // trust the counts in the table.
+                       $potentialWikis = 
$this->foreignNotifications->getWikis( $wgEchoSectionTransition ? 
EchoAttributeManager::ALL : $section );
+                       if ( !$potentialWikis ) {
+                               return false;
+                       }
+                       $foreignResults = $this->getFromForeign( 
$potentialWikis, array( $this->getModulePrefix() . 'filter' => '!read' ) );
+
+                       $countsByWiki = array();
+                       $timestampsByWiki = array();
+                       foreach ( $foreignResults as $wiki => $result ) {
+                               if ( isset( 
$result['query']['notifications']['list'] ) ) {
+                                       $notifs = 
$result['query']['notifications']['list'];
+                               } elseif ( isset( 
$result['query']['notifications'][$section]['list'] ) ) {
+                                       $notifs = 
$result['query']['notifications'][$section]['list'];
+                               } else {
+                                       $notifs = false;
+                               }
+                               if ( $notifs ) {
+                                       $countsByWiki[$wiki] = count( $notifs );
+                                       $timestampsByWiki[$wiki] = max( 
array_map( function ( $n ) {
+                                               return $n['timestamp']['mw'];
+                                       }, $notifs ) );
+                               }
+                       }
+
+                       $wikis = array_keys( $countsByWiki );
+                       $count = array_sum( $countsByWiki );
+                       $maxTimestamp = new MWTimestamp( max( $timestampsByWiki 
) );
+                       $timestampsByWiki = array_map( function ( $ts ) {
+                               return new MWTimestamp( $ts );
+                       }, $timestampsByWiki );
+               } else {
+                       // In non-transition mode, or when querying all 
sections, we can trust the euw table
+                       $wikis = $this->foreignNotifications->getWikis( 
$section );
+                       $count = $this->foreignNotifications->getCount( 
$section );
+                       $maxTimestamp = 
$this->foreignNotifications->getTimestamp( $section );
+                       $timestampsByWiki = array();
+                       foreach ( $wikis as $wiki ) {
+                               $timestampsByWiki[$wiki] = 
$this->foreignNotifications->getWikiTimestamp( $wiki, $section );
+                       }
+               }
+
+               if ( $count === 0 || count( $wikis ) === 0 ) {
+                       return false;
+               }
 
                // Sort wikis by timestamp, in descending order (newest first)
-               usort( $wikis, function ( $a, $b ) use ( $section ) {
-                       $aTimestamp = 
$this->foreignNotifications->getWikiTimestamp( $a, $section ) ?: new 
MWTimestamp( 0 );
-                       $bTimestamp = 
$this->foreignNotifications->getWikiTimestamp( $b, $section ) ?: new 
MWTimestamp( 0 );
-                       return $bTimestamp->getTimestamp( TS_UNIX ) - 
$aTimestamp->getTimestamp( TS_UNIX );
+               usort( $wikis, function ( $a, $b ) use ( $section, 
$timestampsByWiki ) {
+                       return $timestampsByWiki[$b]->getTimestamp( TS_UNIX ) - 
$timestampsByWiki[$a]->getTimestamp( TS_UNIX );
                } );
 
                $row = new StdClass;
@@ -310,7 +374,7 @@
                ) );
 
                $row->notification_user = $user->getId();
-               $row->notification_timestamp = 
$this->foreignNotifications->getTimestamp( $section );
+               $row->notification_timestamp = $maxTimestamp;
                $row->notification_read_timestamp = null;
                $row->notification_bundle_base = 1;
                $row->notification_bundle_hash = md5( 'bogus' );
@@ -326,7 +390,7 @@
                $output['sources'] = EchoForeignNotifications::getApiEndpoints( 
$wikis );
                // Add timestamp information
                foreach ( $output['sources'] as $wiki => &$data ) {
-                       $data['ts'] = 
$this->foreignNotifications->getWikiTimestamp( $wiki, $section )->getTimestamp( 
TS_MW );
+                       $data['ts'] = $timestampsByWiki[$wiki]->getTimestamp( 
TS_MW );
                }
                return $output;
        }

-- 
To view, visit https://gerrit.wikimedia.org/r/291676
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: merged
Gerrit-Change-Id: Ibcc8ac102dac3cf06916d67427b42457fdb93db6
Gerrit-PatchSet: 12
Gerrit-Project: mediawiki/extensions/Echo
Gerrit-Branch: master
Gerrit-Owner: Catrope <roan.katt...@gmail.com>
Gerrit-Reviewer: Catrope <roan.katt...@gmail.com>
Gerrit-Reviewer: Mattflaschen <mflasc...@wikimedia.org>
Gerrit-Reviewer: Sbisson <sbis...@wikimedia.org>
Gerrit-Reviewer: jenkins-bot <>

_______________________________________________
MediaWiki-commits mailing list
MediaWiki-commits@lists.wikimedia.org
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to