Take the 2-minute tour ×
WordPress Development Stack Exchange is a question and answer site for WordPress developers and administrators. It's 100% free, no registration required.

I have the following function. This is part of a membership-based site. When a post of the type "message" is moved to Publish status, it should send an email to all users who belong (custom user meta) to the district(s) that are checked (taxonomy) when the post is drafted.

If I send to a small group, it seems to work. If I send to all the districts, it hangs and no messages appear to go out. I tried limiting it into chunks (25) to ease the pain, but does not help. Any thoughts besides telling the client to only send to one group at a time?

add_action('publish_messages', 'messages_notify', 10, 3);
function messages_notify() {
    global $post;
    // if post already published, abort
    if($post->post_status == 'publish') {
        return;
    }
    // get districts ticked, else abort
    $districts = get_the_terms($post->ID, 'message_district');
    if(empty($districts)) return;

    // query members who belong to one of the districts ticked
    $the_districts = array();
    $district_args = array('relation' => 'OR' );
    foreach($districts as $district){
        $the_districts[] = $district->name;
        if ($district->name == 'Northeast') {
            $district_args[] = array('key' => 'whprms_district_ne', 'value' => 1);
        }
        if ($district->name == 'North Central') {
            $district_args[] = array('key' => 'whprms_district_nc', 'value' => 1);
        }
        if ($district->name == 'Southeast') {
            $district_args[] = array('key' => 'whprms_district_se', 'value' => 1);
        }
        if ($district->name == 'Southwest') {
            $district_args[] = array('key' => 'whprms_district_sw', 'value' => 1);
        }
        if ($district->name == 'West') {
            $district_args[] = array('key' => 'whprms_district_w', 'value' => 1);
        }
    }
    $district_members = get_users( 
            array(
            'meta_query' => $district_args,
            )
    );

    $attachment_args = array('post_type' => 'attachment', 'orderby' => 'date', 'order' => 'ASC', 'numberposts' => -1, 'post_status' => NULL, 'post_parent' =>$post->ID);
    $attachments = get_posts($attachment_args);
    if ($attachments) {
        $att_array = array();
        foreach ($attachments as $attachment) {
            $att_array[] = get_the_title($attachment->ID);
        }
    }

    // message only users with desired role(s)
    $bcc = array();
    foreach($district_members as $district_member) {
        if(user_can($district_member->ID, 'board_members') || user_can($district_member->ID, 'members')) {
            $bcc[] = $district_member->user_email;
        }
    }

    $subject = 'WHPRMS Message: '.$post->post_title;
    $message = 'New message posted by ';
    if(user_can($post->post_author, 'administrator')) {
        $message .= get_the_author_meta('user_nicename', $post->post_author)."\n\r";
    }
    else {
        $message .= get_the_author_meta('first_name', $post->post_author).' '.get_the_author_meta('last_name', $post->post_author)."\n\r"; 
    }

    $message .= 'Excerpt: '.stripslashes(substr($post->post_content, 0, 240)).'...'."\n\r";
    $message .= 'Link: '.get_permalink($post->ID)."\n\r";

    $message .= 'District(s): '.implode(', ', $the_districts)."\n\r";
    if (!empty($att_array)) {
        $message .= 'Attachment(s): '.implode(', ', $att_array)."\n\r";
    }

    $message .= 'You are receiving this message because you belong to one of the districts mentioned above.';

    $chunked_bcc = array_chunk($bcc, 25);

    foreach($chunked_bcc as $bcc_chunk){
        $headers = array();
        $headers['Bcc'] = 'BCC: '.implode(', ', $bcc_chunk);
        wp_mail('no-reply@whprms.org', $subject, $message, $headers);
    }

}
share|improve this question
    
It brings me no great pleasure to do this, but I must reopen this question as this is still timing out. There must be a problem in my code somewhere. If I send this to less recipients, it seems to work. But around 200 and it times out. I even tried using wpMandrill as an alternative to wp_mail, and the same story is true. I am using HostGator as host and have been through it with them. They claim no fault. Doesn't wp_mail() wait for a response? How come sometimes the messages sends to some people, and sometimes not at all? –  GhostToast Jan 16 '13 at 19:40

2 Answers 2

up vote 2 down vote accepted
+50

Well, this isn't a full solution per se, but I would start by isolating whether the problem is the SQL queries or the wp_mail() call. If you comment out the wp_mail() line, you can get an idea of how long the function takes to run the queries and build the messages without trying to send them.

If you find that suddenly the function is lightning fast, then it's a fair bet that the mail connections are what's slowing you down, but unfortunately that opens another can of worms since email configurations can vary wildly from web host to web host.

If on the other hand the function is just as slow, then the queries are the likely culprit, and then it would be very helpful to see the actual SQL that Wordpress is generating in order to try to identify the performance leak.

EDIT:

Sounds like it is the queries doing many many JOINs after all. In that case, the goal is to avoid that. We can take advantage of the fact that we're looking for different cases of the same wp_usermeta.meta_key values to build a query that doesn't need all the JOINs:

function messages_notify() {
    global $post;
    // if post already published, abort
    if($post->post_status == 'publish') {
        return;
    }
    // get districts ticked, else abort
    $post_districts = get_the_terms($post->ID, 'message_district');
    if(empty($post_districts)) return;

    // Build a list of districts to get users from.
    $districts = array(
        'Northeast' => 'whprms_district_ne', 
        'North Central' => 'whprms_district_nc', 
        'Southeast' => 'whprms_district_se', 
        'Southwest' => 'whprms_district_sw', 
        'West' => 'whprms_district_w', 
        //'Another' => 'whprms_district_XX', 
    );

    $searchDistricts = array();
    $districtNames = array_keys($districts);
    foreach($post_districts as $d){
        if(in_array($d->name, $districtNames){
            $searchDistricts[] = $districts[$d->name];
        }
    }

    $districts = implode(' OR ', array_map(function($s){ return "wp_usermeta.meta_key = '{$s}'"; }, $searchDistricts));  // Squash it down into an sql-compatible WHERE subclause.

    $sql = "SELECT DISTINCT wp_users.* 
    FROM wp_users 
    INNER JOIN wp_usermeta ON (wp_users.ID = wp_usermeta.user_id) 
    WHERE 
    -- This part isolates to keys in the list of districts
    ({$districts}) 
    -- Where the value of the district is 1.
    AND CAST(wp_usermeta.meta_value AS CHAR) = '1'
    ORDER BY user_login ASC";

    $district_members = $wpdb->WHATEVER_THE_CUSTOM_QUERY_METHOD_IS_CALLED($sql);

    $attachment_args = array('post_type' => 'attachment', 'orderby' => 'date', 'order' => 'ASC', 'numberposts' => -1, 'post_status' => NULL, 'post_parent' =>$post->ID);
    $attachments = get_posts($attachment_args);
    if ($attachments) {
        $att_array = array();
        foreach ($attachments as $attachment) {
            $att_array[] = get_the_title($attachment->ID);
        }
    }

    // message only users with desired role(s)
    $bcc = array();
    foreach($district_members as $district_member) {
        // Don't use user_can()! It's a DB hit!
        //if(user_can($district_member->ID, 'board_members') || user_can($district_member->ID, 'members')) {  //@TODO:  Look at $district_member->caps
        if(in_array('board_members', $district_member->roles) || in_array('members', $district_member->roles)) {
            $bcc[] = $district_member->user_email;
        }
    }

    $subject = 'WHPRMS Message: '.$post->post_title;
    $message = 'New message posted by ';
    if(user_can($post->post_author, 'administrator')) {
        $message .= get_the_author_meta('user_nicename', $post->post_author)."\n\r";
    }
    else {
        $message .= get_the_author_meta('first_name', $post->post_author).' '.get_the_author_meta('last_name', $post->post_author)."\n\r"; 
    }

    $message .= 'Excerpt: '.stripslashes(substr($post->post_content, 0, 240)).'...'."\n\r";
    $message .= 'Link: '.get_permalink($post->ID)."\n\r";

    $message .= 'District(s): '.implode(', ', $the_districts)."\n\r";
    if (!empty($att_array)) {
        $message .= 'Attachment(s): '.implode(', ', $att_array)."\n\r";
    }

    $message .= 'You are receiving this message because you belong to one of the districts mentioned above.';

    $chunked_bcc = array_chunk($bcc, 25);

    foreach($chunked_bcc as $bcc_chunk){
        $headers = array();
        $headers['Bcc'] = 'BCC: '.implode(', ', $bcc_chunk);
        wp_mail('no-reply@whprms.org', $subject, $message, $headers);
    }

}

A couple things this doesn't take into account, since I don't have this particular WP instance in front of me to test with:

  • WHATEVER_THE_CUSTOM_QUERY_METHOD_IS_CALLED. Replace this with whatever the right method is.
  • I also don't know what format that will return results in. If you can get WP_user objects out of it, that'd be ideal, because the next section depends on $district_users looking the same as what get_users() provided.
  • Also pay attention to the change in foreach($district_members as $district_member). The user_can() method is 2 unnecessary DB hits for EVERY user you're looping over. You have the user's capabilities in the results returned from the query already-- use that instead.
share|improve this answer
    
This is true. It is not wp_mail() after all, but is the expensive JOIN process of WordPress's complex SQL calls it seems... –  GhostToast Jan 16 '13 at 21:46
    
Barring a couple typos, this is perfect. This posted in 4 seconds to 195 recipients. Thank you so much! I can't award you your whuffie for another 20 hours though. Will do tomorrow. –  GhostToast Jan 16 '13 at 23:05
    
Just wanted to point out that you would need to global $wpdb and then reference that object to make the (more or less) direct SQL call using $district_members_ids = $wpdb->get_col( $sql ); to get the IDs, then $district_members = get_users(array('include' => $district_members_ids)); to get the actual users. Thanks again @beporter. –  GhostToast Jan 17 '13 at 19:49

Sounds to me perhaps its hanging after your PHP running exceeds PHP.ini's script timeout limit.

share|improve this answer
    
I believe you are right. I was able to get it to post after increasing it from 90s to 180s. Thank you. –  GhostToast Nov 29 '12 at 21:34
    
Not a problem :) –  Chris Nov 30 '12 at 0:28

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.