Piranha介紹:過期代碼自動刪除的開源工具

在Uber,我們使用功能標誌(feature flags)定製移動應用程序的執行,爲不同的用戶組提供不同的功能。例如,這些標誌允許我們將用戶的體驗本地化到我們操作的不同區域,更重要的是,我們可以逐步向用戶推出功能,並嘗試同一功能的不同變體。

然而,在一個特性被100%地發佈給我們的用戶或者一個實驗性的特性被認爲是不成功的之後,代碼中的特性標誌就過時了。這些非功能特性標誌可以說是技術“債務”,使開發人員難以處理代碼庫,並且可能會使我們的應用程序膨脹。爲了應對,可能需要進行一些不必要的操作,影響用戶的性能體驗,甚至影響整個應用程序的可靠性。

對於工程師來說,消除這些“債務”可能會耗費大量時間,從而影響功能的開發。

爲了實現這一過程的自動化,我們開發了Piranha。這是一個通過掃描源代碼,刪除過時的,功能標誌相關的,代碼優化工具。它可以帶來更乾淨、更安全、更高效和更可維護的代碼庫。在Uber的Android和iOS版代碼的持續更新過程中,我們嘗試運行Piranha,並使用它刪除了大約2000個過時的功能標誌及其相關代碼。

我們相信Piranha會是一個很棒的工具,尤其適用於那些在APP部署中使用了功能標誌等的組織機構。因此在介紹工具的同時,也開放了它的源代碼(https://github.com/uber/piranha)。當前版本支持Objective-C、Swift和Java,開源貢獻者可嘗試使Piranha支持更多的語言,或者提高其執行深層代碼重構的能力。

功能標誌(feature flags)和它的生命週期

爲了引入標誌,開發人員在Uber的標誌管理系統中創建一個條目,並輸入諸如標誌名稱、標誌類型、目標推出百分比、目標平臺和標誌運行的位置等屬性。此外,在源代碼中手動引入了標記,從而在實驗平臺的標記和移動應用程序的實例之間建立了一致的關聯。從那時起,這個標誌作爲代碼中的變量來管理應用程序的行爲。

從正在運行的應用程序的角度來看,功能標誌是一個“鍵”,映射到兩個或多個條件之一,如開/關、顏色值、大小和複製文本。啓動時,移動應用程序通過網絡查詢標誌管理系統,並檢索當前實例每個標誌的特定處理條件。返回的值決定了應用程序的功能和行爲。

在最簡單的情況下,當逐步推出單個功能時,我們有一個控制(control)條件(該功能未啓用)和一個處理(treatment )條件(該功能已啓用)。最初我們傾向於將處理條件應用於一小部分用戶,如果推廣成功,則逐漸擴大應用範圍,以涵蓋與功能相關的所有用戶(例如,特定地理位置的每個人)。如果在推出過程中出現問題,我們可以停止和回滾,確保對用戶的影響最小。

我們的系統還可以處理同一功能的各種不同實現,例如在不同的用戶集上測試不同的接口(例如A/B測試)。

許多功能最終在全球100%的用戶中推出。有時,我們會在代碼中保留功能標誌來保護該功能,作爲非關鍵應用程序功能的“安全終止開關”。這樣一個小功能中的潛在bug或崩潰,就可以通過關閉這個“開關”來輕鬆應對(否則可能會毀掉整個應用)。但是,由於大多數功能嵌套在其他功能下,因此此類主動的“安全終止開關”並不是大多數功能標誌的最終狀態。

我們認爲與100%覆蓋的功能相關聯的功能標誌都是“過時的”,這意味着標誌本身不再起作用,可以用完整版本的硬編碼來代替。類似的情況也發生在實驗或處理條件下,這些實驗或處理條件已經完全恢復到其控制(即無特徵)狀態。

過期標誌導致的技術“債務”

當標誌過期時,應在功能標誌管理系統中禁用該標誌,並且需要從源代碼中刪除與該標誌相關的所有代碼構件,包括對應功能的其他不可用版本的實現。這確保了改進的代碼衛生性並避免了技術債務。

然而實際上,開發人員並不總是執行這個簡單的事後清理過程,因而留下與過期標誌相關的代碼,導致技術債務的累積。與這些代碼的存在會影響多個維度的軟件開發。首先,開發人員必須對與這些過時標誌相關的控制流進行推理,並處理大量無用的代碼。其次,在意外情況下(例如,由於標記管理後端錯誤),此類代碼仍然可以執行,從而降低了應用程序的總體可靠性。然後,測試中還需要保證這些無用路徑的覆蓋率。最後,無用代碼和無用測試的存在會影響整個構建和測試時間,從而影響開發人員的生產力。

自動刪除過期標誌相關的代碼

爲了解決過期標誌帶來的技術“債務”問題,我們設計並實現了這個自動的源代碼到源代碼(source-to-source)重構工具Piranha。該工具用於自動生成差異修訂(https://secure.phabricator.com/book/phabricator/article/differential/)(類似於,diff)以刪除與過期特徵標誌對應的代碼。Piranha會把標誌的名字、預期的處理方式和標誌作者的名字作爲輸入。它分析程序的抽象語法樹(https://en.wikipedia.org/wiki/Abstract_syntax_tree)(AST)以生成適當的重構,這些重構被打包成一個diff。diff被分配給標誌的作者以供進一步檢查,他可以按原樣提交(提交到master分支),或者在提交之前執行任何其他修正。我們還圍繞Piranha 構建了工作流,以便它能夠以配置的方式定期刪除過期的代碼。

特徵標誌示例

接下來,通過一個簡單的例子來說明Uber源代碼中特徵標誌的基本用法。

最初,我們在RidesExpName類的標誌列表中定義一個名爲RIDES_NEW_FEATURE 的新標誌,並將其註冊到標誌管理系統中。隨後,我們使用特徵標誌API(isTreated)將其寫入代碼,並分別在if和else分支下做了對應處理/控制行爲的實現:

public enum RidesExpName implements ExpName {
   RIDES_NEW_FEATURE,
   …
}

if (experiments.isTreated(RIDES_NEW_FEATURE)) {
     // implementation for treatment (on) behavior
} else {
     // implementation for control (off) behavior
}

爲了測試不同的標誌值,對於每個單元測試,我們可以添加一個註解(annotation )來指定特徵標誌的值。例如下面的示例,當該標誌處於treated對應的狀態時test_new_feature將運行:

@Test

@RidesExpTest(treated=RidesExpName.RIDES_NEW_FEATURE)

public void test_new_feature() {

   …

}

當RIDES_NEW_FEATURE過期時,需要從代碼庫中刪除與之相關的所有代碼。這包括:

1.RidesExpName中的定義。

2.它在已創建的API中的用法。

3. @RidesExpTest的註解。

此外,必須刪除else分支的內容,該分支實現了現在不再訪問的控制行爲。我們還想刪除涉及此刪除行爲的任何測試的代碼。

不刪除這些代碼???這些代碼組件會逐漸增加源代碼的複雜性,影響總體的可維護性。

自動化的挑戰

可以預料,在自動檢測過期標誌和相關代碼刪除方面存在許多困難。從確定標誌是否正在使用到標誌的所有者,再到其代碼細節的實現。克服這些挑戰是Piranha的關鍵。

過期標誌

確定一個標誌是否過期是非常重要的。首先,作爲某種功能或控制,相關的標誌應該被百分之百地上線。否則則可能意味着它的實驗仍在進行中。即使當它已經上線時,開發人員可能還沒有準備好消除這個標誌。例如,標誌可以用作終止開關或用於監視調試信息。因此,即使標誌已完全上線,它們仍可能在使用中。

標誌所有權

在Uber早期的發展過程中,確定過期標誌的所有權信息變得非常困難。即使我們可以完全確定標誌的作者身份,該作者也可能已經異動到另一個團隊或離開了公司。

代碼風格

與功能標誌相關的代碼沒有任何標準限制,這增加了自動化工具設計的複雜性。例如,與標誌相關的代碼的helper函數很難與代碼中的任何其他函數區分開來。此外,有些測試中開發人員允許對標誌進行手動狀態更改,這類測試的複雜性可能會限制工具執行全面的清理。例如,當與標誌相關的代碼正在進行單元測試時,有時不清楚是否可以完全放棄測試,因爲相關功能已被刪除。有時也需要刪除測試主體中的特定狀態的更改,以便可以繼續測試剩餘的功能。

用靜態分析法構建Piranha

基於以上背景知識和分析,我們可以通過靜態分析(https://en.wikipedia.org/wiki/Static_program_analysis)來有效的刪除過期標誌相關的不必要代碼。

我們確定了執行清理的三個關鍵維度:

1.刪除與功能標誌api直接關聯的代碼。

2.刪除由於執行之前步驟而不可達的代碼。我們稱之爲深度清理。

3.刪除與功能標誌相關的測試。

爲了在所有三個維度上執行精確的清理,必須執行可達性分析,以識別無法到達的代碼區域,並實現算法來識別與特徵標誌相關的測試。在理想情況下完全自動化的實現,可以確保開發人員只需檢查刪除並將更改合併到master分支中,但它仍需要克服兩個挑戰:確保執行清理的底層分析是健全和完整的,以及對應的方法可實現並且在處理上百萬行代碼時仍然能夠保證時效性。

由於以健全和完整的方式確定可達性通常是不切實際的。這種方式,開發者在清理後的干預量是未知的,而且工程投資的回報也是不清楚的,因此我們決定不建立這樣複雜的分析。取而代之的是,我們根據在代碼庫中觀察到的編碼模式,選擇了一種迭代設計的可行方案。

我們發現通常有三種標誌api:

1.返回布爾值並用於確定執行的、控制路徑的布爾api

2.更新運行系統中的功能標誌值的更新api

3.返回非布爾原始值(整數、雙精度等)的參數api,該值與從後端控制的實驗值相對應。

我們的重構技術會解析輸入源代碼的ast,以檢測所考慮的特性標誌api的使用的地方。

對於布爾api,我們執行簡單的布爾表達式簡化。如果得到的值是一個布爾常量,我們會對代碼進行適當的重構。例如,如果布爾API作爲if語句的一部分出現並簡化爲true,那麼我們通過刪除整個>if語句來重構代碼,並將其替換爲then子句中的語句。

對於更新api,我們只需刪除相應的語句。

對於參數api,我們不會處理。因爲處理它們所需的工程工作量要大得多,它們在代碼庫中出現的頻率要低得多。

由於我們觀察到布爾api不總是在條件語句中使用,因此我們在重構中設計了第二個步驟。我們識別右邊是布爾API的賦值,Piranha將其簡化爲常量並跟蹤該變量。類似地,我們跟蹤包裝器方法,返回一個被簡化爲常量的布爾API。隨後,我們定位賦值變量或包裝器方法在保護條件下的使用,以此來執行重構。

最後,對於測試的標記註釋,如果註解(annotations)與輸入不匹配,我們會通過丟棄整個測試來處理。否則,我們只需刪除測試的註釋。

綜上所述,Piranha將以下內容作爲輸入:過期標誌、處理行爲和標誌的所有者。它分析了在預定義的特性標誌api中使用此標誌的代碼,並根據處理行爲重構它以刪除各種相關代碼。

Piranha在Uber的應用

我們應用了Piranha來重構Objective-C、Swift和Java程序。PiranhaJava重構了Java應用程序中陳舊的功能標誌相關代碼,特別是針對Android平臺的應用程序。它是作爲一個Error Prone的插件並用Java實現的。PiranhaSwift是用Swift實現的,使用了SwiftSyntax重構Swift代碼。PiranhaObjC用於清理Objective-C的代碼,它是用C++實現的Clang插件,使用了AST匹配和重寫的內部解析庫,以實現AST的解析和重寫。

雖然Piranha作爲一個獨立的工具可以執行代碼重構,但開發人員並不總是優先考慮標誌的清除,因此可能不會那麼頻繁地使用它。就像Piranha可以自動清除標記一樣,我們需要一個系統來自動啓動這些清除。

在內部,我們構建了一個規範工作流,該管道定期(我們是每週執行)生成差異和清理過期功能標誌的任務。這個工作流向標誌管理系統查詢過期標誌的列表,對於每個這些標誌,它分別調用Piranha,並以過期標誌的名稱、所有者和預期的輸出行爲(處理或控制)作爲系統的輸入。

 

圖1. 在我們的Piranha工作流中,標誌管理系統定期向Piranha發送一個可能過期的標誌列表,Piranha生成一個diff並將其發送給原始標誌作者。然後,作者可以決定是否處理diff。

上圖1顯示了Piranha工作流的結構圖。Piranha生成一個diff(即一個pull請求)並將其放入我們的代碼審查系統中,標誌的原始作者是默認的審查者。作者可以按原樣接受diff,或者根據需要修改它,或者拒絕並將標誌標記爲不過期。工作流還會在我們的任務管理系統中生成一個清理任務,以跟蹤每個生成的差異的狀態。由於開發人員不能總是及時處理這些差異,我們還引入了一個名爲“PiranhaTidy”的提醒機器人,以定期爲相關任務添加提醒。

Piranha工作流使用一種啓發式方法,將超過一個特定期限(例如8周)未在標誌管理系統中修改過的視爲過期的志,併爲這些標誌生成diff。負責處理Piranha輸出的各個團隊,爲標誌配置確切的過期時間。我們觀察到目前使用Piranha產生diff所需的時間少於3分鐘。

在你的代碼中使用Piranha

對三種所受支持語言,我們很高興地宣佈食人魚現在是開源的。如果您的代碼滿足以下條件,我們相信該工具將對您的團隊有用:

1.廣泛使用功能標誌

2.有特定的api來控制標誌的行爲

3.是用Java、Swift或Objective-C實現的

爲代碼庫設置Piranha 非常簡單:在屬性文件中定義與功能標誌相關的api和預期行爲,然後以標誌名稱和預期輸出爲參數運行Piranha 。有關在每種語言如何使用Piranha 的詳細信息,請參閱文檔(https://github.com/uber/piranha)。

我們歡迎開發人員爲Piranha貢獻代碼 。所有的開發人員都是受歡迎的。並且,致力於Piranha 的實現,可能是理解該領域非專家程序分析細微差別的一個非常好方法。有許多有趣的項目涉及到優化Piranha 生成的代碼重構、將Piranha 擴展到其他語言(如Kotlin、Go等),以及設計和實現其他與功能標誌相關的程序分析。那麼,請下載Piranha 的源代碼(https://github.com/uber/piranha)開始吧。

如果您有興趣加入我們的編程系統團隊(https://www.uber.com/us/en/about/science/?_ga=2.144053109.1910292101.1588403826-1280549756.1585307101),從事編程語言、編譯器和軟件工程方面的項目,並且在程序分析、編譯器和相關領域有學術或行業經驗,請聯繫我們的團隊([email protected])。

有關Piranha 的詳細研究論文(https://github.com/uber/piranha/blob/master/report.pdf)將發表在韓國首爾舉行的軟件工程、軟件工程和實踐跟蹤國際會議(International Conference of Software Engineering, Software Engineering and Practices track )(ICSE-SEIP'20)上。

 

1.翻譯自Uber Engineering,原文地址:https://eng.uber.com/piranha/ 外網連接不穩定,附上英文原文,按需自取。

2.文中we第一人稱未做修改,意爲原文團隊(本人非團隊成員)。

3.並不生產水,只是搬運工

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章