在每个时区每天凌晨 3:00 运行 celery 任务?姜戈

Posted

技术标签:

【中文标题】在每个时区每天凌晨 3:00 运行 celery 任务?姜戈【英文标题】:Run celery task everyday at 3:00 am in every timezone? Django 【发布时间】:2021-10-02 07:12:48 【问题描述】:

我需要在每天凌晨 3:00 为多个时区运行一项任务,在应用中每个组织都有一个时区。

class Organization(models.Model):

    timezone = models.IntegerField(choices=((i, i) for i in range(-12, 13)))

因此,如果两个组织的时区不同,则任务应在各自时区的凌晨 3:00 执行,即不能同时执行。

我怎样才能做到这一点?

【问题讨论】:

那些不是时区。时区可能有夏令时。它也可能只有不到一个小时,例如 UTC+5:30 左右。更好地使用 Python 的内置时区模块。它们通常被命名为“布鲁塞尔/欧洲”等。 我无法更改这一点,因为该项目不仅是我的,而且其中的代码依赖于该模型,包括“时区”字段。 如果两个组织有相同的时区,任务是否只执行一次,这意味着已经处理了两个组织? 没错,这将是该时区每个组织的一项任务。 【参考方案1】:

一个解决方案是:

    列出所有必要的时区(例如Asia/Manila)及其对应的UTC偏移量(例如+8.0)。这是timezone names and their UTC offsets 的完整列表。 获取每个时区凌晨 03:00 的 UTC 等值 将 Celery 配置为基于 UTC,然后使用 celery beat 安排将在每个时区的凌晨 03:00 转换为 UTC 的任务

代码:

from dataclasses import dataclass
from datetime import datetime, time, timezone
from decimal import Decimal
import pytz  # If using > Python3.9, you can try using the built-in library zoneinfo

### Step 1: Collect all the UTC offsets for all timezones

utc_offsets = set()

for tz in pytz.all_timezones:
    # If needed, filter the timezones to only the ones that are necessary, skip those that are not needed.
    dt = datetime.now(pytz.timezone(tz))

    # Decimal was used instead of the built-in float for a more accurate precision
    dt_utc_offset_seconds = Decimal(dt.utcoffset().total_seconds())
    dt_utc_offset_minutes = dt_utc_offset_seconds / Decimal('60')
    dt_utc_offset_hours = dt_utc_offset_minutes / Decimal('60')
    utc_offsets.add(dt_utc_offset_hours)

### Step 2: Get the equivalent in UTC of each timezone's 03:00 AM

TASK_HOUR_TARGET = Decimal('3')
task_hour_per_tz = set()

for utc_offset in utc_offsets:
    if utc_offset == 0:
        task_hour_as_utc_offset = TASK_HOUR_TARGET

    elif utc_offset > 0:
        task_hour_as_utc_offset = TASK_HOUR_TARGET - utc_offset

        if task_hour_as_utc_offset < 0:
            task_hour_as_utc_offset = 24 - abs(task_hour_as_utc_offset)

    elif utc_offset < 0:
        task_hour_as_utc_offset = TASK_HOUR_TARGET + abs(utc_offset)

        if task_hour_as_utc_offset >= 24:
            task_hour_as_utc_offset = task_hour_as_utc_offset - 24

    task_hour_per_tz.add((utc_offset, task_hour_as_utc_offset))

### Step 3: Schedule the execution of celery for each converted 03:00 AM of each timezone

@dataclass
class crontab:
    # For the sake of testing, here is a mocked celery.schedules.crontab
    minute: int
    hour: int

# This assumes that celery_app.conf.timezone = "UTC"
beat_schedule = 
    f"utc_offset": 
        'task': 'task.func',
        'schedule': crontab(
            minute=int((task_hour * Decimal('60')) % Decimal('60')),
            hour=int(task_hour),
        ),
        'kwargs': "utc_offset": utc_offset,  # To let the task know for what timezone is the trigger for
    
    for utc_offset, task_hour in sorted(task_hour_per_tz) # No need of the sort. It is just here so that when we print the dictionary, the order is by utc_offset.


for key in beat_schedule:
    print(key, beat_schedule[key])

输出:

-12 'task': 'task.func', 'schedule': crontab(minute=0, hour=15), 'kwargs': 'utc_offset': Decimal('-12')
-11 'task': 'task.func', 'schedule': crontab(minute=0, hour=14), 'kwargs': 'utc_offset': Decimal('-11')
-10 'task': 'task.func', 'schedule': crontab(minute=0, hour=13), 'kwargs': 'utc_offset': Decimal('-10')
-9.5 'task': 'task.func', 'schedule': crontab(minute=30, hour=12), 'kwargs': 'utc_offset': Decimal('-9.5')
-9 'task': 'task.func', 'schedule': crontab(minute=0, hour=12), 'kwargs': 'utc_offset': Decimal('-9')
-8 'task': 'task.func', 'schedule': crontab(minute=0, hour=11), 'kwargs': 'utc_offset': Decimal('-8')
-7 'task': 'task.func', 'schedule': crontab(minute=0, hour=10), 'kwargs': 'utc_offset': Decimal('-7')
-6 'task': 'task.func', 'schedule': crontab(minute=0, hour=9), 'kwargs': 'utc_offset': Decimal('-6')
-5 'task': 'task.func', 'schedule': crontab(minute=0, hour=8), 'kwargs': 'utc_offset': Decimal('-5')
-4 'task': 'task.func', 'schedule': crontab(minute=0, hour=7), 'kwargs': 'utc_offset': Decimal('-4')
-3 'task': 'task.func', 'schedule': crontab(minute=0, hour=6), 'kwargs': 'utc_offset': Decimal('-3')
-2.5 'task': 'task.func', 'schedule': crontab(minute=30, hour=5), 'kwargs': 'utc_offset': Decimal('-2.5')
-2 'task': 'task.func', 'schedule': crontab(minute=0, hour=5), 'kwargs': 'utc_offset': Decimal('-2')
-1 'task': 'task.func', 'schedule': crontab(minute=0, hour=4), 'kwargs': 'utc_offset': Decimal('-1')
0 'task': 'task.func', 'schedule': crontab(minute=0, hour=3), 'kwargs': 'utc_offset': Decimal('0')
1 'task': 'task.func', 'schedule': crontab(minute=0, hour=2), 'kwargs': 'utc_offset': Decimal('1')
2 'task': 'task.func', 'schedule': crontab(minute=0, hour=1), 'kwargs': 'utc_offset': Decimal('2')
3 'task': 'task.func', 'schedule': crontab(minute=0, hour=0), 'kwargs': 'utc_offset': Decimal('3')
4 'task': 'task.func', 'schedule': crontab(minute=0, hour=23), 'kwargs': 'utc_offset': Decimal('4')
4.5 'task': 'task.func', 'schedule': crontab(minute=30, hour=22), 'kwargs': 'utc_offset': Decimal('4.5')
5 'task': 'task.func', 'schedule': crontab(minute=0, hour=22), 'kwargs': 'utc_offset': Decimal('5')
5.5 'task': 'task.func', 'schedule': crontab(minute=30, hour=21), 'kwargs': 'utc_offset': Decimal('5.5')
5.75 'task': 'task.func', 'schedule': crontab(minute=15, hour=21), 'kwargs': 'utc_offset': Decimal('5.75')
6 'task': 'task.func', 'schedule': crontab(minute=0, hour=21), 'kwargs': 'utc_offset': Decimal('6')
6.5 'task': 'task.func', 'schedule': crontab(minute=30, hour=20), 'kwargs': 'utc_offset': Decimal('6.5')
7 'task': 'task.func', 'schedule': crontab(minute=0, hour=20), 'kwargs': 'utc_offset': Decimal('7')
8 'task': 'task.func', 'schedule': crontab(minute=0, hour=19), 'kwargs': 'utc_offset': Decimal('8')
8.75 'task': 'task.func', 'schedule': crontab(minute=15, hour=18), 'kwargs': 'utc_offset': Decimal('8.75')
9 'task': 'task.func', 'schedule': crontab(minute=0, hour=18), 'kwargs': 'utc_offset': Decimal('9')
9.5 'task': 'task.func', 'schedule': crontab(minute=30, hour=17), 'kwargs': 'utc_offset': Decimal('9.5')
10 'task': 'task.func', 'schedule': crontab(minute=0, hour=17), 'kwargs': 'utc_offset': Decimal('10')
10.5 'task': 'task.func', 'schedule': crontab(minute=30, hour=16), 'kwargs': 'utc_offset': Decimal('10.5')
11 'task': 'task.func', 'schedule': crontab(minute=0, hour=16), 'kwargs': 'utc_offset': Decimal('11')
12 'task': 'task.func', 'schedule': crontab(minute=0, hour=15), 'kwargs': 'utc_offset': Decimal('12')
12.75 'task': 'task.func', 'schedule': crontab(minute=15, hour=14), 'kwargs': 'utc_offset': Decimal('12.75')
13 'task': 'task.func', 'schedule': crontab(minute=0, hour=14), 'kwargs': 'utc_offset': Decimal('13')
14 'task': 'task.func', 'schedule': crontab(minute=0, hour=13), 'kwargs': 'utc_offset': Decimal('14')

从输出中可以看出,每个时区都有自己的计划任务,该任务将在相当于 UTC 03:00 AM 运行(例如,UTC+8.0 为 19:00)。

如果您不喜欢为每个时区生成单独的计划任务,您可能需要配置更复杂的crontab,例如crontab(minute=0, hour='0,1,2,3,4...') 但我认为某些时区组合不可能使用不同的分钟,例如以满足转换后的 UTC+5.0 和 UTC+5.5 的 03:00 AM 分别为 22:00 和 21:30(第 0 分钟和第 30 分钟)。尽管您也可能只是为不同的分钟设置生成不同的计划任务,例如第 0 分钟、第 15 分钟、第 30 分钟和第 45 分钟。

【讨论】:

您知道如何过滤组织时区吗?由于用于将它们保存在模型中的格式(如问题所示)并不是真正的时区。 Organization.timezone 字段是一个整数,对吗?不能只是Organization.objects.filter(timezone=int(utc_offset))吗?在这里(使用我上面的解决方案),utc_offset(直接映射到您的Organization.timezone)是一个Decimal 对象,它是执行时任务的输入,例如Decimal("8") 为 UTC+8(或 GMT+8)的时区。

以上是关于在每个时区每天凌晨 3:00 运行 celery 任务?姜戈的主要内容,如果未能解决你的问题,请参考以下文章

夏令时和 Cron

新手php时间戳的问题如何获取每天凌晨的时间戳?

Celery时区设置问题源码探究

DST 在 GCal 事件中导致 +1 小时时差

将celery定时任务设置为根据本地时区触发

elk-logstash时区问题