和少婦白潔一塊兒Thinking in Spec

第一講寫在了微博上,篇幅不長,講了一個api或function call返回error時要保證檢查順序的問題。node

這件事情看起來不重要,也很難打動你;因此須要講一個positive case來改變你對spec和測試的見解。編程

這也是我這兩天要寫的代碼,一個在服務器端copy/move一組文件或文件夾的功能,我是作nas的,在2018年書寫在1998年各大操做系統就完成的功能,不一樣之處在於,如今我們restful和microservice了。api

事實上咱們在過去的兩年寫了海量的相似的業務,各類異步併發過程組合,各類流的mux/demux,各類lazy和各類在併發狀況下的錯誤處理,隨着對併發狀態機組合的瞭解愈來愈深刻,常見的應用場景都沒什麼問題;直到遇到這個copy功能的實現。服務器

這個功能不一樣在哪裏呢?象文件批量上傳這樣的功能,系統對客戶端而言是blackbox,你內部有多少併發或串行組合狀態機,外部是看不見的,最可能是遇到錯誤時內部要優雅的停下來。restful

可是遞歸式的複製文件和文件夾不同,客戶端那裏蹲着一個充滿好奇心的靈長目動物,它想看見你的內部過程,或者至少是一部分;同時這個操做還不可避免的會遇到各類衝突和錯誤,同名的文件和文件夾,由於系統其餘用戶的併發操做致使的一些失敗,甚至是這個用戶忽然被管理員刪除了,諸如此類。併發

理論上它等價於一個狀態機把狀態暴露出來,可是這裏有個問題,外部的觀察實際上是異步的,內部的不少狀態存在自發遷移,黑盒測試的代碼在polling和拿到api結果時再想去assert系統數據(文件系統上的數據,不是服務器內部的狀
態),可能已經發生了變化。異步

可是業務上說,若是所以把服務端的行爲設計成串行和單步的,雖然得到了良好的可測試性,缺沒有了實用價值,效率犧牲太多。函數

因此這是一個很是好的如何specify系統行爲的問題;之因此用specify這個詞,是由於咱們要指定的仍然是黑盒意義上客戶端和服務端的合約(contract),而不是服務端內部的狀態機實現,若是是後者,咱們就是在說design了。高併發

咱們的目的是在系統的實用性和黑盒可測試性上找到一個平衡。沒有一個好的合約,就像一個定理缺少證實,你只好隨機的找一大堆test data和test case來試行爲和結果,俗稱quality assurance。性能


測試一個函數的基本邏輯是y = f(x),咱們先找到一個數學函數f,給定x就能計算出y,而後咱們去測代碼實現的f',檢查對於同一個x,它是否和咱們定義的數學函數給出的結果一致。

在這裏咱們剝離了測試數據x和系統行爲定義f,f即spec。

對於api測試,咱們把參數拓展爲[a,b,c,d]理論:

a: 系統預置狀態
b: 調用api的參數
c: api返回結果
d: 系統結束狀態

參數多了點,但本質是同樣的,系統的spec是存在f: (a,b) => (c,d),這是肯定的。


咱們來腦部一下加入程序跑一下應該獲得什麼結果。

系統的初始狀態是存在一個源文件夾src,一個目標文件夾dst,以及用戶須要從src中copy到dst中的一些entries,多是子文件夾,多是文件,這個列表不該該爲空,不然沒有業務意義。

對於測試程序而言,[src, dst, entries]是參數,即上面所述的b;a呢?a能夠理解爲在開始時src和dst的整個hierarchy。

好了咱們有了a和b,咱們期待什麼樣的c和d?

假如被測程序沒有任何限制,即遇到多少個命名衝突整個任務應該停下來的狀況,再假如沒有遇到文件系統訪問錯誤,這個操做到最後是完成了儘量多的能夠成功複製的文件和文件夾,可是也找到了全部遇到衝突沒法繼續的狀況,等待用戶決策。

在這個假設下咱們是可以獲得c和d的定義的,c能夠定義爲所有衝突狀況的列表,d是最終任務停下來時的結果。

這是一個spec嗎?咱們說是的,由於abcd有無歧義的定義,並且是能夠實現這個函數的,在src和dst兩個tree上visit一遍就能根據a/b計算出c/d。

可是這個spec有兩個問題:

  1. 它的粒度太粗了,只有開始和結束;
  2. 它有不肯定性;

若是你在任務中間去polling服務器,即便服務器只暴露出現命名衝突的文件夾和文件給客戶端,這個列表中的內容是穩定的,客戶端不操做其中的條目不會自行消失;可是條目的出現順序能夠是隨機的;這種隨機性在實現角度看沒有任何問題,徹底不至於由於亂序複製一個文件夾中的內容影響執行結果的正確性。

可是在合約角度看,它難以assert,一方面由於隨機性,另外一方面由於服務器不會停下來等待客戶端去assert。

去除隨機性很簡單,例如咱們能夠約定服務器端的執行必須按照depth first/previsit的順序進行,這對服務器知足功能正確性而言是沒必要要的spec,可是對於test它會讓測試代碼方便不少,只要沒有顯著的性能影響,咱們不介意把這樣一條rule寫到spec裏去。

但另外一方面會比較麻煩,如何讓服務器停下來容許客戶端assert?

熟悉生產者消費者模式的人一眼就能看出這裏須要一個調度器;若是不熟悉這個模式能夠這樣理解,在一個子任務完成後,代碼中會有繼續執行下一個任務的邏輯,只要在這裏插入過程,便可讓continuation中斷和恢復。

若是沒有額外的限制,這個continuation的實現方法不少:

  1. 你能夠pre-visit src tree,產生一個dst任務的列表,這個列表用調度器調度執行;
  2. 你也能夠爲src -> dst構造一個任務tree,這個continuation體如今了父子對象之間的event handler上;
  3. 你也能夠幾構造task tree,而後把調度器寫成visitor,不用隊列結構,知足前面加入的pre-visitor規則;

都OK,可是誰該win design呢?

使用全局調度器的1和3都是能夠容易知足stop任務執行的要求的。若是沒有一些錯誤處理要求1是OK的,若是遇到父文件夾發生文件夾丟失之類的上下文相關錯誤,使用tree結構處理更方便一些。

具體怎麼實現不是重點,這裏想強調的是:如何break複雜過程,讓更細粒度的testing/verification可行。

好了,有了這兩個新規則咱們再看一下問題:

首先,它應該提供一個stepping模式;在任務初始建立時處於stopped狀態;

對於建立任務的需求,咱們的a和以前同樣,b是建立任務的參數,c是返回的任務結果,這個時候什麼實質性的工做也沒有作,只是server端有個任務描述,處於stopped狀態;而d和a同樣。

而後咱們有了第二個客戶端動做,姑且稱爲step;step的意思是server端能夠根據調度規則選擇幾個任務開始,一直執行到這些任務完成,可是調度器不工做,因此不會有新的任務產生;step在完成時馬上返回內部任務狀態,它能夠列入spec,也能夠不列入,取決於你想在多大程度上assert系統行爲,但本質上這是灰盒的。

第三個客戶端動做是watch;服務器在收到客戶端的watch調用時,若是已經完成了一個step的全部操做,進入stopped狀態,則馬上返回狀態描述;若是沒有,它應該等到step完成時再返回,這樣對客戶端來講比較容易使用。

step-watch構成了一個cycle,若是不想觀察step以後哪些任務在執行的狀態,step-watch能夠合併成一個api調用。無論怎樣,咱們獲得了一個細粒度的assert能力,在每次step-watch結束時,能夠assert c和d了。由於server停下來了,d能夠assert。


那麼在這個設計下,服務器端的每次併發多少,是一個參數,或者說policy,若是不區分文件和文件夾併發數設置爲1,它就退化成了順序執行;實際使用中會用到的併發限制,對node.js而言,建立文件夾能夠是個很大的值甚至不作限制,若是你不介意在任務完全失敗時預先建立了海量的空文件夾的話;複製文件高併發沒有意義,瓶頸在磁盤io那裏,通常2個併發就夠了。

因此如今你能腦補出來的執行過程是:每次step,調度器要按照previsit原則填充指定的任務,當他們結束時,不管是遇到衝突仍是成功完成任務,結果都是容易預先計算的。

可是還有一個問題:這樣一步一步執行的結果,真的和實際使用時,每一個子任務結束時都kick調度器的結果同樣嗎?能保證這一點嗎?

這須要把調度器的要求再提升一點:即便系統不處於stopped的狀態,仍然能夠調度,離併發限制差多少就該填充多少任務進去。在這種狀況下,你能夠連續step屢次最終只watch一次。每次step的結果能夠assert c,最終的watch結果能夠assert c和d。

這是final的保證嗎?也不是,除非你真的能坐下來給一個數學上的proof,不然都不算能證實其正確性。咱們仍然或多或少的須要不那麼可靠的直覺。

可是把spec強化到這個程度,對我來講,它的每次執行能夠算是很容易預測(計算)出來的,這意味着step by step的spec函數能夠書寫、易讀、且狀態打印結果是比較易懂的。在工程上,這已經很好了。


說一個小問題。

好比在SRS文檔上寫了一個限制,在整個任務遇到5個衝突時應該停下來。

這個限制多是靈長目客戶出了錢要必定實現的;實現也不難,調度器調度的時候要考慮還剩多少個能夠衝突的併發可用,而不是預設的併發數限制,換句話說,若是已經有4個衝突了,這個程序就只能墮落成one by one的順序執行了,由於若是併發了兩個,兩個都衝突,這條愚蠢的限制就無法meet了。

可是若是這個限制是假裝成靈長目客戶的另外一個靈長目同事提出的(俗稱產品經理),你最好跟他商量商量,手裏要拎着一個棒子,上面刻着persuader。由於這種需求就是在沒有推敲細節時拍腦殼想出來的,它沒考慮對性能的影響。嚴格遵照這個5沒有什麼意義,除非團隊用易經指導編程。


說完了。小結一下:

  1. spec和design是緊密相關的;
  2. design的自由度是很大的,對它增長限制條件,以得到spec意義上的可測試性,是考量設計好壞的重要指標;
  3. 要讓spec函數能夠寫出來和容易寫出來,即便你如今代碼忙得沒空寫,可是不要搞成將來須要寫的時候所有重寫生產代碼;

Matias Duarte說:

Design is all about finding solutions within constraints. If there were no constraints, it is not design - it's art.

對於代碼來怎麼理解這句話呢?就是不要把代碼怎麼寫都是能夠的掛在嘴邊,找到那些你還沒發現的constraint,纔是區分好的design和壞的design的關鍵;在這篇文章裏,將的就是從spec/testing的角度去經過加入更多的constraint,讓design變得更容易test/verification。

Tony Hoare說:

There are two ways of constructing a software design: one way is to make it so simple that there are obviously no deficiencies, and the other way is to make it so complicated that there are no obvious deficiencies. The first method is far more difficult.

Hoare說的so simple that there are obviously no deficiencies怎麼理解呢?如何作到呢?就是你的spec合約如此的簡單,即便對於複雜行爲實現,你仍然能夠break it down,用概括法獲得足夠簡單的驗證方法 - 尤爲是在遇到複雜問題,在實現層面難以簡化的時候。

相關文章
相關標籤/搜索