# Celery_Application
###### tags: `celery` `celery 5.2` `python`
[官方連結_Application](https://docs.celeryproject.org/en/stable/userguide/application.html)
## Application
使用Celery之前必需實例化,這個實例化物件稱為Application(App)。
這個App是thread-safe<sub>(線程安全)</sub>的,因此可以多個不同配置(config)、組件(components)與任務(tasks)的Celery在相同的process space中共存。
先來建置一個吧:
```python
>>> from celery import Celery
>>> app = Celery()
>>> app
<Celery __main__:0x100469fd0>
```
最後一行文字顯示了應用程式的文本表示:包含app class(Celery)名稱,目前主要模組名稱(`__main__`),以及該物件的記憶體位置。
### Main Name
這當中只有一件事是重要的,就是主要模組名稱,我們來看為什麼。
當你在Celery中傳送一個任務訊息<sub>(task message)</sub>的時候它並不會包含任何的程式碼,只會有你要執行的任務名稱。這種作法類似於在網路世界中的hostname:每一個Worker都維護著一個任務名稱與實際function的映射,這稱為`task registry`(任務註冊?)。
在你定義任務的時候也會一併的將它加入`local registry`,我們來看一個例子:
```python=
>>> @app.task
... def add(x, y):
... return x + y
>>> add
<@task: __main__.add>
>>> add.name
__main__.add
>>> app.tasks['__main__.add']
<@task: __main__.add>
```
這邊你再次的看到`__main__`;當Celery無法偵測到function所屬module的時候即會以main module(主要模組)的名稱`__main__`來做為該任務名稱的開頭(前綴)。
這只會在下面情況發生:
1. 如果定義任務的模組做為程序執行
2. 如果應用程式是在python shell(REPL)中建立
下面這個例子,任務模組也被用來以`app.worker_main()`啟動Worker:
`tasks.py`:
```python
from celery import Celery
app = Celery()
@app.task
def add(x, y): return x + y
if __name__ == '__main__':
app.worker_main()
```
當這個模組被執行的時候,任務將會以`__main__`為前綴命名,但是當模組被其它程序載入,比如調用任務,這個任務將以`tasks`開頭來命名(模組實際名稱):
```python
>>> from tasks import add
>>> add.name
tasks.add
# 範例檔案西稱為tasks.py,因此名稱前綴為tasks
```
你可以為主要模組指定另外的名稱:
```shell=
>>> app = Celery('tasks')
>>> app.main
'tasks'
>>> @app.task
... def add(x, y):
... return x + y
>>> add.name
tasks.add
```
:::warning
See also:
* [Names](https://docs.celeryproject.org/en/stable/userguide/tasks.html#task-names)
:::
### Configuration
有幾個選項是你可以設置的,這將改變Celery的工作模式。這些選項可以在實作應用程式的時候直接設置,或者你可以使用專用的組態模組(configuration module)。
組態可以以[app.conf](https://docs.celeryproject.org/en/stable/reference/celery.app.utils.html#celery.app.utils.Settings)提供:
```python
>>> app.conf.timezone
'Europe/London'
```
你可以直接設置組態設定值:
```python
>>> app.conf.enable_utc = True
```
或者用更新的方法一次性的更性多個設定值:
```python
>>> app.conf.update(
... enable_utc=True,
... timezone='Europe/London',
...)
```
這個組態物件由按順序查閱的多個字典所組成:
1. 執行時候變更
2. 組態模組(如果有)
3. 預設的組態設置([celery.app.defaults](https://docs.celeryproject.org/en/stable/reference/celery.app.defaults.html#module-celery.app.defaults))
你甚至可以用[app.add_defaults()](https://docs.celeryproject.org/en/stable/reference/celery.html#celery.Celery.add_defaults)來新增預設來源。
:::warning
See also:
至[Configuration reference](https://docs.celeryproject.org/en/stable/userguide/configuration.html#configuration)參考完整的可用配置以及它們的預設值
:::
#### config_from_object
[`app.config_from_object()`](https://docs.celeryproject.org/en/stable/reference/celery.html#celery.Celery.config_from_object)由組態配置物件來載入配置。
它可以是一個組態模組,或是任意擁有組態屬性的物件。
需注意到的是,在呼叫[`config_from_object()`](https://docs.celeryproject.org/en/stable/reference/celery.html#celery.Celery.config_from_object)之後,所有之前的組態設置通通都會被重置,因此如果有額外組態設置需求的話,應該在這之後再執行。
##### Example 1: Using the name of a module
`app.config_from_object()`方法可以採用Python模組的完全合格名稱,甚至可以採用Python屬性的名稱,舉例來說:"celeryconfig","myproj.config.celery"或"myproj.config:CeleryConfig":
```python
from celery import Celery
app = Celery()
app.config_from_object('celeryconfig')
```
這個`celeryconfig`模組也許看起來像這樣:
`celeryconfig.py`:
```python
enable_utc = True
timezone = 'Europe/London'
```
只要可以成功import`celeryconfig`,應用程式就可以使用它。
##### Example 2: Passing an actual module object
你也可以傳入已導入的模組物件,但並不是那麼建議。
:::info
Tip:
建議使用模組名稱,因為這意味著在使用`prefork pool`時不需要序列化模組。如果你遇到配置問題或`pickle`錯誤,請試著改用模組的名稱。
:::
```python
import celeryconfig
from celery import Celery
app = Celery()
app.config_from_object(celeryconfig)
```
##### Example 3: Using a configuration class/object
```python
from celery import Celery
app = Celery()
class Config:
enable_utc = True
timezone = 'Europe/London'
app.config_from_object(Config)
# or using the fully qualified name of the object:
# app.config_from_object('module:Config')
```
#### config_from_envvar
[`app.config_from_envvar()`](https://docs.celeryproject.org/en/stable/reference/celery.html#celery.Celery.config_from_envvar)利用環境變數來取得組態設置模組名稱
舉例來說,從環境變數`CELERY_CONFIG_MODULE`取得要指定載入的組態配置模組名稱:
```python
import os
from celery import Celery
#: Set default configuration module name
os.environ.setdefault('CELERY_CONFIG_MODULE', 'celeryconfig')
app = Celery()
app.config_from_envvar('CELERY_CONFIG_MODULE')
```
然後,你可以指定要通過環境使用的配置模組:
```python
$ CELERY_CONFIG_MODULE="celeryconfig.prod" celery worker -l info
```
#### Censored configuration
如果你需要列印出組態資訊,作為debug或類似行為,可能還想過濾掉密碼和API密鑰等敏感信息。
`Celery`附帶幾個用於呈現組態配置的實用工具,其中一個是[`humanize()`](https://docs.celeryproject.org/en/stable/reference/celery.app.utils.html#celery.app.utils.Settings.humanize):
```python
>>> app.conf.humanize(with_defaults=False, censored=True)
```
這個方法將組態配置以表格字串回傳。預設只會包含預設組態配置有改變的部份,你也可以透過啟用參數`with_default`來取得內置的預設值。
如果要將配置轉為字典格式(dict),可以使用[`table()`](https://docs.celeryproject.org/en/stable/reference/celery.app.utils.html#celery.app.utils.Settings.table):
```python
>>> app.conf.table(with_defaults=False, censored=True)
```
請注意到,Celery不會移除所有敏感資訊,因為它僅使用正則表達式來搜索常用命名的密鑰。如果你新增客制化的設置包含了敏感資訊,則應該使用Celery標記為機密的命名規則。
如果名稱包含以下任何子字符,組態配置將被審查:
API, TOKEN, KEY, SECRET, PASS, SIGNATURE, DATABASE
### Laziness
應用程式實例是延遲執行(lazy)的,這意思是說,在被實際應用的時候它才會執行。
實例化Celery的時候只會做下面四個動作:
1. 建立一個應用於事件的`logical clock`實例
2. 建立任務註冊表(task registry)
3. 將自己設置為當前應用(如果`set_as_current`參數是`disabled`則不執行)
4. 執行callback function [`app.on_init()`](https://docs.celeryproject.org/en/stable/reference/celery.html#celery.Celery.on_init)(預設不做任何事情)
裝飾器[`app.task()`](https://docs.celeryproject.org/en/stable/reference/celery.html#celery.Celery.task)不會在定義任務時候建立任務,而是將任務的建立延遲到使用任務時或應用程序完成後才執行。
下面範例說明任務會一直到你使用它,或者是訪問一個屬性(此範例為訪問`repr()`)才建立:
```shell=
>>> @app.task
>>> def add(x, y):
... return x + y
>>> type(add)
<class 'celery.local.PromiseProxy'>
>>> add.__evaluated__()
False
>>> add # <-- causes repr(add) to happen
<@task: __main__.add>
>>> add.__evaluated__()
True
```
應用程式的`finalization(定案)`狀態對發生在顯式的呼叫[`app.finalize()`](https://docs.celeryproject.org/en/stable/reference/celery.html#celery.Celery.finalize)或隱式的調用`app.tasks`屬性之後。
完成物件之後將會:
1. 複製必須在應用程式之間共享的任務
* 任務預設為共享,但如果參數`shared`在裝飾器`app.tasks`被設置為`disabled`,那任務將私有化到它所綁定到的應用程式。
2. 評估所有待處理的任務裝飾器
3. 確認所有的任務都綁定到當前應用程式
* 任務綁定到應用程式,以便它們可以從配置中讀取預設值。
:::info
**The "default app"**
Celery並不總是有應用程式,過去只有一個基於模組的API,對於向後相容性,在Celery 5.0發佈之前,舊API仍然存在,不過已經被刪除了。
Celery總是建立一個特別的應用程式,`default app`,如果沒有其它自定義的應用程式,則使用`default app`。
`celery.task`已經不能用了。請使用app instance上的方法,而不是API上的模型:
```python
from celery.task import Task # << OLD Task base class.
from celery import Task # << NEW base class.
```
:::
### Breaking the chain
儘管可以依賴於當前設置的應用程式,但最好的作法還是將app instance傳給需要的對象。
我稱它為`app chain`,因為它根據被傳遞的應用程式所建立的`chain of instances`。
下面是比較不好的作法:
```python
from celery import current_app
class Scheduler(object):
def run(self):
app = current_app
```
相反的,下面作法將應用程式`app`視為參數:
```python
class Scheduler(object):
def __init__(self, app):
self.app = app
```
Celery內部使用函數['celery.app.app_or_default()'](https://docs.celeryproject.org/en/stable/reference/celery.app.html#celery.app.app_or_default),以便一切在基於模組的相容性API中工作
```python=
from celery.app import app_or_default
class Scheduler(object):
def __init__(self, app=None):
self.app = app_or_default(app)
```
在開發環境中可以透過設置環境變數`CELERY_TRACE_APP`以便在app發生`chain breaks`的時候拋出異常
```shell
$ CELERY_TRACE_APP=1 celery worker -l info
```
:::info
**Evolving the API**
從2009年之後,Celery有著很大的變化。
舉例來說,在開始時可以使用任何`callback`做為任務
```python
def hello(to):
return 'hello {0}'.format(to)
>>> from celery.execute import apply_async
>>> apply_async(hello, ('world!',))
```
或者你也可以建立一個`Task`類別來設置某些項目,或覆寫其它行為
```python
from celery.task import Task
from celery.registry import tasks
class Hello(Task):
queue = 'hipri'
def run(self, to):
return 'hello {0}'.format(to)
tasks.register(Hello)
>>> Hello.delay('world!')
```
後來,我們決定傳遞任意可調用函數是一種反面模式(anti-pattern),因為那很難用`pickle`以外的序列化方式,該功能在2.0的時候被移除了,並以`task`裝飾器取代:
```python
from celery import app
@app.task(queue='hipri')
def hello(to):
return 'hello {0}'.format(to)
```
:::
### Abstract Tasks
所有使用裝飾器[`app.task()`](https://docs.celeryproject.org/en/stable/reference/celery.html#celery.Celery.task)所建立的任務都將繼承它的基類[`Task`](https://docs.celeryproject.org/en/stable/reference/celery.app.task.html#celery.app.task.Task)。
你可以利用參數`base`來指定不同的基類(base class):
```python
@app.task(base=OtherTask):
def add(x, y):
return x + y
```
要建立自定義的任務類別,你應該繼承自`celery.Task`:
```python=
from celery import Task
class DebugTask(Task):
def __call__(self, *args, **kwargs):
print('TASK STARTING: {0.name}[{0.request.id}]'.format(self))
return self.run(*args, **kwargs)
```
:::info
**Tip:**
如果你覆寫`Task`的`__call__`,那調用`self.run`來執行任務的主體是非常重要。不要呼叫`super().__call__`。`celery.Task`的`__call__`是給你參照用的。如果要優化,那要展開到`celery.app.trace.build_tracer.trace_task`,如果沒有定義`__call__`,那這個呼叫就會在自定義的task class上執行。
:::
`neutral base class`是特別的,因為它尚未綁定任何指定的應用程式(app),一旦任務綁定到一個應用程式(app)上,它就會讀取組態配置來設置預設值,以此類推。
要實現基類,我們需要使用裝飾器[`app.task()`](https://docs.celeryproject.org/en/stable/reference/celery.html#celery.Celery.task)來建立任務:
```python=
@app.task(base=DebugTask)
def add(x, y):
return x + y
```
甚至可以透過調整[`app.Task()`](https://docs.celeryproject.org/en/stable/reference/celery.app.task.html#celery.app.task.Task)屬性來更改應用程式的預設基類(base class):
```python
>>> from celery import Celery, Task
>>> app = Celery()
>>> class MyBaseTask(Task):
... queue = 'hipri'
>>> app.Task = MyBaseTask
>>> app.Task
<unbound MyBaseTask>
>>> @app.task
... def add(x, y):
... return x + y
>>> add
<@task: __main__.add>
>>> add.__class__.mro()
[<class add of <Celery __main__:0x1012b4410>>,
<unbound MyBaseTask>,
<unbound Task>,
<type 'object'>]
```
## History
20190719_依據4.3版本說明調整
20211206_依據5.2版本說明調整