action-scheduler icon indicating copy to clipboard operation
action-scheduler copied to clipboard

as_schedule_cron_action doesn't schedule for a time considering the site timezone

Open renatho opened this issue 4 years ago • 10 comments

I'd like to schedule a cron job to run every midnight based on the site timezone. Currently, it seems it's always scheduled as UTC time. Notice that I configured a timezone in the WordPress settings, in my php.ini, and using date_default_timezone_set.

If I schedule the job like that:

\as_schedule_cron_action( current_datetime()->getTimestamp(), '0 0 * * *', $name, $args, $group );

It creates the job to run midnight in UTC timezone: Scheduled date: 2021-09-17 00:00:00 +0000

Checking the code, I noticed that I could use a string and create the cron like that:

\as_schedule_cron_action( current_datetime()->format( 'Y-m-d H:i:s e' ), '0 0 * * *', $name, $args, $group );

The above code creates a job with the corrected time, but still with the UTC timezone. Scheduled date: 2021-09-17 07:00:00 +0000

The documentation says to use an int timestamp. So probably it's not a good way to do it, right? Is there some good workaround for that? Or should I calculate the timezone and create the cron-link schedule string with the hour corrected based on the timezone I want?

renatho avatar Sep 16 '21 23:09 renatho

I've handled this issue with something like this:

$hour = 12; // Get your hour value from... wherever.
// Compute offset from server time in hours and adjust our hour based on it.
$offset = ( current_time( 'timestamp' ) - time() ) / HOUR_IN_SECONDS;
$hour = ( $hour - $offset ) % 24;
	
// Schedule it.
as_schedule_cron_action( gmdate( 'U' ), "* $hour * * *", 'your_action', [], 'your_group' );

drywall avatar Oct 29 '21 19:10 drywall

We handled that a bit differently to give support for timezones with minutes variations too.

	private function apply_wp_timezone_to_cron_schedule( $schedule ) {
		$gmt_offset = get_option( 'gmt_offset' );

		if ( false === $gmt_offset ) {
			return $schedule;
		}

		$schedule_parts    = explode( ' ', $schedule );
		$minutes           = $gmt_offset * 60;
		$time              = current_datetime()->setTime( $schedule_parts[1], $schedule_parts[0], 0 )->modify( "-{$minutes} minutes" );
		$hour              = (int) $time->format( 'G' );
		$minute            = (int) $time->format( 'i' );
		$schedule_parts[0] = $minute;
		$schedule_parts[1] = $hour;

		return implode( ' ', $schedule_parts );
	}

renatho avatar Oct 29 '21 20:10 renatho

That's definitely more robust for handling timezones that are not just off by hours, e.g. India and Newfoundland. Nice!

Be careful using gmt_offset though, newer WP environments utilize timezone_string instead and leave gmt_offset unset. Probably best to actually use wp_timezone() instead... see here.

drywall avatar Oct 29 '21 21:10 drywall

wp_timezone

Thank you for letting me know! I remember that when I was doing this, I didn't find a way to get the offset from DateTimeZone (returned by wp_timezone). But since it can be a problem, I'll try to dig into that a little more and check the link you sent! Thank you again! =)

renatho avatar Oct 29 '21 21:10 renatho

Thanks for flagging this.

I think it makes sense to use the site timezone here, though being mindful of backwards compatibility it might be that to resolve this we either should introduce a new function, or else tweak the signature of the existing function (adding an additional optional param), or both those things.

For instance:

function as_schedule_cron_action(
    $timestamp, 
    $schedule, 
    $hook, 
    $args = array(), 
    $group = '',
    $schedule_is_gmt = true # <- Set to false to use the site timezone
) { /* ... */ }

Otherwise we might cause problems for code that depends on the existing behavior, or where a workaround is already in place. Looking at 712-gh-Automattic/sensei-wc-paid-courses this feels low priority—but let me know if that reading is incorrect :-)

barryhughes avatar Jan 20 '22 17:01 barryhughes

@barryhughes, Sorry for the long delay to answer that! But yeah! Our issue is a low priority now. 😉

renatho avatar Feb 14 '22 18:02 renatho

I'm interested in this topic too. I have several tasks set using "as_schedule_recurring_action" to run at specific times of the day (daily) and have offset the time when setting the task to match my site timezone - but what happens when there is a change to local daylight savings time?

I presume that because the task is scheduled to run every 24 hours, while it is correct now, it will not be when the clocks change, can anyone confirm this and suggest a way to manage that change? I am guessing that I'll actually need to somehow detect the change, and then delete and reschedule the tasks with the new offset, yeah?

JomacInc avatar Jul 13 '22 11:07 JomacInc

Currently, you would need to manage this yourself (not ideal, but there is no baked-in support for this at time of writing). A possible pattern you could adopt as an interim measure:

  • Scheduling information could be passed to your callbacks via one or more additional arguments.
  • Each callback could then assess if a correction is needed, and take care of re-scheduling if necessary.

Alternative:

  • Schedule an action that recurs each hour (or some other suitable cadence) and triggers a 'manager action'.
  • The manager action would then check what the time is in local terms, and trigger one or more specific actions as needed (according to a map that indexes callbacks by local time, or something along those lines).

Further alternative:

  • Schedule an action that kicks in the day before DST starts/ends.
  • Perform corrections to scheduling as needed.

There are of course various disadvantages to each of the above strategies, but one of them might fit your needs as an interim measure.

barryhughes avatar Jul 29 '22 20:07 barryhughes

Cheers @barryhughes , great alternative ideas. Will implement one of them.

JomacInc avatar Jul 31 '22 06:07 JomacInc

Thanks again @barryhughes, I actually ended up solving this with a pretty simple solution.

I have a recurring task that runs once every hour (at a few mins past the hour) and the within the hook, I test what hour it is in a switch statement using current_time('H') to get the localised numeric hour.

As long as I don't schedule anything to run in the hours DST changes (2am & 3am in my case) then everything is sweet.

JomacInc avatar Oct 01 '22 23:10 JomacInc