Skip to main content

Fixing the advancing time problem with Drupal's "popular/today" View

Posted in

Simmering problems are the worst and one that has been irking me for quite some time now is the utter uselessness of the "popular/today" view which comes with the Views module. Judging by it's name, you'd suspect it to reset the daily counter on midnight. Surprise! It doesn't. Worse yet, the time by which the counter gets reset advances constantly. In my case, one hour per day.

Ok, whats the matter here? Inspecting the "popular/today" view yields that it just builds it's list from the "node_counter" table which belongs to the statistics module. So, the problem must lie somewhere within the Drupal core. Furthermore, since the advance in time is always one hour per day and I run CRON hourly, the problem must also be somehow CRON related.

Digging a bit around Drupal's source code uncovers the following function in modules/statistics/statistics.module:

  function statistics_cron() {
    $statistics_timestamp = variable_get('statistics_day_timestamp', '');

    if ((time() - $statistics_timestamp) >= 86400) {
      // Reset day counts.
      db_query('UPDATE {node_counter} SET daycount = 0');
      variable_set('statistics_day_timestamp', time());
    }

    // [..] Rest does not matter

  }

Bingo! Dead wrong! For non developers, this code snippet simply states: query the database for when the "daycount" column of the "node_counter" table was last reset. If it has been at least 86400 seconds (= one day) ago, reset again and mark down the current time.

The assumption that this function would (at least most of the time) run exactly every 86400 is flawed in two subtle ways:

  1. The function calls time() twice, assuming that both calls would return the same timestamp. This assumption is wrong. The database query may take more than one second to complete or the system clock could advance to the next second in between the two calls to the time() function.
  2. While CRON may run in precise intervals, this does not necessarily mean that statistics_cron() will do so, too. CRON performs a lot of maintenance tasks each run, including checking for code updates. This results in a highly variable delay.

Being off by as much as one second is already fatal. Even that one second will cause the CRON run, suppose to reset the counter, to decide that it is not the time for doing it, yet. Ergo, the next CRON run does it and in doing so, advances the reset time by one interval. When Poormanscron is used, things become even more unreliable.

So much for the analysis of the problem. How can it be fixed? Obviously, the condition "once every 86400 seconds" is wrong. It should be "today, at midnight". But how do we replace that without hacking the core? Luckily, we don't have to. We only need to fix the "statistics_day_timestamp" variable. It's sufficient to set it to "midnight" somewhere between breakfast and supper time, but not around midnight.
One way to do that would be to create an extra cronjob that executes at noon and uses Drush and the UNIX date program to make the adjustment. For those, who prefer not to fiddle around with shellscripts, I prepared a module that will do the job.

Why not patch the Drupal core? Simple. The author of the statistics_cron() function obviously tried to avoid the hassle of having to deal with timezones, daylight savings time and other quirks. The resulting algorithm is flawed but at least robust. My solution on the other hand depends on CRON running at least hourly.