Hello. We recently experienced a serious problem on our site wherein some users began complaining that our site was spamming them with dozens of the same emails being sent over and over again.

What was happening was, our e-learning site features a few "Email Reminder Sequences" that mail out to a user, for example, once a week with a tip about how best to approach that week's lesson. They can set a start date for their reminder sequence for any time that they'd like. It just adds a UNIX timestamp to a profile field.

We were using user_stats module's "User is a Day Older" event trigger to check, once a day, the current timestamp compared to the the timestamp marking the start of their reminder sequence. So, the user turns a day older, then the system looks to see when their sequence started. It finds the sequence started 7 days ago and, hence, sends the user their one-week email with tips about the week's lesson.

The problem was that, on this particular day, another module was eating up crazy amounts of memory and causing Cron to fail with a memory limit error. Hence, it would try again 10 minutes later, the next time it regularly ran. This event trigger does what it claims and only finds users who are one day older - HOWEVER - in the event that Cron fails, it will continue REPEATEDLY finding the same users over and over again. Hence, the few users who were supposed to receive an email began receiving it over and over, every time Cron ran, because it kept failing catastrophically.

Basically there needs to be something that filters out users who have already been processed as becoming a "day older" by marking them as such AS SOON AS the event is triggered. So immediately upon completion of the rules_invoke_event('user_stats_day_older', $update_user->uid); code, that user must be tagged in some way so [s]he is never triggered for that event again.

My recommendation is that the module add a row to {user_stats_values} for each user processed with the name "last_known_age" (or something) and the value as the user's age (in days) as calculated at the last time of processing.

This seems a little strange to me, but I'm not easily seeing a better way. Perhaps a better alternative is to, during the cron run, ALWAYS add/update a "user_age" row for each user processed based on their actual age, simply running the trigger first. ...Is that the same thing?

I'm going to work on a patch for this, I think, but if anyone sees that I'm clearly overlooking something, please let me know.

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

Offlein’s picture

Status: Active » Needs review
FileSize
3 KB

OK, here's the patch! So what's happening here, in order of what you see in the patch, is:

  1. I'm modifying the function user_stats_get_stats(), when it's called with $type == 'reg_days' to, instead check the `user_stats` table for existing stats where the stat name is "last_known_age". If there's no row, the data returned is "0". There is also a new "stat" I added called "reg_days_onthefly" which does just what the old "reg_days" type did - it calculates on the fly.
  2. In the hook_cron() implementation, instead of just getting the UID of all users who are one day older, I am selecting the UID, their calculated age, AND ALSO their `user_stats` table last_known_age.
  3. Now, instead of just triggering the event, we first check to see if the user's calculated age is the same as their "last_known_age" and, if it IS THE SAME, then we do nothing. Because...
  4. ...If it's NOT the same, we first record the user's calculated age as their "last known age". Keep in mind again that the user_stats_get_stats() function does NOT go by immediately calculated age anymore, it only goes by what's in the last_known_age table.
  5. HENCE, on a cron run, if the user_stats "last_known_age" does not equal the calculated age, then basically the "last_known_age" is wrong, and needs to be updated because the user has become a day older. Then we can be safe in triggering the event. If the "calculated age" IS the same as the "last_known_age" then something went wrong (like Cron crashed) and, while Cron may indeed believe this user is just turning a day older, they were in fact already processed, and the event trigger already occurred. So we will NOT run that trigger for a second time today.

And there we have it. I am now using it on my site.

Offlein’s picture

Hey, anyone have an opinion on this? I'd like to see it committed. I know it's obscure. It's been working well for me these past few months, however.

Liam McDermott’s picture

Status: Needs review » Fixed

I've committed this with a couple of very small amendments for code style (two lines had trailing spaces and one if block was all on one line).

This is fine for now, and thank you very much for creating the patch, in the longer term however we should, IMO, use Rules' scheduled events feature (forgotten what it's called), since this feature has always been a bit of a ghastly hack. :)

So, instead of saying 'Do this when the user is a day older' we say, 'When the user has been signed-up for one month'. At least, that's the idea.

Offlein’s picture

Hooray! Thank you Liam!

And agreed.

Status: Fixed » Closed (fixed)

Automatically closed -- issue fixed for 2 weeks with no activity.