# Celery_Periodic Tasks
###### tags: `celery` `celery 5.2` `python`
[官方連結_Periodic Tasks](https://docs.celeryproject.org/en/stable/userguide/periodic-tasks.html)
## Periodic Tasks
### Introduction
`celery beat`是一個排程器;它在定期的時間間隔中啟動任務,然後再由集群中的可用節點上執行這任務。
預設情況下,這些項目(entires)是由[beat_schedule](https://docs.celeryproject.org/en/stable/userguide/configuration.html#std-setting-beat_schedule)所設置,不過你也可以自定義,像是利用資料庫來保存這些項目之類的。
你必需確保一次只會有一個排程器在調度,否則你會遇到重複執行任務的問題。使用集中式的方法意味著不需要特別的去同步排程,而且服務可以在不使用鎖定(locking)的情況下運行。
### Time Zones
預設情況下,週期性任務排程器使用UTC時區,你可以使用[timezone](https://docs.celeryproject.org/en/stable/userguide/configuration.html#std-setting-timezone)來調整時區設置。
下面給出一個時區為`Europe/Londan`的範例:
```python
timezone = 'Europe/London'
```
這個設置必需要加到你的應用程式中,不管是直接使用`(app.conf.timezone = 'Europe/London')`,或是使用`app.config_from_object`的話就可以加到你的配置模組裡面。更多細節請參閱[Configuration](https://docs.celeryproject.org/en/stable/getting-started/first-steps-with-celery.html#celerytut-configuration)。
預設的排程器(儲存排程在檔案`celerybeat-schedule`的資料)會自動偵測到時區的改變,所以只要你的時區有變更,排程器本身就會自己重新啟動,但其它的排程器可能就沒有那麼聰明(像是,Django資料庫排程器,見下說明),這種情況下,你必須手動重新啟動排程器。
#### Django Users
Celery建議並相容Django 1.4中引入的新設置`USE_TZ`。
對Django用戶來說,你們將會使用`TIME_ZION`來設置時區,或是你可以使用[timezone](https://docs.celeryproject.org/en/stable/userguide/configuration.html#std-setting-timezone)單獨為Celery指定自定義時區。
資料庫排程器在時區調整之後並不會自動重啟,因此你應該手動執行:
```shell
$ python manage.py shell
>>> from djcelery.models import PeriodicTask
>>> PeriodicTask.objects.update(last_run_at=None)
```
Django-Celery僅支援到Celery 4.0及更低版本,對於Celery 4.0及更高版本,請執行以下操作:
```python
$ python manage.py shell
>>> from django_celery_beat.models import PeriodicTask
>>> PeriodicTask.objects.update(last_run_at=None)
```
### Entries
如果你想要週期性的呼叫任務,你就需要新增一個項目(entry)到`beat schedule list`裡面。
```python=
from celery import Celery
from celery.schedules import crontab
app = Celery()
@app.on_after_configure.connect
def setup_periodic_tasks(sender, **kwargs):
# Calls test('hello') every 10 seconds.
sender.add_periodic_task(10.0, test.s('hello'), name='add every 10')
# Calls test('world') every 30 seconds
sender.add_periodic_task(30.0, test.s('world'), expires=10)
# Executes every Monday morning at 7:30 a.m.
sender.add_periodic_task(
crontab(hour=7, minute=30, day_of_week=1),
test.s('Happy Mondays!'),
)
@app.task
def test(arg):
print(arg)
@app.task
def add(x, y):
z = x + y
print(z)
```
從[on_after_configure](https://docs.celeryproject.org/en/stable/reference/celery.html#celery.Celery.on_after_configure)處理程序中設置這些週期性任務意味著在使用`test.s()`的時候我們不會在模組級別中評估應用程式。注意到,[on_after_configure](https://docs.celeryproject.org/en/stable/reference/celery.html#celery.Celery.on_after_configure)是在應用程式(app)設置之後才派送的,因此應用程式(app)所宣告的模組之外的任務(像是位於[celery.Celery.autodiscover_tasks()](https://docs.celeryproject.org/en/stable/reference/celery.html#celery.Celery.autodiscover_tasks)的`tasks.py`)就必需要使用later signal,像是[on_after_finalize](https://docs.celeryproject.org/en/stable/reference/celery.html#celery.Celery.on_after_finalize)。
函數[add_periodic_task()](https://docs.celeryproject.org/en/stable/reference/celery.html#celery.Celery.add_periodic_task)會在背景新增項目到[beat_schedule](https://docs.celeryproject.org/en/stable/userguide/configuration.html#std-setting-beat_schedule),相同的設置也可以用於手動設置週期性任務:
範例:每30秒執行一次`tasks.add`:
```python
app.conf.beat_schedule = {
'add-every-30-seconds': {
'task': 'tasks.add',
'schedule': 30.0,
'args': (16, 16)
},
}
app.conf.timezone = 'UTC'
```
:::info
**Note:**
如果你想要知道這些設置應該放那,請參考[Configuration](https://docs.celeryproject.org/en/stable/getting-started/first-steps-with-celery.html#celerytut-configuration)。你可以直接在你的應用程式上設置這些選項,也可以保留單獨一個模組來做配置。
如果你想要在`args`使用`single item tuple`,請不要忘記構造函數是逗號,而不是一對括弧。
:::
你在排程中使用[timedelt](https://docs.python.org/dev/library/datetime.html#datetime.timedelta)意味著任務將會以每30秒的間隔派送(第一個任務會在`celery beat`啟動之後的30秒派送,接著每30秒派送)。
還有一個類似於排程器的`Crontab`,可參考下面[Crontab schedules](https://docs.celeryproject.org/en/stable/userguide/periodic-tasks.html#crontab-schedules)說明。
與`cron`一樣,如果上一個任務在下一個任務派出之前還沒完成,那就可能會造成任務的重疊。如果你會擔心這一點的話,那你應該使用鎖定策略來確保一次只會有一個實例可以執行。(參考範例[Ensuring a task is only executed one at a time](https://docs.celeryproject.org/en/stable/tutorials/task-cookbook.html#cookbook-task-serial))
#### Avaliable Fields
* task
* 預計執行的任務名稱
* schedule
* 執行頻率
* 可以是整數<sub>(整數即代表幾秒)</sub>,[timedelta](https://docs.python.org/dev/library/datetime.html#datetime.timedelta),或[crontab](https://docs.celeryproject.org/en/stable/reference/celery.schedules.html#celery.schedules.crontab)。你也可以自定義排程類型,相關接口的部份請參考[schedule](https://docs.celeryproject.org/en/stable/reference/celery.schedules.html#celery.schedules.schedule)
* args
* Positional arguments (`list` or `tuple`)
* kwargs
* Keyword arguments (`dict`)
* options
* 執行選項(`dict`)
* 可以是[apply_async()](https://docs.celeryproject.org/en/stable/reference/celery.app.task.html#celery.app.task.Task.apply_async)所支援的任意參數,`exchange`、`routing_key`、`expires`
* relative
* 如果`relative`為True,則[timedelta](https://docs.python.org/dev/library/datetime.html#datetime.timedelta)時間表"按時鐘"安排。這意味著頻率時間被四捨五入到最接近的秒、分鐘、小時或天,這取決於[timedelta](https://docs.python.org/dev/library/datetime.html#datetime.timedelta)設置的期間
* 預設情況下,`relative=False`,頻率的部份不會四捨五入,而是相對於celery beat開始的時間
### <span id='corntab'>Crontab schedules</span>
如果你想要對任務執行的時候有更多控制,舉例來說,特定的時間或一週特定的日期,你可以使用[crontab](https://docs.celeryproject.org/en/stable/reference/celery.schedules.html#celery.schedules.crontab):
```python=
from celery.schedules import crontab
app.conf.beat_schedule = {
# Executes every Monday morning at 7:30 a.m.
'add-every-monday-morning': {
'task': 'tasks.add',
'schedule': crontab(hour=7, minute=30, day_of_week=1),
'args': (16, 16),
},
}
```
`Crontab`表達式的語法非常靈活。
幾個範例:
| Example | Meaning |
| -------- | -------- |
| crontab() | 每分鐘執行 |
|crontab(minute=0, hour=0) |每天午夜12點執行|
|crontab(minute=0, hour='\*/3')|每三個小時執行: midnight, 3am, 6am, 9am, noon, 3pm, 6pm, 9pm|
|crontab(minute='\*/15')|每十五分鐘執行|
|crontab(day_of_week='sunday')|禮拜天的每分鐘執行|
|crontab(minute='\*', hour='\*', day_of_week='sun')|禮拜天的每分鐘執行|
|crontab(minute='\*/10', hour='3,17,22', day_of_week='thu,fri')|禮拜四、五的早上三點、下午五點、下午十點,每十分鐘執行|
|crontab(minute=0, hour='\*/2,\*/3')|每隔一小時,時/3的時間,即1am, 5am, 7am, 11am, 1pm, 5pm, 7pm, 11pm除外|
|crontab(minute=0, hour='\*/5')|時/5的時間執行,這意味執行時間是3點,非5點,因為3點在24小時制中是15,15/3=5|
|crontab(minute=0, hour='\*/3,8-17')|時/3的時間以及早上8點至下午5點執行|
|crontab(0, 0, day_of_month='2')|每個月第二天執行|
|crontab(0, 0, day_of_month='2-30/2')|每個偶數日執行|
|crontab(0, 0, day_of_month='1-7,15-21')|每個月的第一、第三週執行|
|crontab(0, 0, day_of_month='11', month_of_year='5')|每年的五月十一日執行|
|crontab(0, 0, month_of_year='\*/3')|每個季度第一個月的每日執行|
更多可參考[celery.schedules.crontab](https://docs.celeryproject.org/en/stable/reference/celery.schedules.html#celery.schedules.crontab)說明
### Solar schedules
如果你的任務需要根據日出、日落、黎明或黃昏來執行,你可以使用[solar](https://docs.celeryproject.org/en/stable/reference/celery.schedules.html#celery.schedules.solar):
```python
from celery.schedules import solar
app.conf.beat_schedule = {
# Executes at sunset in Melbourne
'add-at-melbourne-sunset': {
'task': 'tasks.add',
'schedule': solar('sunset', -37.81753, 144.96715),
'args': (16, 16),
},
}
```
參數非常簡單:`solor(事件, 緯度, 經度)`。
請務必使用正確的經緯符號:
| Sign | Argument | Meaning |
|------|-----------|---------|
| + | latitude | North |
| - | latitude | South |
| + | longitude | East |
| - | longitude | West |
可能有的事件類型如下:
|Event | Meaning|
|------|-----------|
|dawn_astronomical|在天空不再完全黑暗,黎明中出現一道署光的那一刻執行。這時候太陽是在地平線上18度的時間|
|dawn_nautical|Execute when there’s enough sunlight for the horizon and some objects to be distinguishable; formally, when the sun is 12 degrees below the horizon.|
|dawn_civil|Execute when there’s enough light for objects to be distinguishable so that outdoor activities can commence; formally, when the Sun is 6 degrees below the horizon.|
|sunrise|Execute when the upper edge of the sun appears over the eastern horizon in the morning.|
|solar_noon|當天太陽已經升到最高的時候執行|
|sunset|Execute when the trailing edge of the sun disappears over the western horizon in the evening.|
|dusk_civil|Execute at the end of civil twilight, when objects are still distinguishable and some stars and planets are visible. Formally, when the sun is 6 degrees below the horizon.|
|dusk_nautical|Execute when the sun is 12 degrees below the horizon. Objects are no longer distinguishable, and the horizon is no longer visible to the naked eye.|
|dusk_astronomical|Execute at the moment after which the sky becomes completely dark; formally, when the sun is 18 degrees below the horizon.|
`solar`所有的事件都是使用UTC計算,因此不受時區設置影響。
在極地區域,也許不會每天都有日出或日落。排程器可以處理這些狀況(例如,如果當天沒有日出,那日出的事件(sunrise event)在當天就不會執行)。唯一的例外是`solor_noon`,它被正式定義為太陽經過天體子午線的那一刻,即使太陽低於地平線也會每天發生。
曙暮光被定義為介於黎明與日出之間;在日落與黃昏之間。你可以根據你對曙暮光的定義來排程(民間、航海或天文)以及你是否希望事件在黃昏的開始或結束時進行,使用上面清單中的適合事件。
更多資訊請參考[celery.schedules.solar](https://docs.celeryproject.org/en/stable/reference/celery.schedules.html#celery.schedules.solar)
:::success
譯者註:這邊天文的太專業了,直接取機器翻譯。
:::
### Starting the Scheduler
啟動`celery beat`服務方式如下指令
```shell
$ celery -A proj beat
```
你也可以透過設置選項`worker -B`將beat崁入worker,對於不會有多個worker節點的情況來說是方便的,不過不常用,因此不建議用於生產環境:
```shell
$ celery -A proj worker -B
```
beat需要一個local端的文件來保存最後執行記錄,名稱預設為`celerybeat-schedule`,當然你也可以指定,也要記得設置好相對應文件目錄的寫入權限:
```shell
$ celery -A proj beat -s /home/celery/var/run/celerybeat-schedule
```
:::info
**Note:**
如果有背景執行需求的話,請參閱[Daemonization](https://docs.celeryproject.org/en/stable/userguide/daemonizing.html#daemonizing)
:::
#### Using custom scheduler classes
你可以在命令列中利用(['--scheduler'](https://docs.celeryproject.org/en/stable/reference/cli.html#cmdoption-celery-beat-S))來指定自定義排程器類別。
預設的排程器為[celery.beat.PersistenScheduler](https://docs.celeryproject.org/en/stable/reference/celery.beat.html#celery.beat.PersistentScheduler),它單純的在本地端[shelve](https://docs.python.org/dev/library/shelve.html#module-shelve)資料庫檔案中保存最後的的執行時間。
另外還有[django-celery-beat](https://pypi.org/project/django-celery-beat/)的擴展,它會把排程保存在Django資料庫中,提供方便的管理者接口來管理周期性任務。
安裝使用該擴展:
1. 使用pip安裝該模組:
```shell
$ pip install django-celery-beat
```
2. 在Django專案的`settings.py`中新增`django_celery_beat`模組到`INSTALLED_APPS`
```python
INSTALLED_APPS = (
...,
'django_celery_beat',
)
```
注意到,沒有`-`,只有底線`_`
3. 執行Django資料庫轉移,以便建立需求資料表:
```shell
$ python manage.py migrate
```
4. 起動`celery beat`服務
```shell
$ celery -A proj beat -l INFO --scheduler django_celery_beat.schedulers:DatabaseScheduler
```
注意:你也可以直接地把它加入做為[beat_scheduler](https://docs.celeryproject.org/en/stable/userguide/configuration.html#std-setting-beat_scheduler)的設置。
5. 至`Django-Admin`界面設置一些周期性任務。
## History
20190725_依據4.3版本說明調整
20211217_依據5.2版本說明調整