top of page
Search
  • Learn with Shin

Tenacity - 鍥而不捨的小幫手



想像我們在餐廳點餐的場景,如果剛好客人很多廚房很忙,或是服務生人手不足,可能偶爾會發生一些小問題,像是餐點被忘記或是送上不對的餐點,或是甚至沒人來。


大家或多或少有過這樣的經驗吧🙁?


如果發生這樣的情況,你會怎麼做?



再跟服務生講一次🙋🏻‍♂️,或叫到他來為止,是吧😂?


在網路的世界,偶爾也會發生這樣的事情。當你去Call一個API,發出一個要求(Client Request),對方的伺服器(Server)不見得每次會正確的給你回應。


不管基於什麼理由,當Request失敗的時候,往往我們會想要再試一次(Retry),或是不斷嘗試直到成功為止 -- 就跟我們點餐一樣...當然你也可以不爽走人😅


現實中,我們會需要用到Retry的情況其實不少。想想看,假設你今天想要上網登記打疫苗,結果衛福部Server一直塞車的時候,我們是不是會一直去按更新? 


這時候,如果有個自動重播(Retry)功能是不是就很方便(...當然如果大家都這麼做的話網路可能就爆掉了...)


講了那麼多,今天主要想來跟大家介紹一個好用的第三方Python Library - tenacity,來幫助我們解決這個問題!



模擬不安定的API

在講tenacity之前,我想先來做一個function模擬剛剛提到的點餐的情況。姑且就叫它order_meal_api


當你call這個order_meal_api的時候,有可能你會成功拿到你要的餐點,但是也有可能失敗,譬如說送錯餐或是等很久。


參考下面的Code:


稍微解說一下這個order_meal_api做的事情:

  • 這裡為了demo測試,我們用的誇張一點,有50%的機率你可能會拿到錯的餐。當拿到錯的餐的時候,我們就拋出一個Error - 可想而知,這會造成程式的中斷。

  • 同時我們還加上了等待餐點的時間。大部分的機會(6/7的機率)等餐會花上0-3秒,但是有可能(1/7的機率)會花到10秒~


現在我們可以直接去Call這個order_meal_api了。多用幾次試試看,有可能你會一下子就拿到正確的餐點,但也有可能等了很久還是拿到錯誤的餐點(Exception)😅。



OK~現在設置好場景了。

大家應該同意,order_meal_api是一個相當不可靠的服務吧⛈?



使用tenacity


為了應付這樣不穩定的服務,終於我們的tenacity要登場了!


安裝如下:

$ pip install tenacity==7.0.0

tenacity提供的主要功能很單純,就是retry(再度嘗試)!


如果我們只是要不斷嘗試到成功為止,最簡單的方法就是使用retry如下(節省篇幅,這裡我們就只秀出相關的code)

retry這個function主要以decorator的方式出現。(關於decorator我們之後可以寫一篇文章來更深入探討,這裡只要知道decorator的長相跟用法:就是有一個@的小老鼠在前面,並放在你定義的function上面,藉此對該function做特定的操作)


當我們在order_meal_api上面套上了retry之後,接下來去執行order_meal_api,就會發現不管花多少時間,最後一定會拿到正確的餐點!


換句話說,不會再有因為call這個function而發生Error,進而終止程式的問題。



Cool!就是這麼簡單!


但是,現實情況中,我可能不想要一直嘗試到天荒地老。如果我點了餐但是其實廚師跑去約會了,我可不想一直等到他爽完回來😡。


所以今天假設我只想給他三次機會,試了三次如果還是不行我就放棄...我可以用tenacity做到這一點嗎?


當然可以!只要利用tenacity提供的stop_after_attempt功能,並指定你想要的嘗試次數。

寫法如下:

stop_after_attempt會用在stop這個parameter,算是蠻特殊的使用方法...不過code本身應該蠻容易理解的吧。


那...如果說我很心急,譬如說叫了餐點但是超過一段時間他還不來我就要放棄,可以做的到嗎?


當然可以!這時候就利用stop_after_delay,來設定retry的等待時間。一旦超過設定的時間就會放棄嘗試。


如果想要同時設定retry的次數以及等待的極限,可以利用 " | " 將兩者結合:


這樣一來,我們就不會一直苦苦的retry,畢竟人的忍耐是有極限的😎


到這裡,我們知道,retry發生的點是當Error發生的時候對吧?


order_meal_api例子中,我們設定如果點餐失敗的話就會拋出IOError。要知道,python有許多不同種類的Error,如ValueError, KeyError, ZeroDivisionError...甚至是各種客製的Error。


如果我們只在意某個特定的Error,其實我們可以更進一步的去設定促使retry發生的Error種類喔!


這裡用retry=retry_if_exception_type(IOError),代表我們只在發生IOError的時候進行retry,如果發生的是其他的Error我們就不retry - 直接讓它失敗。


OK,這樣一來,我們基本的retry條件大概都齊全了:

  • retry最多三次

  • 等待時間最多5秒

  • 只針對IOError進行Retry。


不過,目前的情況是,當retry發生的時候,其實我們是不知道的。有時候嘗試了一次,有時候兩次,甚至三次...。如果我們在意到底失敗了幾次(譬如說好奇這家餐廳的服務有多穩定),tenacity有提供logging供我們使用(關於logging如果有興趣可以參考另一篇文章:logging-掌控你的程式)。



tenacity提供before_log跟after_log,argument的部分我們要給它的是logger物件(透過logging.getLogger產生),以及logging的level。


現在你去執行order_meal_api,


如果retry發生的時候,你就可能會看到類似下面的log:

DEBUG:__main__:Starting call to '__main__.order_meal_api', this is the 1st time calling it.
DEBUG:__main__:Finished call to '__main__.order_meal_api' after 0.001(s), this was the 1st time calling it.
DEBUG:__main__:Starting call to '__main__.order_meal_api', this is the 2nd time calling it.
DEBUG:__main__:Finished call to '__main__.order_meal_api' after 1.004(s), this was the 2nd time calling it.
DEBUG:__main__:Starting call to '__main__.order_meal_api', this is the 3rd time calling it.
DEBUG:__main__:Finished call to '__main__.order_meal_api' after 2.012(s), this was the 3rd time calling it.

這個log的例子顯示我們嘗試了三次(最後還是失敗了...),以及每一次的等待時間。


這樣一來是不是就很清楚了!


最後還有一點,當放棄retry而導致Error發生的時候,tenacity會拋出的不是原始的Error,而是tenacity特有的RetryError。所以如果我們想要原始的Error的話,得加上reraise=True的這一項參數。個人認為這是比較常用的作法。


OK,這樣一來就大功告成了✨!完整的code會貼在最下方供參考喔。



小結


今天介紹了tenacity這個第三方library,來幫助我們應付不穩定的API!


Tenacity除了提供retry以外,也讓我們可以控制:

  • retry的次數限制

  • 等待的時間限制

  • retry的Error類型

  • 是否要顯示(前後)logs

當然,tenacity還有提供其他功能來應對我們各種進一步的需求,如果有興趣可以參考官方的資料。


希望對大家在使用python上有幫助喔!


最後附上完整的example code供參考:





Comments


Post: Blog2_Post
bottom of page