HUAN 讙 · 開發者
ADR-0002:用 desired / reported state 而不是命令式 RPC
狀態
已採用
背景
Server 需要讓裝置播放特定的內容。最直覺的做法是下命令:「下載這個檔案」、「切換到那個版面」、「重新啟動播放器」。
決策
Server 不下命令,而是宣告一個目標狀態:
{ "version": 42, "defaultLayout": { … }, "schedules": [ … ], "assets": [ … ] }
裝置回報現況,並自己負責把差距補上。少數真正屬於「動作」的操作(強制同步、重新啟動播放器)才用命令表達。
理由
命令式的模型必須記住每一條命令的狀態。 裝置在下載到一半時斷電,Server 怎麼知道?它得記錄命令送出、命令確認、命令失敗、命令重送——這就是在資料庫裡重新發明一套不可靠的工作流程引擎,而且每加一種操作就要多一套狀態。
宣告式的模型沒有中間狀態要記。 裝置重新開機後只做一件事:比較目標與現況,把差距補上。中間斷過幾次、重試過幾輪、命令有沒有送到,都不影響最終結果。這是冪等的。
除錯也容易得多。 「這台裝置為什麼播錯東西」在命令式模型下要去翻命令歷史,在宣告式模型下只要比對兩個 JSON。
這個模式在基礎設施領域已經被驗證很久了——Kubernetes 的控制器、Terraform 的計畫執行、各種設定管理工具,用的都是同一個想法。
後果
- 裝置端的邏輯比較複雜:它必須自己算出差異、排下載順序、處理部分失敗。這個複雜度集中在
@huan/device-core的調和器裡,而且是可以單獨測試的。 - Server 端變得很單純:算出目標、寫進資料庫、發一個通知。
- 版本號必須只在內容真的改變時才遞增。否則改一個不相干的排程就會讓所有裝置誤以為自己落後,然後同時來要狀態。
替代方案
純命令式 RPC:如上所述,需要在 Server 端維護一套可靠的命令狀態機。
混合模型:內容用宣告式、動作用命令式。這其實就是最終採用的做法——force_sync、restart_player 與 unbind 是命令,因為它們本來就是動作而不是狀態。