在開發後臺與任務相關的功能中,遇到一個需求:用戶須要可以爲任務配置定時策略,使任務定時執行某個操做。html
根據需求,咱們能夠拆解成以下幾個步驟:前端
其中步驟 1 與本文無關不提;對於定時任務的實現,在上節 Celery異步任務隊列 有簡單提到 celery 也支持定時任務。python
Celery 的定時任務策略配置於代碼中,在啓動 celery 時寫入本地 shelve 文件,不利於管理。shell
所以在 celery 的文檔中也提到一個擴展模塊 django-celery-beat
,該模塊將定時任務的配置寫入 Django 配置的數據庫中,當程序啓動後能夠經過 admin
後臺進行管理,而且能夠直接經過 ORM 對定時任務配置進行修改,無需修改代碼而後重啓 celery,符合咱們預期。數據庫
固然還有不少其餘庫也能實現,由於咱們已經使用 celery 執行異步任務,因此本文仍是用 django-celery-beat
解決問題。django
Celery 的定時任務使用的是相似 crontab
的語法,所以在用戶體驗上,要考慮普通用戶的學習成本,能夠提供一些經常使用的配置,例如每週的工做日天天 1 點執行任務;也要考慮後期的擴展性,能夠提供輸入框方便配置。json
CrontabSchedule
支持類 crontab 語法,一樣是 5 個配置域,分別爲:後端
每一個配置域使用空格隔開。框架
對每一個配置域經常使用語法:異步
*
: 範圍內的全部值M-N
: M到N之間的值M-N/X
或 */X
: 每X分鐘、每X天等等A,B,...,Z
: 枚舉的值舉個例子: 每一個工做日1點執行:0 1 1-5 * *
建立定時策略代碼以下:
from django_celery_beat.models import CrontabSchedule, PeriodicTask
>>> schedule, _ = CrontabSchedule.objects.get_or_create(
... minute='30',
... hour='*',
... day_of_week='*',
... day_of_month='*',
... month_of_year='*',
... )
複製代碼
定時任務能夠依賴不一樣的定時策略,例如 crontab, interval 等,建立時指定 schedule
便可。以 crontab 定時任務爲例:
>>> import json
>>> from datetime import datetime, timedelta
>>> PeriodicTask.objects.create(
... crontab=schedule, # we created this above.
... name='Importing contacts', # simply describes this periodic task.
... task='proj.tasks.import_contacts', # name of task.
... args=json.dumps(['arg1', 'arg2']),
... kwargs=json.dumps({
... 'be_careful': True,
... }),
... expires=datetime.utcnow() + timedelta(seconds=30)
... )
複製代碼
其中 name
爲定時任務的名稱,每一個任務名必須惟一;task
爲須要執行的 celery 任務。加上定時策略調度器,這三個是一個定時任務所必須的屬性。
定時任務還有其餘配置,如 args
/kwargs
對應一個 celery 任務的入參;expires
設置了該定時任務的過時時間。
最基礎的配置只須要在 INSTALLED_APPS
中添加引用,並設置定時任務調度器便可:
settings.py
INSTALLED_APPS = [
...
'django_celery_beat'
]
# 配置 celery 定時任務使用的調度器
CELERY_BEAT_SCHEDULER = 'django_celery_beat.schedulers:DatabaseScheduler'
複製代碼
在使用 django-celery-beat
過程當中遇到兩個關於時區的問題:
解決方案:
8小時明顯是由於時區不一樣致使,而 django-celery-beat
對時區的處理彷佛總有問題(若不對請指教)。
修改 settings.py
中的時區配置:
settings.py
# 設置 Django 大部分應用通用的時區
TIME_ZONE = 'Asia/Shanghai'
# 關閉 UTC
USE_TZ = False
CELERY_ENABLE_UTC = False
# 設置 django-celery-beat 真正使用的時區
CELERY_TIMEZONE = TIME_ZONE
# 使用 timezone naive 模式
DJANGO_CELERY_BEAT_TZ_AWARE = False
複製代碼
關於 timezone naive 與 timezone aware 模式的區別能夠參考文章:Django時區詳解
簡單來講就是,naive 模式不存儲時區信息,只存儲通過時區轉換後的時間;反之 aware 模式則存儲了 UTC 時間和 UTC 時區信息。
根據文檔,在修改了時區後,須要將已執行過的定時任務的 last_run_at
重置爲 None
:
python manage.py shell
>>> from django_celery_beat.models import PeriodicTask
>>> PeriodicTask.objects.all().update(last_run_at=None)
複製代碼
修改完成後,重啓 celery beat
。
PS: 就算是通過這樣配置,我也仍然遇到了任務不斷執行的問題,而且在我屢次重啓 celery 後再也不復現,所以本配置可能還有問題。
CrontabSchedule
的 timezone
配置始終是 UTC
解決方案:
查看 CrontabSchedule
模型的源碼,找到數據庫中 timezone
字段的屬性:
class CrontabSchedule(models.Model):
...
timezone = timezone_field.TimeZoneField(
default='UTC',
verbose_name=_('Cron Timezone'),
help_text=_(
'Timezone to Run the Cron Schedule on. Default is UTC.'),
)
複製代碼
因爲咱們在建立 CrontabSchedule
實例時並未指定 timezone
,所以在建立任務時,添加該字段的配置便可:
from django_celery_beat.models import CrontabSchedule
>>> schedule, _ = CrontabSchedule.objects.get_or_create(
... minute='30',
... hour='*',
... day_of_week='*',
... day_of_month='*',
... month_of_year='*',
... timezone='Asia/Shanghai'
... )
複製代碼
本節內容僅供參考,不必定適用其餘場景。
設計前端定時任務配置項,包含一個開關,一個三選一單選組件,以及一個輸入框:
爲了方便非技術人員設置定時任務,優化用戶體驗,定時任務除了「自定義」的輸入模式,還有一個「天天」與「每週」的選項:
0 1 1-5 * *
0 1 1 * *
單選框與字符串雙向綁定,在後端返回上面兩個字符串之一時選中天天或每週,不然選中自定義選項。
假設對於個人業務來講,前端須要的任務數據字段爲:
{
"task_id": 1,
"is_periodic_task": true,
"periodic_task_id": 1,
"crontab": "* * * * *"
}
複製代碼
ER 模型如圖:
返回給前端的數據中,若 periodic_task
不爲空,則 is_periodic_task
爲 True
,並經過 periodic_task.crontab_id
獲取到 CrontabSchedule
實例,轉化爲字符串返回。
要注意,CrontabSchedule
的 __str__
方法除了返回 crontab
配置,還會返回時區等信息,而這些信息前端展現時並不須要。
所以能夠新建一個方法:
def get_crontab_str(contab) -> str:
""" 獲取前端配置須要的 5 項值 :param contab: CrontabSchedule對象 :return: """
return '{0} {1} {2} {3} {4}'.format(
cronexp(contab.minute), cronexp(contab.hour),
cronexp(contab.day_of_week), cronexp(contab.day_of_month),
cronexp(contab.month_of_year)
)
複製代碼
序列化時調用該方法返回給前端便可。
修改任務包括如下三種狀況
對應流程圖以下:
1:
2, 3:
圖中「修改配置中的」指前端傳來的修改請求中的新配置信息
具體代碼就不贅述,只提一下暫停定時任務的方法:
修改 PeriodicTask.objects.enabled
爲 False/0
便可
>>> periodic_task.enabled = False
>>> periodic_task.save()
複製代碼
Django 實現網頁端配置定時任務的功能實現大抵如此,惋惜 Django 及其插件對時區的配置比較複雜,花了不少時間踩了不少坑都仍是有些問題沒能搞清楚。繼續探索吧!
框架/服務/組件 | 版本 | 說明 |
---|---|---|
Python | 3.6.7 | |
Django | 2.2 | |
RabbitMQ | 3 | |
Celery | 4.3 | |
django-celery-beat | 1.5.0 |