服務
Graft 中的服務就是一般的 Effect Context 鍵及其對應的值。Graft 增加了兩點保證:
- 服務可以在應用程式執行期間發布和撤回。
- 使用某個服務的外掛會在該服務消失之前停止。
在 apply 內呼叫 Graft.provide:
import { Context, DateTime, Effect } from "effect"
import { Graft, Plugin } from "@graftland/graft"
export class Clock extends Context.Service< Clock, { readonly now: Effect.Effect<number> }>()("app/Clock") {}
export const clock = Plugin.make({ name: "app/clock", apply: () => Graft.provide(Clock, { now: Effect.map(DateTime.now, DateTime.toEpochMillis), }),})| 規則 | 細節 |
|---|---|
| 歸屬 | 服務歸發布它的 fork 所有,在該 fork 卸載或重新啟動前一直可用。 |
| 數量 | 一個 fork 可以發布任意數量的服務。 |
| 唯一性 | 在同一個領域(realm)內,一個鍵只能由一個 fork 發布。 |
| 衝突 | 再次對同一個鍵呼叫 Graft.provide 會以 ServiceConflict 失敗。 |
| 切換 | 取代發布它的外掛,或使用 Graft.replace。 |
將服務鍵列入 inject,然後透過 yield* 讀取:
import { Effect, Option, Stream } from "effect"
import { Fork, Graft, Plugin } from "@graftland/graft"
import { Clock, clock } from "./clock.ts"
const uptime = Plugin.make({ name: "app/uptime", inject: [Clock], apply: () => Effect.gen(function* () { const clock = yield* Clock const started = yield* clock.now yield* Effect.addFinalizer(() => Effect.flatMap(clock.now, (now) => Effect.log(`Ran for ${now - started} ms`), ), ) }),})
const state = await Effect.runPromise( Effect.gen(function* () { const provider = yield* Graft.plugin(clock) yield* provider.await const fork = yield* Graft.plugin(uptime) yield* fork.await return yield* Stream.runHead(fork.state) }).pipe(Effect.scoped, Effect.provide(Graft.layer)),)state // => Option.some(Fork.ForkState.Active())上面的終結器在外掛停止時呼叫
clock。這樣做是安全的:在所有注入該服務的外掛停止之前,服務始終可用。
發布方不一定在你的行程裡執行。用 Remote
移到子行程、worker 或另一台機器上的外掛,仍然在這裡發布它的服務,uptime
也照樣注入
Clock。只是呼叫會經過一條連線,失敗時呼叫端看到的是遠端錯誤。
服務撤回流程
Section titled “服務撤回流程”發布服務的 fork 停止時,Graft 依序執行以下步驟:
- 如果
apply仍在執行,先停止它。 - 關閉發布。 在一筆交易中撤回該 fork 的服務。此後新的消費者不能取得 lease,該 activation 也不能再發布服務。
- 排空消費者。
等待既有消費者釋放全部 lease,然後重新啟動這些消費者(見下表)。期間健康的
Plugin.fork任務及其資源繼續服務消費者的 finalizer。 - 停止剩餘任務。 關閉它們的 Scope,並等待取消清理完成。
- 關閉 activation Scope。 執行終結器、停止子外掛並移除事件處理函式。
- 釋放該 fork 自身使用的服務。
第 3 步中消費者如何重新啟動:
| 消費者的情況 | 重新啟動後 |
|---|---|
| 有環境後備值 | 使用後備值。 |
| 必要相依,沒有後備值 | 進入 Pending。 |
| 可選相依,沒有後備值 | 在缺少該服務的情況下重新啟動。 |
失敗觸發清理時
Section titled “失敗觸發清理時”遵循相同的順序。失敗的任務已經停止,健康任務繼續執行,直到消費者排空。
- Graft 會收集排空期間出現的其他任務失敗,並記錄它們的完整
Cause,包括清理 defect。 - 由失敗觸發的清理會將這些失敗納入最終的
Failed狀態,資源終結器的失敗也一樣。 - 這些失敗不會中斷後續清理階段,也不會妨礙相依 lease 的釋放。
- 明確呼叫
dispose時,會以Fork.CleanupError回報清理失敗,其cause保留原始失敗原因。
外掛一旦開始使用某個服務,就會一直持有它,直到自身停止。Graft 在檢查外掛相依項目的同一筆交易中記錄這些使用關係,因此服務不會在外掛啟動到一半時被撤回。
來自環境的服務
Section titled “來自環境的服務”建構 Graft.layer 時所在環境中的服務對所有外掛可見。它們可以滿足 inject
宣告,生命週期與該環境一致。與整個應用程式生命週期一致的服務適合放在環境中:
import { Effect, FileSystem, Layer, Option, Path, Stream } from "effect"import { NodeServices } from "@effect/platform-node"
import { Fork, Graft, Plugin } from "@graftland/graft"
const layer = Layer.provideMerge(Graft.layer, NodeServices.layer)
const files = Plugin.make({ name: "app/files", inject: [FileSystem.FileSystem, Path.Path], apply: () => Effect.void,})
const state = await Effect.runPromise( Effect.gen(function* () { const fork = yield* Graft.plugin(files) yield* fork.await return yield* Stream.runHead(fork.state) }).pipe(Effect.scoped, Effect.provide(layer)),)state // => Option.some(Fork.ForkState.Active())使用這個 Layer 後,外掛可以直接注入 FileSystem.FileSystem 或 Path.Path。
對於 inject 中的每個鍵,無論必要或可選,Graft 都依下列順序解析:
- 消費者所在領域的註冊表。
- 註冊表中沒有時,使用環境。
使用後備值的消費者仍會監看自己的領域:
| 領域中的事件 | 消費者…… |
|---|---|
| 出現新的 provision | 重新啟動並改用發布的值。 |
| provision 被撤回 | 重新啟動並回到環境值。 |
因此,無論先載入消費者還是先發布服務,消費者最終都會使用相同的值。發布操作不會等待既有消費者完成重新啟動。
inject 與 Effect.serviceOption
Section titled “inject 與 Effect.serviceOption”inject |
未宣告的 Effect.serviceOption |
|
|---|---|---|
| 先查找領域,再查找環境 | 是 | 否,只讀取 activation 的 context |
| 訂閱註冊表變化 | 是 | 否 |
| 取得 provision 的 lease | 是 | 否 |
需要這種查找和重新啟動行為時,請使用 inject。
Reference
Section titled “Reference”inject 中的 Context.Reference 始終有後備值,因此不會讓外掛停在
Pending。解析順序:
- 消費者所在領域的 provision。
- 繼承的 Reference 值(包含 intercept)。
- Reference 的預設值。
它和其他注入鍵一樣監看發布與撤回。
發布導致的相依環
Section titled “發布導致的相依環”Graft 會拒絕會導致發布者自身重新啟動的發布。它在發布服務的同一筆交易中進行檢查:
- 找出在同一領域中,透過 ambient 後備值或缺席的可選相依監看該鍵的執行中消費者。
- 沿著這些消費者的子樹、受影響 provider 的消費者擴展,直到不再有 fork 會被停止。
- 如果發布者也在其中,註冊表保持不變,
Graft.provide以ServiceConflict失敗。
Pending fork 的必要相依是在等待啟動,不屬於重新啟動的 watcher。
| 欄位 | 值 |
|---|---|
reason |
直接環為 "SelfDependency",間接環為 "DependencyCycle"。 |
reason |
相依項目的變更已使提供者失效時為 "Invalidated"。 |
fork |
發布者的 fork ID。 |
realm |
服務所在的領域。 |
包裝環境服務時,請為輸入和輸出使用不同的鍵。
接下來會發生什麼
Section titled “接下來會發生什麼”- 如果環錯誤從
apply或Plugin.fork任務傳播出來,Graft 會清理發布者,發布者進入Failed。 - 隨後它等待設定、相依項目或圖發生變化,而不會依 retry schedule 反覆撞上同一個環。
- 重試之前,Graft 會先把失敗 fork 預期取得的相依項目放回圖中再做檢查。清理期間釋放 fork 的 lease,本身並不能解除環。
- 拒絕發布不會寫入服務值,也不會觸發既有 watcher 重新啟動。正常的失敗傳播仍然適用:等待子外掛失敗的外掛可能隨之失敗;失敗的 fork 之前已發布的服務被撤回時,其消費者仍會受到影響。