Celery beat - different time zone per task
Asked Answered
D

4

9

I am using celery beat to schedule some tasks. I'm able to use the CELERY_TIMEZONE setting to schedule the tasks using the crontab schedule and it runs at the scheduled time in the mentioned time zone.

But I want to be able to setup multiple such tasks for different timezones in the same application (single django settings.py). I know which task needs to run in what timezone when the task is being scheduled.

Is it possible to specify a different timezone for each of the tasks?

I'm using django (1.4) with celery (3.0.11) and django celery (3.0.11).

I've looked at the djcelery.schedulers.DatabaseScheduler class and it's base class, but I can't figure out how and where the timezone is getting used. Can I write a custom scheduler that can make each job run in a different timezone?

Thanks,

Dishwater answered 17/2, 2014 at 11:2 Comment(1)
from django.utils import timezone now = timezone.now()Dyslexia
A
22

You can achieve a timezone-aware scheduling of individual tasks in a celery schedule. This way you can run a task according to the local time in a specific timezone (also adjusting to e.g. daylight saving time) by specifying a separate now function for each celery schedule

crontab supports the nowfun argument to specify the datetime function to be used to check if it should run

import datetime
import pytz
nowfun = lambda: datetime.datetime.now(pytz.timezone('Europe/Berlin'))

In your schedule, set this function as the datetime function via

'periodic_task': {
    'task': 'api.tasks.periodic',
    'schedule': crontab(hour=6, minute=30, nowfun=nowfun)
}

This runs every day at 6.30am CET adjusted to daylight savings.

In case you use the function more than once, consider creating a helper

from functools import partial
cet_crontab = partial(crontab, nowfun=nowfun)
'periodic_task': {
    'task': 'api.tasks.periodic',
    'schedule': cet_crontab(hour=6, minute=30)
}

Make sure you have CELERY_ENABLE_UTC = False set, otherwise celery converts your schedules to UTC.

Atalanti answered 31/3, 2016 at 17:43 Comment(4)
This no longer works in Celery as nowfun will not be picklable.Mercantilism
@BrettJackson To pickle nowfun, I've declared it as a normal function instead of lambda, that helped.Subgroup
This is not a good way to solve this problem - rather keep everything in UTC and convert the timezone for the task to UTC as shown in this solution: linkManaging
@Managing Converting the time zone on creation does not handle DST changesWillson
A
0

I think the easiest way to do it is to use decorator with mock

from mock import patch

@task
@patch.multiple(settings, CELERY_TIMEZONE='time_zone')
def my_task(*args):
    #do your staff

I'm not tested it yet but it seems right. I hope i helped you :)

Anima answered 17/2, 2014 at 12:39 Comment(5)
Thanks for the suggestion. Let me try that. I was thinking something like that was needed but was not aware of mock.Dishwater
To think about it now, the timezone would need to be patched when beat starts and not when the task is executed I'd think. I'm going to test that.Dishwater
I've got a similar situation, did you come up with anything else?Grosz
No, I haven't come up with anything else because for now I've decided to make do with a single time zone. However, I did dig deep into the flow if database scheduler is used. It seems achievable if we customise that class and whole bunch of related code. The other thing to consider is where to store the timezone for each individual class. For now I store custom information related to a schedule in a different table that has a fkey to djcelery_periodictask. I hope to investigate the possibility of adding a new column to djcelery_periodic task for timezone and if it can be contributed to celery.Dishwater
I meant - where to store the timezone for each individual 'task'Dishwater
P
0

In my case, I needed a function that will run every time and get a timezone for a specific value from db.
I created a new function that will accept a variable and will get me the timezone needed.


def get_time(db_query: str) -> datetime.datetime:
    """Extract timezone from DB"""

    # get location from db
    timezone = 'get timezone for location'
    # return calculated datetime using timezone
    return datetime.datetime.now(pytz.timezone(timezone))


def custom_crontab(*args, **kwargs):
    """Create a custom crontab that will call get_time function and assign value to nowfun"""

    db_query = kwargs.pop("db_query")
    nowfun = get_time(db_query)
    kwargs['nowfun'] = nowfun
    # return the call to crontab
    return crontab(*args, **kwargs)


beat_schedule = {
    'periodic_task': {
        'task': 'main_job',
        'schedule': custom_crontab(minute=0, db_query='some_value'),
    }
}
Piled answered 5/10, 2020 at 22:16 Comment(0)
T
-3

Maybe I'm misunderstanding the problem, but if you want to run tasks at a certain time in different time zones, could you not just offset the scheduled time by the hour difference between the time zones? For example, if I wanted a task to run at 5 PM in two different TZs:

# settings.py
CELERY_TIMEZONE = "US/Eastern"

CELERYBEAT_SCHEDULE = { 
    # Task to run in EST
    'schedule-my-est-task': {
        'task': 'path.to.my.est.task',
        'schedule': crontab(minute=0, hour=17),
    },
    # Task to run in UTC - hour time is offset
    'schedule-my-utc-task': {
        'task': 'path.to.my.utc.task',
        'schedule': crontab(minute=0, hour=10), 
    },
}
Tryck answered 17/2, 2014 at 12:54 Comment(2)
Actually that's exactly what I'm trying to avoid. Currently that is what is being done. But every time there is a daylight settings change, someone needs to change the schedule. Users will add a schedule using an admin console like interface and will specify a time zone (actually a location and I figure out the time zone for that). I want the user to set the schedule for a timezone and forget about it and not worry about changing the schedule when day light settings change.Dishwater
That makes sense, and makes the original problem more difficult. So you want the task to run at a certain time based on someone's time zone in order that they could change time zone and still get it at the same time in whatever timezone they are in? Maybe you could run another scheduled task every hour, and each hour, check for users in the time zone where it is currently the time you want your task to run at. Then you could trigger the original task from that scheduled task for only the relevant users?Tryck

© 2022 - 2024 — McMap. All rights reserved.