[Twisted] Part 14: When a Deferred Isn’t

本文由Dave的Part 14: When a Deferred Isn’t翻譯而成,你可以由Part 1開始閱讀這個系列的文章,也可以在這裡找到整個系列的目錄。

Introduction

在這個部分我們要學習Deferred類別的另一個特點。為了便於討論,我們將會在我們穩定的詩歌相關服務中再增加一個伺服器。假設我們有大量內部用戶端想要從相同的外部伺服器獲得詩歌。但是這個外部伺服器處理速度太慢,並且已經因為網際網路上無止盡的詩歌需求而負載過重。我們不想將所有的用戶端請求都送來這,再造成這個可憐的伺服器的問題。

所以我們將製作一個快取代理伺服器。當用戶端連接到代理伺服器後,代理伺服器會從外部伺服器取回詩歌,或者回傳先前取回的詩歌快取副本。然後我們可以將所有用戶端的連接指向代理伺服器,而我們對外部伺服器造成的負載就微不足道了。我們在圖30中說明了這個步驟:
30.a%2Bcaching%2Bproxy%2Bserver.png-Part 14: When a Deferred Isn’t

圖30.快取代理伺服器


考慮一下當用戶端連接到代理伺服器來取得詩歌時會發生什麼事情。如果代理伺服器的快取是空的,則代理伺服器在傳回詩歌給用戶端前必須等待(非同步的)外部伺服器回應。到目前為止,我們已經知道如何使用回傳deferred實例的非同步函式處理這個情況。另一方面,如果快取中已經有一首詩了,代理伺服器便可以無需等待的馬上送回這首詩。所以代理伺服器取得詩歌的內部機制有時是非同步的,有時是同步的。

那麼,如果我們要一個只有在某些時候是非同步的函式,我們該怎麼辦?Twisted提供了幾個選項,這些選項都依靠一個我們還沒在Deferred類別使用到的功能:你可以在回傳deferred實例給呼叫者之前觸發它。

可以這樣做是因為雖然你不能觸發deferred實例兩次,但是你可以在deferred被觸發後加入callback與errback。當你這樣做的時候,deferred實例只會從上次離開的地方繼續觸發callback/errback鏈。有一件事情要注意,已經觸發的deferred實例可能會馬上觸發新加入的callback(或errback,取決於deferred實例的狀態),也就是當你加入callback/errback的同時。

看一下圖31,它顯示了已經被觸發的deferred實例:
31.a%2Bdeferred%2Bthat%2Bhas%2Bbeen%2Bfired.png-Part 14: When a Deferred Isn’t

圖31.已經被觸發的deferred實例


如果我們在這個時候加入另一對callback/errback,deferred實例會馬上觸發這個新的callback,如圖32:
32.the%2Bsame%2Bdeferred%2Bwith%2Ba%2Bnew%2Bcallback.png-Part 14: When a Deferred Isn’t

圖32.有新callback的同一個deferred實例


新的callback(而不是errback)被觸發是因為前一個callback的結果是成功的。如果是失敗(拋出一個例外或回傳一個Failure物件),則會是新的errback被呼叫。

我們可以用twisted-deferred/defer-11.py的範例程式碼來測試這個新功能。閱讀與執行這個script來了解當你觸發deferred實例然後加入callbacks時它的行為。注意第一個範例中每個新的callback是如何被立即調用(你可以由print輸出順序中看出來)。

這個script中第二個範例展示我們如何用pause()暫停deferred實例,讓它不會馬上觸發callback。當我們準備好觸發callbacks時,我們呼叫unpause()。這實際上與當deferred實例的callback回傳另一個deferred時,deferred實例用來暫停自己的機制相同。幹的漂亮!

Proxy 1.0

現在讓我們來看看twisted-server-1/poetry-proxy.py中詩歌代理伺服器的第一個版本。由於代理伺服器既可以作為用戶端與伺服器,所以它有兩對Protocol/Factory類別,一個用於提供詩歌,一個用於從外部伺服器取得詩歌。我們將不會花時間去查看用戶端Protocol/Factory類別的程式碼,它們與之前的詩歌用戶端相同。

但在我們查看伺服器Protocol/Factory類別之前,我們會先看在伺服器端協定用於取得詩歌的ProxyService
class ProxyService(object):

    poem = None # the cached poem

    def __init__(self, host, port):
        self.host = host
        self.port = port

    def get_poem(self):
        if self.poem is not None:
            print 'Using cached poem.'
            return self.poem

        print 'Fetching poem from server.'
        factory = PoetryClientFactory()
        factory.deferred.addCallback(self.set_poem)
        from twisted.internet import reactor
        reactor.connectTCP(self.host, self.port, factory)
        return factory.deferred

    def set_poem(self, poem):
        self.poem = poem
        return poem

這裡關鍵的方法是get_poem。如果已經有一首詩歌在快取裡面,這個方法就回傳這首詩歌本身。另一方面,如果我們還沒有取得詩歌,我們會初始化到外部伺服器的連接,並回傳當取回詩歌時會觸發的deferred實例。所以get_poem是個只在某些時間是非同步的函式(譯註:快取有資料就會同步的回傳詩歌,沒資料就會以非同步的方式取得資料)。

你該如何處理像這樣的函式呢?讓我們看看伺服器端的協定與工廠類別:
class PoetryProxyProtocol(Protocol):

    def connectionMade(self):
        d = maybeDeferred(self.factory.service.get_poem)
        d.addCallback(self.transport.write)
        d.addBoth(lambda r: self.transport.loseConnection())

class PoetryProxyFactory(ServerFactory):

    protocol = PoetryProxyProtocol

    def __init__(self, service):
        self.service = service

這個工廠很簡單-它只儲存了對代理服務的參考,這樣協定實例就能呼叫get_poem方法。協定才是真正在做事的地方。協定使用一個在twisted.internet.defer模組中名為maybeDeferred的包裝函式,來代替直接呼叫get_poem。

maybeDeferred函式以另一個函式的參考,加上一些可選的引數(我們在這裡沒有使用)來呼叫該函式。然後maybeDeferred會實際呼叫該函式並且:

  • 如果該函式回傳deferred實例,maybeDeferred回傳相同deferred。
  • 或者,如果該函式回傳Failure物件,maybeDeferred回傳已經被用Failure物件觸發(透過errback)的新deferred實例。
  • 或者,如果該函式回傳常規值(regular value),maybeDeferred回傳已經被用該值作為結果觸發的deferred實例。
  • 或者,如果該函式拋出例外,maybeDeferred回傳已經被包裹著該例外的Failure物件觸發(透過errback)的deferred實例。

換句話說,maybeDeferred的回傳值保證是deferred實例,即使你傳入的函式永遠不會回傳deferred實例。這樣使我們可以安全的呼叫一個同步函式(即使這個函式因為例外而失敗),並將其視為一個回傳deferred實例的非同步函式。

注意1.不過這還是有一些細微的差別。同步函式回傳的deferred實例已經被觸發了,所以任何你加入的callbacks或errback會立即執行,而不是在reactor迴圈之後的迭代中執行。

注意2.事後看來,也許命名一個永遠回傳deferred實例的函式為「maybeDeferred」並不是最好的選擇,但沒辦法就是這樣了。

一旦協定得到真正的deferred實例,它就可以加入一些傳送詩歌給用戶端的callbacks,然後關閉連接。這就是我們第一個詩歌代理伺服器。

Running the Proxy

為了嘗試代理伺服器,先像這樣啟動一個詩歌伺服器:
python twisted-server-1/fastpoetry.py --port 10001 poetry/fascination.txt

然後現在像這樣啟動代理伺服器:
python twisted-server-1/poetry-proxy.py --port 10000 10001

它應該會告訴你它正在10000埠,替10001埠的伺服器代理詩歌。現在你可以將用戶端指向代理:
python twisted-client-4/get-poetry.py 10000

我們在這邊使用早期版本的用戶端,它不關心詩歌的轉換。你應該會看到詩歌出現在用戶端的視窗,以及一些在代理伺服器視窗的文字說明它正在從詩歌伺服器取得詩歌。現在再執行用戶端一次,代理伺服器應該會向你證實它正使用快取版本的詩歌,而用戶端應該顯示與以前相同的詩歌。

Proxy 2.0

如我們之前提到的,還有一個替代方法去實現這個方案。我們在twisted-server-2/poetry-proxy.py的2.0版本詩歌代理伺服器中來說明。由於我們可以在回傳deferreds之前觸發它們,所以當已經有詩歌在快取中時,我們可以讓代理服務回傳已經觸發的deferred實例。以下是代理服務的新版本get_poem方法:
    def get_poem(self):
        if self.poem is not None:
            print 'Using cached poem.'
            # return an already-fired deferred
            return succeed(self.poem)

        print 'Fetching poem from server.'
        factory = PoetryClientFactory()
        factory.deferred.addCallback(self.set_poem)
        from twisted.internet import reactor
        reactor.connectTCP(self.host, self.port, factory)
        return factory.deferred

defer.succeed函式是一種方便的方式,以一個成功結果來建立一個已經觸發的deferred實例。當你看到這個函式的原始碼時,你會發現它只是建立一個新deferred實例然後用callback()觸發它。如果我們想要回傳已經失敗的deferred實例(譯註:與已經觸發的deferred實例相反,deferred已經觸發完畢而且最後是errback回傳錯誤結果),我們可以用defer.fail代替。

在這個版本中,由於get_poem永遠會回傳deferred實例,所以協定類別也不再需要使用maybeDeferred(雖然使用maybeDeferred仍然有用,就像我們上面所學的):
class PoetryProxyProtocol(Protocol):

    def connectionMade(self):
        d = self.factory.service.get_poem()
        d.addCallback(self.transport.write)
        d.addBoth(lambda r: self.transport.loseConnection())

除了這兩個改動之外,第二版的代理伺服器其他部分就如第一版一樣,你可以像執行原始版本一樣執行第二版。

Summary

這個章節中,我們學習了deferred如何在回傳之前被觸發,因此我們可以在同步的(或者有時候是同步的)程式碼中使用它。而且我們有兩種方法可以做到這點:

  • 我們可以使用maybeDeferred來處理一個有時候會回傳deferred實例,而其他時間回傳常規值(或者拋出例外)的函式。
  • 或者,我們可以使用defer.succeed與defer.fail預先觸發我們的deferreds,所以無論如何我們的「半同步(semi-synchronous)」函式總是會回傳deferred實例

使用哪種技術取決於我們。前者著重於我們的函式並不總是同步的的情況,而後者使用戶端的程式碼更簡單。並沒有明確的論點說哪個比較好。

兩種技術都可以讓我們在deferred實例觸發後加入callbacks與errbacks。這就解釋了我們在Part 9發現的奇怪的情況,以及twisted-deferred/defer-unhandled.py這個範例。我們了解到deferred實例中最後一個callback或errback的「未處理的錯誤(unhandled error)」,會直到deferred被垃圾回收(即使用者程式碼中不再有東西參考到它)時才會報告出來。現在我們知道為什麼了-因為我們可以一直加入其他成對的callback到deferred實例中來處理錯誤,直到最後一個deferred實例的參考被刪除時,Twisted才能說錯誤沒有得到處理。

現在你已經花了很多時間去探索twisted.internet套件中的Deferred類別,你可能注意到它實際上與internet沒有任何關係。它只是一個用來管理callbacks的抽象。那它在那幹嘛?這是Twisted歷史的遺跡(artifact)了。在千萬可能世界的最美好的那個世界中(那個我在世界終極飛盤聯盟付了幾百萬美元去比賽的世界),defer模組可能在twisted.python套件中。當然,在那個世界裡,你可能忙於使用超能力打擊犯罪而無法閱讀這系列文章。我想這就是人生。(譯註:作者想表達的應該是「我不知道,它就是這樣了」)

那對於deferreds呢?我們終於知道所有它們的功能了嗎?對大部分的功能來說,我們是知道了。但Twisted包含了使用deferreds的替代方法,那是我們還沒討論到的(我們會討論到的!)。與此同時,Twisted的開發人員一直在增加新的東西。在即將發布的版本中,Deferred類別將有全新的功能。我們會在未來的部分中介紹它,但首先我們要在deferred這休息一下,並看看Twisted的其他特點,包括在Part 15的Twisted的測試。

Suggested Exercises

  1. 修改twisted-deferred/defer-11.py的範例以.errback()說明pre-failing deferrds。閱讀defer.fail()函式的文件與實現。
  2. 修改代理伺服器,丟棄快取超過兩小時的詩歌,讓下一個詩歌請求從伺服器重新請求詩歌。
  3. 代理伺服器應該避免多次聯繫伺服器,但如果當快取中沒有詩歌時多個用戶端同時請求,代理伺服器會建立多個詩歌請求。如果你使用慢速伺服器去測試可以很輕易地看到。
    修改代理服務,讓它只生成一個請求。現在該服務只有兩種狀態:這首詩歌要不就在快取中,要不就不在。你需要識別第三種狀況,已經請求完成但是還沒下載完成。在第三種狀態時呼叫get_poem方法,向「waiter」串列加入新的deferred實例。新的deferred實例將是get_poem方法的結果。當詩歌完成下載後,以詩歌觸發所有等待中的deferreds實例並轉變到快取狀態。另一方面,如果詩歌下載失敗,觸發所有等待中deferreds實例的.errback()方法,並轉變到非快取狀態。
  4. 加入一個轉換代理到代理服務中。這個服務應該向原本的轉換服務一樣運作,但是使用外部伺服器去進行轉換。
  5. 研究這個假設性的程式碼片段:
    d = some_async_function() # d is a Deferred
    d.addCallback(my_callback)
    d.addCallback(my_other_callback)
    d.addErrback(my_errback)
    假設當deferred實例d在第一行被回傳時,它還沒有被觸發。當我們在第二至四行加入我們的callbacks與errback時deferred是否可能觸發?為什麼或為什麼不可能?

留言