# 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版本說明調整