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.
Comment | File | Size | Author |
---|---|---|---|
#1 | user_day_older_repeat_fail-1415916-1.patch | 3 KB | Offlein |
Comments
Comment #1
Offlein CreditAttribution: Offlein commentedOK, here's the patch! So what's happening here, in order of what you see in the patch, is:
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.And there we have it. I am now using it on my site.
Comment #2
Offlein CreditAttribution: Offlein commentedHey, 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.
Comment #3
Liam McDermott CreditAttribution: Liam McDermott commentedI'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.
Comment #4
Offlein CreditAttribution: Offlein commentedHooray! Thank you Liam!
And agreed.