Get rid of email bundling

Bug: T135446
Change-Id: I95dc3d70c82c19d89b7a32be0c36294350d5c30d
This commit is contained in:
Stephane Bisson 2016-05-17 15:58:06 -04:00
parent 46e69972c6
commit bb7a15e8be
4 changed files with 16 additions and 367 deletions

View file

@ -108,11 +108,9 @@ $wgAutoloadClasses += [
'MWEchoDbFactory' => __DIR__ . '/includes/EchoDbFactory.php', 'MWEchoDbFactory' => __DIR__ . '/includes/EchoDbFactory.php',
'MWEchoDbFactoryTest' => __DIR__ . '/tests/phpunit/EchoDbFactoryTest.php', 'MWEchoDbFactoryTest' => __DIR__ . '/tests/phpunit/EchoDbFactoryTest.php',
'MWEchoEmailBatch' => __DIR__ . '/includes/EmailBatch.php', 'MWEchoEmailBatch' => __DIR__ . '/includes/EmailBatch.php',
'MWEchoEmailBundler' => __DIR__ . '/includes/EmailBundler.php',
'MWEchoEventLogging' => __DIR__ . '/includes/EventLogging.php', 'MWEchoEventLogging' => __DIR__ . '/includes/EventLogging.php',
'MWEchoNotifUser' => __DIR__ . '/includes/NotifUser.php', 'MWEchoNotifUser' => __DIR__ . '/includes/NotifUser.php',
'MWEchoNotifUserTest' => __DIR__ . '/tests/phpunit/NotifUserTest.php', 'MWEchoNotifUserTest' => __DIR__ . '/tests/phpunit/NotifUserTest.php',
'MWEchoNotificationEmailBundleJob' => __DIR__ . '/includes/jobs/NotificationEmailBundleJob.php',
'NotificationControllerTest' => __DIR__ . '/tests/phpunit/controller/NotificationControllerTest.php', 'NotificationControllerTest' => __DIR__ . '/tests/phpunit/controller/NotificationControllerTest.php',
'NotificationsTest' => __DIR__ . '/tests/NotificationsTest.php', 'NotificationsTest' => __DIR__ . '/tests/NotificationsTest.php',
'SpecialDisplayNotificationsConfiguration' => __DIR__ . '/includes/special/SpecialDisplayNotificationsConfiguration.php', 'SpecialDisplayNotificationsConfiguration' => __DIR__ . '/includes/special/SpecialDisplayNotificationsConfiguration.php',

View file

@ -1,310 +0,0 @@
<?php
/**
* This class handles email bundling, it has only two public interfacing entries:
*
* 1. a single notification is triggered which calls self::addToEmailBatch()
* (a) cycle is null/reset, send single notification, schedule a bundle job for next notification
* (b) cycle is in bundle mode, add the notification to the queue
*
* 2. a job is popped off the queue which calls self::processBundleEmail()
*
*/
class MWEchoEmailBundler {
/**
* @var User
*/
protected $mUser;
/**
* @var string
*/
protected $bundleHash;
/**
* @var string
*
* The timestamp of email being sent
*/
protected $timestamp;
/**
* @var EchoEvent
*/
protected $baseEvent;
/**
* @var int
*
* seconds between sending batch email for a bundle notification
* this only applies to a bundle type
*/
protected $emailInterval;
/**
* Protected constructor so subclasses can call it
*/
protected function __construct( $user, $hash ) {
global $wgEchoBundleEmailInterval;
$this->mUser = $user;
$this->bundleHash = $hash;
$this->emailInterval = $wgEchoBundleEmailInterval;
if ( $this->emailInterval < 0 ) {
$this->emailInterval = 0;
}
}
/**
* Factory method
*/
public static function newFromUserHash( User $user, $hash ) {
if ( !$user->getId() ) {
return false;
}
if ( !$hash || !preg_match( '/^[a-f0-9]{32}$/', $hash ) ) {
return false;
}
return new self( $user, $hash );
}
/**
* Check if a new notification should be added to the batch queue
* true - added to the queue for bundling email
* false - not added, the client should send single email
*
* @param int $eventId
* @param int $eventPriority
*
* @return bool
*/
public function addToEmailBatch( $eventId, $eventPriority ) {
$this->retrieveLastEmailTimestamp();
$this->retrieveBaseEvent();
// send instant single notification email if there is no base event in the batch queue
// and the email is ready to send, otherwiase, add the email to batch and schedule
// a delayed job
if ( !$this->baseEvent && $this->shouldSendEmailNow() ) {
$this->timestamp = wfTimestampNow();
$this->updateEmailMetadata();
return false;
} else {
// add to email batch queue
MWEchoEmailBatch::addToQueue(
$this->mUser->getId(),
$eventId,
$eventPriority,
$this->bundleHash
);
// always push the job to job queue in case the previous job
// was lost, job queue will ignore duplicate
$this->pushToJobQueue( $this->getDelayTime() );
return true;
}
}
/**
* Get the time diff since last email
*/
protected function timeSinceLastEmail() {
// if there is no timestamp, next email should be sent right away
// set the time diff longer than the email interval
if ( !$this->timestamp ) {
return $this->emailInterval + 600;
}
$now = wfTimestamp( TS_UNIX );
return $now - wfTimestamp( TS_UNIX, $this->timestamp );
}
/**
* Check if an email should be sent right away
* @return bool
*/
protected function shouldSendEmailNow() {
if ( $this->timeSinceLastEmail() > $this->emailInterval ) {
return true;
} else {
return false;
}
}
/**
* Get the delay time
* @return int
*/
protected function getDelayTime() {
$delay = $this->emailInterval - $this->timeSinceLastEmail();
if ( $delay <= 0 ) {
$delay = 0;
}
return $delay;
}
/**
* Get the timestamp of last email
*/
protected function retrieveLastEmailTimestamp() {
$data = ObjectCache::getMainStashInstance()->get( $this->getMemcacheKey() );
if ( $data !== false ) {
$this->timestamp = $data['timestamp'];
}
}
/**
* Get the memcache key
* @return string
*/
protected function getMemcacheKey() {
return wfMemcKey( 'echo', 'email_bundle_status', $this->mUser->getId(), $this->bundleHash );
}
/**
* Retrieve the base event for email bundling, the one with the largest eeb_id
* @return bool
*/
protected function retrieveBaseEvent() {
$dbr = MWEchoDbFactory::getDB( DB_SLAVE );
$res = $dbr->selectRow(
array( 'echo_email_batch' ),
array( 'eeb_event_id' ),
array(
'eeb_user_id' => $this->mUser->getId(),
'eeb_event_hash' => $this->bundleHash
),
__METHOD__,
array( 'ORDER BY' => 'eeb_event_priority DESC, eeb_id DESC', 'LIMIT' => 1 )
);
if ( !$res ) {
return false;
}
$this->baseEvent = EchoEvent::newFromId( $res->eeb_event_id );
return true;
}
/**
* Push the latest bundle data to the queue
* @param $delay int To delay the job in $delay seconds
*/
public function pushToJobQueue( $delay = 0 ) {
$title = Title::newMainPage();
$job = new MWEchoNotificationEmailBundleJob(
$title,
array(
'user_id' => $this->mUser->getId(),
'bundle_hash' => $this->bundleHash,
'jobReleaseTimestamp' => wfTimestamp( TS_MW, wfTimestamp( TS_UNIX ) + $delay )
)
);
JobQueueGroup::singleton()->push( $job );
}
/**
* Main function for processinig bundle email
*/
public function processBundleEmail() {
$emailSetting = intval( $this->mUser->getOption( 'echo-email-frequency' ) );
// User has switched to email digest or decided not to receive email,
// the daily cron will handle events left in the queue
if ( $emailSetting != 0 ) {
throw new MWException( "User has switched to email digest/no email option!" );
}
// If there is nothing in the queue, do not update timestamp so next
// email would be just an instant email
if ( $this->retrieveBaseEvent() ) {
$this->timestamp = wfTimestampNow();
$this->updateEmailMetadata();
$this->sendEmail();
$this->clearProcessedEvent();
} else {
throw new MWException( "There is no bundle notification to process!" );
}
}
/**
* Send the bundle email
*/
protected function sendEmail() {
$content = $this->generateEmailContent();
if ( !isset( $content['subject'] ) || !isset( $content['body'] ) ) {
throw new MWException( "Fail to create bundle email content!" );
}
global $wgNotificationSender, $wgNotificationReplyName;
$toAddress = MailAddress::newFromUser( $this->mUser );
$fromAddress = new MailAddress( $wgNotificationSender, EchoHooks::getNotificationSenderName() );
$replyAddress = new MailAddress( $wgNotificationSender, $wgNotificationReplyName );
// Schedule a email job or just send the email directly?
UserMailer::send(
$toAddress,
$fromAddress,
$content['subject'],
$content['body'],
array( 'replyTo' => $replyAddress )
);
MWEchoEventLogging::logSchemaEchoMail( $this->mUser, 'bundle' );
}
/**
* Generate the content for bundle email
* @return string
*/
protected function generateEmailContent() {
if ( !$this->baseEvent ) {
return '';
}
$this->baseEvent->setBundleHash( $this->bundleHash );
return EchoNotificationController::formatNotification( $this->baseEvent, $this->mUser, 'email', 'email' );
}
/**
* Update bundle email metadata for user/hash pair
*/
protected function updateEmailMetadata() {
$key = $this->getMemcacheKey();
// Store new data and make it expire in 7 days
ObjectCache::getMainStashInstance()->set(
$key,
array(
'timestamp' => $this->timestamp
),
3600 * 24 * 7
);
}
/**
* clear processed event in the queue
*/
protected function clearProcessedEvent() {
if ( !$this->baseEvent ) {
return;
}
$conds = array( 'eeb_user_id' => $this->mUser->getId(), 'eeb_event_hash' => $this->bundleHash );
$conds[] = 'eeb_event_id <= ' . intval( $this->baseEvent->getId() );
$dbw = MWEchoDbFactory::newFromDefault()->getEchoDb( DB_MASTER );
$dbw->delete(
'echo_email_batch',
$conds,
__METHOD__
);
}
}

View file

@ -50,7 +50,7 @@ class EchoNotifier {
$userEmailNotifications = $attributeManager->getUserEnabledEvents( $user, 'email' ); $userEmailNotifications = $attributeManager->getUserEnabledEvents( $user, 'email' );
// See if the user wants to receive emails for this category or the user is eligible to receive this email // See if the user wants to receive emails for this category or the user is eligible to receive this email
if ( in_array( $event->getType(), $userEmailNotifications ) ) { if ( in_array( $event->getType(), $userEmailNotifications ) ) {
global $wgEchoEnableEmailBatch, $wgEchoNotifications, $wgNotificationSender, $wgNotificationReplyName, $wgEchoBundleEmailInterval; global $wgEchoEnableEmailBatch, $wgEchoNotifications, $wgNotificationSender, $wgNotificationReplyName;
$priority = $attributeManager->getNotificationPriority( $event->getType() ); $priority = $attributeManager->getNotificationPriority( $event->getType() );
@ -79,36 +79,23 @@ class EchoNotifier {
return true; return true;
} }
$addedToQueue = false; // instant email notification
$toAddress = MailAddress::newFromUser( $user );
// only send bundle email if email bundling is on $fromAddress = new MailAddress( $wgNotificationSender, EchoHooks::getNotificationSenderName() );
if ( $wgEchoBundleEmailInterval && $bundleHash && !empty( $wgEchoNotifications[$event->getType()]['bundle']['email'] ) ) { $replyAddress = new MailAddress( $wgNotificationSender, $wgNotificationReplyName );
$bundler = MWEchoEmailBundler::newFromUserHash( $user, $bundleHash ); // Since we are sending a single email, should set the bundle hash to null
if ( $bundler ) { // if it is set with a value from somewhere else
$addedToQueue = $bundler->addToEmailBatch( $event->getId(), $priority ); $event->setBundleHash( null );
} $email = self::generateEmail( $event, $user );
if ( !$email ) {
return false;
} }
$subject = $email['subject'];
$body = $email['body'];
$options = array( 'replyTo' => $replyAddress );
// send single notification if the email wasn't added to queue for bundling UserMailer::send( $toAddress, $fromAddress, $subject, $body, $options );
if ( !$addedToQueue ) { MWEchoEventLogging::logSchemaEchoMail( $user, 'single' );
// instant email notification
$toAddress = MailAddress::newFromUser( $user );
$fromAddress = new MailAddress( $wgNotificationSender, EchoHooks::getNotificationSenderName() );
$replyAddress = new MailAddress( $wgNotificationSender, $wgNotificationReplyName );
// Since we are sending a single email, should set the bundle hash to null
// if it is set with a value from somewhere else
$event->setBundleHash( null );
$email = self::generateEmail( $event, $user );
if ( !$email ) {
return false;
}
$subject = $email['subject'];
$body = $email['body'];
$options = array( 'replyTo' => $replyAddress );
UserMailer::send( $toAddress, $fromAddress, $subject, $body, $options );
MWEchoEventLogging::logSchemaEchoMail( $user, 'single' );
}
} }
return true; return true;

View file

@ -1,26 +0,0 @@
<?php
class MWEchoNotificationEmailBundleJob extends Job {
function __construct( $title, $params ) {
parent::__construct( __CLASS__, $title, $params );
// If there is already a job with the same params, this job will be ignored
// for example, if there is a page link bundle notification job for article A
// created by user B, any subsequent jobs with the same data will be ignored
$this->removeDuplicates = true;
}
function run() {
$bundle = MWEchoEmailBundler::newFromUserHash(
User::newFromId( $this->params['user_id'] ),
$this->params['bundle_hash']
);
if ( $bundle ) {
$bundle->processBundleEmail();
} else {
throw new MWException( 'Fail to create bundle object for: user_id: ' . $this->params['user_id'] . ', bundle_hash: ' . $this->params['bundle_hash'] );
}
return true;
}
}