儲存與執行之間程式碼會改變
對於物件 Job,Queue::createObjectPayload() 會執行 serialize(clone $job),並將結果放入 payload 的 data.command。即使啟用加密,在那之前也會進行同樣的序列化。
worker 端的 CallQueuedHandler::getCommand() 會對已儲存的資料執行 unserialize(),並將還原後的 Job 交給 Dispatcher。一般的 handle() 會以當下 worker 正在執行的程式碼與服務容器來執行。
若沒有重新啟動長時間執行的 worker,更新後舊程式碼仍會持續運作。在滾動部署中,不只要考慮「舊分派端與新 worker」,也要考慮「新分派端與舊 worker」的組合。
判斷類別的重新命名或刪除時,不能只看使用者是否直接呼叫該類別。等待中、延遲中、等待重試的 Job,以及失敗 Job 的保存期間,也都屬於相容性的範圍。
SerializesModels 儲存的內容
Illuminate\Foundation\Queue\Queueable 在內部使用了 SerializesModels。它不只負責替換 model,也提供列舉 Job 本身屬性的 __serialize() 與還原值的 __unserialize()。
與宣告預設值相等的值不會被儲存
__serialize() 會排除 static、未初始化等屬性,並且當屬性目前的值與宣告預設值嚴格相等時也會省略儲存。
'csv',儲存的陣列中就不會包含 format。若之後的版本將宣告預設值改為 'json',舊 Job 也可能套用 'json'。這不是單純外觀上的預設值變更,而是讀取舊資料方式的變更。
還原時不會重新呼叫建構子
__unserialize() 會為目前類別中存在、且儲存陣列中也有對應鍵的屬性指派值。它不會執行透過建構子進行的輸入補全或驗證。
'csv' 是建構子參數的預設值。它不是屬性本身的宣告預設值,因此在還原不含 format 的舊 payload 時不會補上這個值。若新增的具型別屬性維持未初始化狀態,之後存取時就會失敗。
此外,public 屬性的鍵就是名稱本身,protected 屬性的鍵帶有表示可見性的前綴,private 屬性的鍵還包含類別名稱。將屬性從 public 改為 private 的變更也要用舊 payload 驗證。
將識別碼與執行服務分開
將執行所需的識別碼與選項儲存在 Job 中,而 API 用戶端或 Exporter 等執行服務則注入到handle()。若將服務傳給建構子並保留下來,其內部狀態也可能成為儲存對象。
下面的範例為舊版中只持有 reportId 的同名 Job 加入格式選擇與 payload 版本。ReportExporter 是套件提供並 bind 到容器的契約,假設它擁有 export(string $reportId, string $format): void。
- 舊 payload 中沒有
format時,使用宣告預設值'csv'。 - 舊 payload 中沒有
payloadVersion時,視為宣告預設值1。 - 新建立 Job 時,建構子會將
payloadVersion設為2。由於與宣告預設值不同,這個值會被儲存。 - 新 Job 的
format為'json'時,該值也會被儲存。為'csv'時會被省略,因此今後也要維持宣告預設值'csv'。
傳遞 model 時要與快照區分
將 model 直接儲存在屬性中時,SerializesAndRestoresModelIdentifiers 會將其替換為包含類別、ID、連線名稱、已載入關聯名稱等資訊的 ModelIdentifier。還原單一 model 時,會透過 firstOrFail() 從資料庫重新取得,並載入目標關聯。
這並不是固定分派當下屬性或關聯資料的機制。
- 若記錄在執行前被更新,還原後參照的是更新後的內容。
- 事先套用在關聯上的篩選條件不會被儲存,因此需要在執行時重新加上必要的條件。
- 不需要的關聯可以透過
withoutRelations()或#[WithoutRelations]排除在儲存對象之外。 - 若 model 已被刪除,Job 可能會在
handle()之前的還原階段失敗。
#[DeleteWhenMissingModels]。不過,套件不應擅自將捨棄設為預設行為,而應向使用者公開失敗方針。若採用只傳遞 ID 並在 handle() 內查詢的設計,則由自己控制該查詢的失敗或略過。
若需要分派當下的內容,就明確儲存所需的值,或將不可變的報表請求等記錄到資料庫並傳遞其 ID。要區分單純傳遞 model 與儲存業務資料快照這兩件事。
提交後分派是另一個問題
處理在交易中建立的記錄時,要確保 worker 不會在提交之前開始讀取。afterCommit() 會等到開啟中的父交易提交後再分派,回滾時則捨棄該分派。它並不是解決類別名稱、屬性相容性或外部處理重複執行的功能。
將 fake 與舊 payload 的測試分開
一般的Queue::fake() 用於記錄分派。光是 assertPushed() 通過,並不代表已確認儲存、還原,或新版對舊 payload 的處理。
目前類別的往返可以這樣驗證。
serializeAndRestore() 會在 fake 內執行 serialize() 與 unserialize()。但它不會驗證對實際後端的分派、worker 的執行以及舊版程式碼的產生。
保留以舊版產生的 fixture
在舊版的GenerateReport 只持有 reportId 的狀態下,產生不含實際資料的固定 Job。以下是在舊版的測試環境中執行一次的準備程式碼。tests/fixtures/ 需事先準備好。
TestCase 是在套件的測試環境中準備的基底類別。傳給 unserialize() 的只能是自己管理的 fixture,不要傳入外部輸入。
此外,也要以測試用的 Exporter 執行還原後舊 Job 的 handle(),驗證到輸出與儲存結果為止。傳遞 model 時,要另外驗證更新、刪除、連線與關聯的重新取得;在實際使用的 Queue 連線上,也要另外驗證提交後分派與重試。
在分派新格式之前先更新讀取端
1
準備能讀取舊格式的版本
先發佈維持舊類別名稱與舊值解讀、同時也能讀取新格式的程式碼。用 fixture 不只確認還原,也確認處理結果。
2
確認沒有殘留的舊 worker
在使用端應用程式的部署流程中加入 worker 的替換。對於一般的
queue:work,使用 php artisan queue:restart 要求在處理完目前的 Job 後結束,並確認行程管理器啟動了新版。重新啟動訊號會使用快取。3
啟用新格式的分派
在讀取端完成支援後,再透過功能旗標等切換到新格式的分派。若光是發佈同一份程式碼就會開始產生新格式,就需要暫停分派,或將新舊 Queue 與對應的 worker 分開等,確保不會交給舊 worker 的流程。
4
在重試與回滾期間也要持續維護
也要確認失敗 Job 的重試,以及在 Job 鏈中稍後分派的 Job。若在儲存新格式後回退到舊程式碼,要驗證舊程式碼也能讀取。不要只因為 Queue 曾經清空過一次就移除對舊類別或舊格式的支援。
queue:restart 並不是改寫已儲存 payload 的指令。在套件的更新說明中,要寫明支援的格式、所需的 worker 替換以及廢除舊格式的條件。同樣的變更也要記錄在版本相容性管理與 Changelog 的運用中。