連按兩次發布,現在只會發布一次
同一個應用程式的三個發布請求,在相隔 84 毫秒後抵達我們的 API,每一個都啟動了該應用程式的一次完整部署。它們彼此爭搶同一個主機,結果有好幾個發布因為另一個已經先到達而失敗,唯一可見的跡象只是一封「發布失敗」的郵件,應用程式本身最後卻是正常的。現在,如果有發布在同一應用程式的另一個發布進行中抵達,就不會再啟動第二次建置。資料庫決定哪個請求獲勝,其他請求則會被告知真相:已有一個發布正在進行中。
發生了什麼
當你在 Mythex 上發布應用程式時,API 會為它記錄一個部署,並要求我們的協調器(負責執行建置的服務)建置並發布它。發布可以從「發布」按鈕開始、從另一個瀏覽器分頁開始,或在你要求時由代理開始。連按兩下、兩個分頁,或一個重複抵達的代理請求,都可能讓同一個應用程式幾乎同時產生多次發布。
在這個案例中,三個請求在 84 毫秒內抵達,每一個都執行了完整部署。在主機平台上,同一應用程式的發布版本在同一秒內被建立,其中三個因為其他版本已在進行中而失敗。之後有一個版本卡住了,但應用程式事後是可以運作的。
這正是它容易被忽略的原因。使用者看到的是一個正常運作的應用程式。失敗只會以發布失敗的郵件形式出現。
為什麼沒有任何東西阻止它
發布程式碼其實有一個步驟會查看該專案中進行中的部署。但它做的事情是:在開始之前,把每個進行中的部署都標記為失敗,理由是「已被新的發布取代」。它原本是為了清理因程序當機而遺留的建置。
實際上,這表示在建置中途抵達的第二個發布,會宣告第一個已經死亡,並開始自己的建置。兩者都會對同一個應用程式跑到完成。沒有任何鎖;反而有一個步驟會主動替競爭清出道路。
對於應用程式的第一次發布,還有另一個更奇怪的症狀。由於還沒有可以重用的部署紀錄,兩個同時進行的第一次發布都會嘗試插入同一筆紀錄。資料庫對應用程式的連結名稱(也就是 slug,例如 your-app.mythex.ai 中的 your-app)有唯一索引,所以第二次插入失敗。API 把這個結果翻譯成「該連結名稱已被使用」,建議使用者去改名一個其實正在用該名稱成功發布的應用程式。
修正方式:宣告即是鎖
現在,宣告一筆部署紀錄是一次有條件的資料庫更新:只有在沒有人正在發布的情況下,才會把這一列取出並標記為建置中。因為它是一個陳述式,資料庫會在同一毫秒內抵達的請求之間做出決定。只有一個請求會改變這一列,其他請求會發現零列被改變,於是停止,且永遠不會抵達協調器。
鎖也需要在持有者死亡時能夠釋放。如果 API 在發布中途重新啟動,那一列就會永遠停留在「建置中」,應用程式也就無法再發布。因此,擁有者已停止送出心跳訊號的列可以被重新宣告:進行中的發布每 30 秒會更新一次這一列,而沉默五分鐘就視為死亡。這個門檻來自心跳,而不是對建置時間的猜測。建置使用者自己的 Dockerfile 可能合法地執行很長時間,而一個仍在送出心跳的慢速建置會保留它的宣告。
在發布之前執行、會把所有進行中部署標記為失敗的那個步驟,已不再執行。現在只會清除由已死亡的程序遺留下來的列。
告訴落敗的一方真相
落敗的請求現在會得到一個清楚的回答:publish_in_progress,並附上正在執行中的部署 ID。每一個可以啟動發布的地方都會處理它:
- 工作區會跟隨已在執行中的建置,而不是在它上面顯示一個錯誤。
- 代理會被告知該應用程式的發布已在進行中,請等待它完成,而不是被告知某件事失敗了。過去那樣的訊息可能會讓它去「修正」Dockerfile 並再試一次。
- 首次發布時的連結名稱衝突會被檢查:如果佔用該名稱的那一列是本專案自己正在進行中的發布,回答就是「已在發布中」,而不是「該名稱已被使用」。與其他專案的真正衝突,仍會得到改名的提示。
我們如何驗證
我們寫了一個重現腳本,在真實的資料庫與一個模擬的協調器上,同時執行同一個應用程式的三次發布,並計算協調器被要求了多少次部署。它涵蓋首次發布與一個已上線應用程式的重新發布。修正前,它計數為兩次部署;修正後為一次。
該腳本也涵蓋了退路:它把應用程式的列標記為建置中,但十分鐘沒有心跳,然後再次發布,並檢查新的發布是否能宣告該列並執行。
單元測試把鎖限定在它必須做到的兩件事上:三個同時進行的重新發布中,只有一個能通過,另外兩個會被告知有發布正在進行;而擁有者已死亡的建置可以被宣告。另一個測試則檢查新的嘗試不會沿用上一次嘗試的錯誤訊息。
我們的收穫
- 清理程式碼可能暗藏競爭。 「先把進行中的全部標記為失敗,然後開始」讀起來像是衛生措施。但若有兩個呼叫者,就會變成彼此互相取消。
- 讓資料庫來裁決。 先讀取再寫入,會給另一個請求留下空隙。單一條件式更新則不會。
- 每個鎖都需要與存活狀態綁定的過期機制。 沒有它,當機就會變成應用程式永遠卡住。
- 錯誤訊息就是指示。 「該名稱已被使用」讓人去做錯誤的事。正確的訊息更簡單,而且是真的。
關於發布的更多資訊,請參閱文件:如何發布。另一個相關的修正是:你的已發布應用程式在重新發布時仍保持上線。