Celery Best Practice 筆記

邊看這篇 Celery – Best Practices 邊做的簡單摘錄與筆記。

  1. 不要用資料庫當作 AMQP Broker。Celery 會建立數個 process 去 poll 資料庫來檢查是否有新的工作,這會導致資料庫的 disk I/O 增加,也會增加對資料庫的連接數目。
  2. 使用更多佇列 (不要只用一個)。
    並不是所有的 task 執行時間、次數跟權重都一樣,例如不重要的 task A 可能會執行很多次,但比較重要的 task B 只有零星幾個。一個佇列會導致 Celery 依序執行佇列裡的工作,所以前面可能會排了很多 task A 工作,就多花了許多時間執行,反而重要的 task B 工作延後了。依照 task 分佇列擺放,可以避免這樣的情況。
  3. 使用權重。Celery 可以針對佇列設定權重與分流,可以使用專門的 worker 來服務指定的佇列,讓 task 得到更好的服務。
    
       CELERY_QUEUES = (
        Queue('default', Exchange('default'), routing_key='default'),
        Queue('for_task_A', Exchange('for_task_A'), routing_key='for_task_A'),
        Queue('for_task_B', Exchange('for_task_B'), routing_key='for_task_B'),
       )
       CELERY_ROUTES = {
        'my_taskA': {'queue': 'for_task_A', 'routing_key': 'for_task_A'},
        'my_taskB': {'queue': 'for_task_B', 'routing_key': 'for_task_B'},
       }
    
    
       celery worker -E -l INFO -n workerA -Q for_task_A
       celery worker -E -l INFO -n workerB -Q for_task_B
    
  4. 使用 Celery 的錯誤處理機制。task 可以指定這些參數 default_retry_delay=300, max_retries=5 來指定重試間隔與重試次數。task 裏面只要使用 try…except 跟 self.retry 就可以了。
    
       @app.task(bind=True, default_retry_delay=300, max_retries=5)
       def my_task_A():
         try:
           print("doing stuff here...")
         except SomeNetworkException as e:
           print("maybe do some clenup here....")
           self.retry(e)     # Retry!
    
  5. 使用 Flower。這是一個只要裝上就能使用的 Module,可以用來觀察 Task/Queue 的狀況。
  6. 只有在真的需要時,才保留執行結果。不需要的話,就加上 CELERY_IGNORE_RESULT = True,Celery 會自動丟棄結果。
  7. 不要傳遞 ORM 物件給 task。這是因為 Celery 是用 serialization 方式來傳遞參數到別的 Process (Task 是在其他的 Process 上執行),預設可以使用 pickle, cPickle, JSON, YAML ,但是 serialization/deserialization 是有負擔的,而且不保證所有狀態都能保存,建議最好是 pure 的物件或是用整數、字串等比較不容易出狀況的型態。

Celery 的 autodiscover_tasks

跟 Django 的整合可以參考 First steps with Django

裏面會要求你在 django app 的目錄下新增一個 celery.py,這裡有一行 app.autodiscover_tasks(lambda: settings.INSTALLED_APPS) ,這行就是找到所有 tasks 的關鍵。找不到 task 的話,執行 python manage.py celeryd 時,不會有錯誤訊息,只在程式要執行這些 task 時印出錯務訊息,說找不到。

原始碼是在 celery/app/base.py 裡,大致就是依照 INSTALLED_APPS 列出的 package 去找 tasks,有的話就 import 進來。如果你的 celery task 沒有列在 INSTALLED_APPS 裡,或是函式不在 tasks 裡的話,可以再多加 app.autodiscover_tasks,例如 app.autodiscover_tasks([‘your_module’], related_name=’my_tasks’) ,這樣就可以引入使用了。

 

keyring

試用 python keyring 這個模組的一些紀錄:

  • 用法就這麼簡單:keyring.set_password(service, username, password) 或 keyring.get_password(service, username, password)
  • keyring password 的長度幾乎沒有限制,至少到 8192 個字元是沒問題。
  • keyring 的密碼存放在哪裡? 可以看看 keyring.get_keyring().file_path 。如果想換位置,沒問題,有兩個方法:
    1. 設置 XDG_DATA_HOME 這個環境變數,這比較簡單
    2. 把 keyring.util.platform_ 裡的 data_root 與 config_root 這兩個函式替換掉。
  • 要有加密功能的話,安裝 pycrypto ,backend 會自動替換為 EncryptedKeyring ,預設是 PlaintextKeyring 。
  • 要看有哪些 backend ,可以看 keyring.backends ,除了檔案為基礎的 PlaintextKeyring、EncryptedKeyring 以外,還有其他的可用。
    目前真正能用的 backends ,是用 keyring.backend.get_all_keyring()
  • 替換 backend 則是用 keyring.set_keyring() ;看目前的 backend 是用 keyring.get_keyring()。

Django model 的 validate

同事問,為什麼用 model 建立 instance 以後,呼叫 save 沒有觸發 validate?

上網找了一下,大致有兩種方法:

  1. 用 ModelForm 的 is_valid() 做檢查,Correct Way to Validate Django Model Objects?
  2. 覆寫 model 的 save(),Django model mixin to force Django to validate (i.e. call `full_clean`) before `save`

事情沒有這樣結束,因為很好奇 Django 的作法,就去追 Code。

如果是走 is_valid() :

  1. forms/models.py:ModelForm 繼承自 BaseModelForm,而 BaseModelForm 又繼承自 BaseForm,這個檔案裡,沒有看到 is_valid
  2. forms/forms.py:
    1. BaseForm.is_valid() 很簡單的只回傳 self.is_bound and not self.errors ,self.is_bound 跳過不看,self.errors 是一個 property,裏面呼叫了 self.full_clean()
    2. self.full_clean() 呼叫了 self._clean_fields(), self._clean_form() 與 self._post_clean() ,關鍵在 self._post_clean() 。BaseForm 裡的 _post_clean() 是空的,讓繼承 BaseForm 的類別去決定。
  3. forms/models.py: 回頭看 BaseModelForm._post_clean(),這裡呼叫了 self.instance.full_clean() ,並處理 ValidationError 。instance 就是 model 類別所創出的實體,Model 繼承自 ModelBase 所以接著看 ModelBase 類別。
  4. db/models/base.py: ModelBase 的 full_clean() 呼叫了 self.clean_fields(), self.clean() 與 self.validate_unique ,並且在這裡處理了 ValidationError 例外,最後判斷了 errors 裡是否有東西,有的話,丟出 ValidationError。先看 self.clean_fields() ,函式裡去取得定義在類別裡的欄位,並且呼叫每個欄位 (Field) 類別的 clean() 函式。如果有錯誤就丟出 ValidationError()
  5. db/models/fields/__init__.py:Field 類別定義在這個檔案裡,裏面的 clean() 呼叫了 self.validate() 、self.run_validators()

至此,ModelForm 的路徑就確定了。

來看看第二個方法,他是要求 Model 類別除了繼承 Django Model 之外,再繼承 ValidateModelMixin 這個自訂類別以覆寫 save() ,看看裏面的 save(),就只是多呼叫了 full_clean() 來檢查,也就是走上述步驟 4 以後的路線。

Django 官方網站不建議直接去呼叫 full_clean() 這件事,不過找了一下,也沒找到什麼更好的解法~

P.S.

pyenv, IronPython 與 pip

會去追 code 主要是試著解決 Linux 下的 pip 不能用 IronPython 的問題,後來是沒解,但這是在去年年底時看的,我不知道現在是不是已經解決了。

pyenv install -l 這指令,大致做下面幾件事情:

  • 呼叫 .pyenv/plugins/python-build/bin/pyenv-install
  • pyenv-install 又呼叫 .pyenv/plugins/python-build/bin/python-build –definitions
  • python-build –definitions 實際上是把 .pyenv/plugins/python-build/share/python-build 下的資料夾都列出來

因為 IronPython 2.7.5 才支援 pip,想要用 IronPython 2.7.5 (或更新的版本) ,可以這麼做:複製 .pyenv/plugins/python-build/share/python-build/ironpython-2.7.4 為 ironpython-2.7.5,把裏面的下載位址改為 https://github.com/IronLanguages/main/releases/download/ipy-2.7.5/IronPython-2.7.5.zip
這樣子,pyenv install -l 就會列出 ironpython-2.7.5,而且也可以下載安裝。

安裝 2.7.5 以後,用 pyenv shell ironpython-2.7.5 切換為 IronPython 2.7.5。
依照 2.7.5 的 release note 裡去執行 python -X:Frames -m ensurepip 時,卻出現以下的錯誤:

OSError: IronPython.Runtime.Exceptions.OSException: cannot load library 
  at IronPython.Modules.CTypes.LoadLibrary (System.String library, Int32 mode) [0x00000] in <filename unknown>:0 
  at IronPython.Modules.CTypes.dlopen (System.String library, Int32 mode) [0x00000] in <filename unknown>:0 
  at Microsoft.Scripting.Interpreter.FuncCallInstruction`3[System.String,System.Int32,System.Object].Run (Microsoft.Scripting.Interpreter.InterpretedFrame frame) [0x00000] in <filename unknown>:0 
  at Microsoft.Scripting.Interpreter.Interpreter.Run (Microsoft.Scripting.Interpreter.InterpretedFrame frame) [0x00000] in <filename unknown>:0

會有這樣的錯誤,最主要是因為 ironpython-2.7.5/bin/Lib/ctypes/init.py 裡的 436~441 行,這裡是這樣寫的:
[python]
if _os.name in (“nt”, “ce”):
pythonapi = PyDLL(“python dll”, None, _sys.dllhandle)
elif _sys.platform == “cygwin”:
pythonapi = PyDLL(“libpython%d.%d.dll” % _sys.version_info[:2])
else:
pythonapi = PyDLL(None)
[/python]

這邊傳了 None 到 PyDLL,而 PyDLL 裡 (就 353 行) 的 _dlopen() 無法開啟 None ,才丟出這個 exception,有試著加程式處理掉這個 exception,但是,後續會因為這行
memmove = CFUNCTYPE(c_void_p, c_void_p, c_void_p, c_size_t)(_memmove_addr) 而出現 SystemError: LocalAlloc 錯誤

結論,Linux 下,IronPython 還是沒辦法用 pip ,另外就是,我不知道現在修正了沒。

Python InsecurePlatformWarning

碰到這個歡樂的錯誤,其實已經碰過兩三次了,前面幾次都不了了之。

InsecurePlatformWarning: A true SSLContext object is not available.

這次是確實的找到方法可以不用改程式避掉的方法,方法很簡單,就是安裝 pyopenssl ndg-httpsclient pyasn1 這幾個模組,這幾個模組會自動將 SSL 相關的憑證注射到 urllib3 模組裡,下載時就不會有 InsecurePlatformWarning 的警告。

這方法是在 StackOverflow 的 python – InsecurePlatformWarning: A true SSLContext object is not available. This prevents urllib3 from configuring SSL appropriately 看到解法的,感謝。

用 pyenv 安裝指定版本時的注意事項

今天用 pyenv 安裝了 3.4.2,卻發現沒有 tkinter 模組,經過一番明查暗訪,終於知道原因。原因就是沒安裝 tk8.5-dev (我是用 12.04,14.04 要改為 tk8.6-dev),用 apt-get 裝上 tk8.5-dev,然後重新用 pyenv 安裝一次 3.4.2 就可以了:pyenv install 3.4.2 。

換言之,用 pyenv 安裝特定版本的 Python 時,會因為當前環境是否有安裝必要函式庫的 header 而影響能使用的模組,以後要特別注意。

mod_wsgi, flask 與 python3

我是在 OpenShift 上遇到的情況,用 python3 + flask 寫的 web app 沒辦法啟動,用 rhc tail 出現這樣的錯誤訊息:

‘module’ object has no attribute ‘__loader__’

用 Google 找,在 StackOverflow 上找到這篇 flask – cannot setup apache 2.2 with mod_wsgi and python 3.3? ,然後跟著提供的線索看到這篇:Problem debugging Flask under Python 3.3 ,謝謝 Eric 提供的 workaround 。

workaround 是這樣的,在初使化 Flask(name) 時,加上 instance_path 參數,所以我就改為 Flask(name, os.path.join(os.environ[‘OPENSHIFT_PYTHON_DIR’], ‘instance’)) ,就解決了。

問題是出在 flask.Flask 的初始函數裡,Flask.init 在 instance_path 參數為 None 時,會呼叫 auto_find_instance_path() 來取得 instance_path 的值,而 auto_find_instance_path() 又呼叫 flask.helpers 的 find_packages(),find_packages() 使用了 pkgutil.get_loader() 來尋找 module 而導致了錯誤發生。

我在想,或許添加適當的 init.py 也可以解決,但就懶得做實驗了,先這樣。

依照片拍攝日期來分類到”年/月/日”資料夾裡

剛好要整理照片,又懶得手動整理,就寫了 script 來處理。照片裡的資訊是放在圖片裡,也就是 EXIF,網路上已經有好心人寫好 exifread 模組,所以只要用 pip 裝上使用即可。想說只是簡單的程式,就沒使用 argparse 來解析參數了,第一個參數帶 *.jpg 或 *.png,第二個參數帶要複製過去的資料夾,例如 python photo_classifier.py *.jpg d:\tmp 即可。

Python patterns – Visitor

開始來看 Python patterns,第一個看的是 Visitor。

  1. __mpro__ :這個內建的隱藏屬性可以列出父類別以及其上的所有類別,程式利用這個來取得繼承樹,並進行訪問。這部分的說明可以參考 What does “mro()” do in Python? – Stack Overflow
  2. 程式利用 getattr() 先去查看類別是否有實作 visit_xxx 方法,如果有就呼叫,如果沒有,才呼叫 generic_visit 的方法。

這跟 Visitor Pattern 似乎不太一樣,作者也在 Extrinsic Visitor Pattern in Python with support for Inheritance – Peter Hoffmann 裡說了,這是一個變形過的 Pattern。

如果要 Python visitor 的範例,可以參考 PythonWise: Visitor Design PatternThe Visitor Pattern in Python

補充:後來看了 ast module 以後,其實作者提的就跟 ast module 裡一模一樣。主要還是應用在 Tree 上。