mediawiki-extensions-Echo/includes/EmailBundler.php
bsitu d44ed993a2 Add email bundling function to Echo notification
* This patch needs intensive testing on Redis delayed job queue
* This patch is -2 mainly for redis/phpredis are not ready on test/test2/mediawiki

To test this locally, you need to:
* set up Redis and phpredis locally
* add the following to localSettings.php
    $wgJobTypeConf['MWEchoNotificationEmailBundleJob'] = array(
        'class'       => 'JobQueueRedis',
        'redisServer' => '127.0.0.1',
        'redisConfig' => array( 'connectTimeout' => 1 ),
        'claimTTL'    => 3600,
        'checkDelay'  => true
    );
* set $wgMainCacheType to CACHE_DB or memcache
* set $wgEchoBundleEmailInterval to smaller number for testing purpose, 0 to disable email bundling

Change-Id: I9313e7f6ed3e13478cec294b5b8408fe8e941faf
2013-04-11 11:25:14 -07:00

287 lines
6.6 KiB
PHP

<?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()
*
*/
abstract class MWEchoEmailBundler {
/**
* @var User
*/
protected $mUser;
/**
* @var string
*/
protected $bundleHash;
/**
* @var string
*
* The timestamp of email being sent
*/
protected $timestamp;
/**
* @var Event
*/
protected $baseEvent;
/**
* @var int
*
* seconds between sending batch email for a bundle notification
* this only applies to a bundle type
*/
protected $emailInterval;
/**
* Private constructor
*/
private function __construct( $user, $hash ) {
global $wgEchoBundleEmailInterval;
$this->mUser = $user;
$this->bundleHash = $hash;
$this->emailInterval = $wgEchoBundleEmailInterval;
if ( $this->emailInterval < 0 ) {
$this->emailInterval = 0;
}
}
/**
* Get the name of the email batch class
* @return string
* @throws MWException
*/
private static function getEmailBundlerClass() {
global $wgEchoBackendName;
$className = 'MW' . $wgEchoBackendName . 'EchoEmailBundler';
if ( !class_exists( $className ) ) {
throw new MWException( "$wgEchoBackendName email batch is not supported!" );
}
return $className;
}
/**
* 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;
}
$className = self::getEmailBundlerClass();
return new $className( $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
* @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;
}
static $now;
if ( !$now ) {
$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() {
global $wgMemc;
$data = $wgMemc->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
* @return bool
*/
abstract protected function retrieveBaseEvent();
/**
* 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() {
$this->retrieveLastEmailTimestamp();
// if there is nothing in the queue, do not schedule a job or
// update timestamp so next email would be just an instant email
if ( $this->retrieveBaseEvent() ) {
$this->timestamp = wfTimestampNow();
$this->updateEmailMetadata();
$this->sendEmail();
// for testing purpose, comment out the line below so events are kept
$this->clearProcessedEvent();
$this->pushToJobQueue( $this->emailInterval );
}
}
/**
* Send the bundle email
*/
protected function sendEmail() {
$content = $this->generateEmailContent();
// Error has occurred
// @Todo more error handling
if ( !isset( $content['subject'] ) || !isset( $content['body'] ) ) {
return;
}
global $wgPasswordSender, $wgPasswordSenderName;
$adminAddress = new MailAddress( $wgPasswordSender, $wgPasswordSenderName );
$address = new MailAddress( $this->mUser );
// Schedule a email job or just send the email directly?
UserMailer::send( $address, $adminAddress, $content['subject'], $content['body'] );
}
/**
* 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() {
global $wgMemc;
$key = $this->getMemcacheKey();
// Delete existing data
$wgMemc->delete( $key );
// Store new data and make it expire in 7 days
$wgMemc->set(
$key,
array(
'timestamp' => $this->timestamp
),
3600 * 24 * 7
);
}
/**
* clear processed event in the queue
*/
abstract protected function clearProcessedEvent();
}