RFC-0121:元件生命週期事件

RFC-0121:元件生命週期事件
狀態已接受
區域
  • 元件架構
說明

這份 RFC 將元件架構事件功能正式化。

問題
Gerrit 變更
作者
審查人員
提交日期 (年-月-日)2021-05-26
審查日期 (年-月-日)2021-08-11

摘要

本文件記錄了關於元件事件的概念、資訊清單語法和 FIDL 通訊協定等設計討論、原則和決策。

下文所述的許多決策都是在 2020 年初做出並實施的。我們提出了其他概念,以解決先前決策中的缺點和複雜性。我們建議將許多功能列入許可清單,以免這些功能在現有用途之外遭到濫用,同時我們也致力將這些功能遷移至更完善的機制。

本文件的範圍涵蓋下列元件架構 API:

  • CML:用來編寫元件資訊清單的語言
  • fuchsia.sys2/events.fidl:元件事件 FIDL API 所在的目前 API 途徑。目標是讓所有事件 API 升級至 fuchsia.component/events.fidl,並在 SDK 中提供這些 API。

提振精神

元件管理員會在內部處理元件的生命週期,不會將該資訊公開給元件。部分特權用戶端 (例如:診斷、測試、元件監控器、偵錯工具) 需要對元件生命週期有更深入的瞭解,才能執行工作。

元件事件是為了向這些特權元件公開這類資訊而推出的解決方案。元件執行個體發生生命週期轉換時,元件管理員會調度所有這些事件。能力路由 API 在本文件中提及的部分位置仍未關閉,以便進一步設計,讓元件可自行公開及提供自訂事件。

設計

本節將概述元件事件的目前設計和 API 修訂版本,並強調目前設計並非長期設計,並說明日後可設計及實作的功能。

元件事件串流功能

元件事件會以「事件串流」功能的形式建模。每個事件串流能力都會參照拓樸中的單一元件或元件的子樹狀結構。在撰寫本文時,事件會視為個別事件功能 (而非事件串流)。元件管理員會在生命週期轉換發生時,代表元件發出事件。

就像其他能力一樣,事件串流也可以進行路由。事件串流:

  • 從架構公開/提供:事件是指公開或提供事件的元件。

  • 由父項/子項使用:元件可以監聽從子項或父項傳送至該元件的事件。元件的父項可以直接使用子項的生命週期事件串流,而不需要子項明確地從架構中公開事件。

來自上述根目錄的事件串流

根層級領域可使用/提供「parent」的事件串流。您可能會想知道,如果根層級領域應為拓樸的根層級,那麼根層級領域的父項是什麼?在元件管理員術語中,元件管理員提供給根領域的功能,據稱來自「根層級以上」。

從「根以上」提供至根領域的事件串流,其範圍為整個元件執行個體拓樸。這些事件串流在路由時,可將範圍縮小至參照拓樸結構的子樹狀結構。這與目錄功能可做為整體或子目錄 (目錄路徑宣告中的 subdir 鍵) 進行路由的情況類似。要監聽範圍為整個領域的事件,唯一的方法就是使用來自根目錄的下限事件串流能力。涵蓋整個領域樹狀結構的事件具有特權/敏感性,且會破壞封裝邊界,因此我們會明確地從上述根目錄中將這些事件重新導向,透過靜態重新導向提供存取權控管。

請參考以下範例:

下方顯示聲明的視覺化樹狀圖

// root.cml
{
  offer: [
    {
      event_stream: "started",
      from: "parent",
      to: "#core",
      scope: "#core"
    },
  ]
}

// core.cml
{
  offer: [
    {
      event_stream: "started",
      from: "parent",
      to: "#test_manager",
      scope: "#test_manager"
    },
  ]
}

// test_manager.cml
{
  offer: [
    {
      event_stream: "started",
      from: "parent",
      to: [ "#archivist", "#tests" ]
      scope: "#tests",
    }
  ]
}

// tests.cml
{
  offer: [
    {
      event_stream: "started",
      from: "parent",
      to: [ "#test-12345" ]
      scope: "#test-12345",
    }
  ]
}

// test-12345.cml
{
  offer: [
    {
      event_stream: "started",
      from: "framework",
      scope: "#bar",
      to: "#foo"
    }
  ],
  use: [
    {
      event_stream: "started",
      from: "parent"
    },
    {
      event_stream: "stopped",
      from: "framework",
      scope: "#bar",
    }
  ]
}

// foo.cml
{
  use: [
    {
      event_stream: "started",
      from: "parent"
    }
  ]
}

// archivist.cml
{
  use: [
    {
      event_stream: "started",
      from: "parent",
    }
  ]
}

在這個例子中:

  • foo 可以取得 started 事件,前提是 test-12345 將事件路由至 bar

  • archivist 可以為 tests 下方的所有元件取得 started 事件,因為 started 是由 test_manager 提供,而 test_manager 是從 core 取得,而 core 是從 root 取得,而 root 是從 above root 取得,且在傳遞過程中已降低範圍。

  • test-12345 是測試根目錄,可以取得其下方所有元件的啟動事件,原因與 archivist 相同。不過,由於 teststest-12345 的事件縮小,因此與存檔器 (可取得 tests 的所有事件) 不同,它只能取得測試相關的事件。

這個範例說明瞭目前事件的其中一個核心用途。檔案管理員可以以隔離的方式觀察每項測試的結果。此外,每項測試可以取得測試下所有元件或特定元件的事件。

合併事件串流

相同類型的事件串流可以合併為單一串流。舉例來說,在上述範例中,#test-12345 可以將 foobarstopped 提供給其他元件,做為單一能力。該元件隨後會為 foobar 取得 stopped 事件。

// core.cml
offer: [
  {
    event_stream: "stopped",
    from: [ "#netstack", "#supervisor" ],
    to: "#someone",
  }
]

目前不允許從自身公開/提供事件。不過,這裡仍有進步空間,讓事件能夠自行公開/提供自訂事件。

事件模式

在撰寫本文時,事件可以以非同步或同步的方式擷取。意圖是「僅」使用非同步事件,並完全淘汰同步事件。

同步使用事件可讓訂閱者在處理事件時封鎖元件管理器,然後再繼續執行。這項功能最初是為了測試而推出,同時也考量到除錯工具。

自那時起,我們發現使用同步事件的測試通常可以使用非同步事件編寫。因此,我們努力完全移除同步事件,但仍在元件管理員內部測試中保留了少數用途。我們認為同步事件在偵錯工具 (例如 step 或 zxdb) 中很實用,但在那個時候,我們會發明一個能精準滿足偵錯工具需求的解決方案。

我們建議將使用同步處理事件的測試加入使用許可清單,並逐步完全移除同步處理事件。

事件類型

在撰寫本文時,我們有兩類事件:

  • 生命週期事件。這些事件反映元件執行個體的生命週期變更,並由管理這類資訊的元件管理器發出。
  • 已淘汰的事件。這些事件並未反映元件執行個體的生命週期變更,我們正在努力將這些事件完全移除,以便採用更適當的解決方案。

生命週期事件類型如下:

  • Discovered:這是元件生命週期的第一個階段。在建立動態子項時,會為動態子項調度;在解析父項時,會為靜態子項調度;在元件管理員啟動時,會為根目錄調度。
  • Resolved:例項的宣告首次成功解析。
  • Started:根據元件管理員的說法,這個例項已啟動。不過,如果這是可執行的元件,執行元件就必須進一步執行工作,才能啟動該元件。元件已開始執行,但可能尚未開始執行程式碼。
  • Stopped:已成功停止執行個體。此事件必須發生在 Destroyed 之前。
  • Destroyed:執行個體的銷毀作業已開始。此時執行個體已停止運作。這個例項仍存在於父項的領域中,但很快就會移除。
  • Purged:已成功銷毀執行個體。執行個體已停止,且不再存在於父項領域中。

以及下列已淘汰的事件類型:

  • Running:元件管理工具會針對訂閱時已在執行的所有元件合成此事件。此事件源自 started,但適用於在事件監聽器訂閱事件前啟動 (且未停止) 的元件。最終目標是提供元件架構查詢 API,以便瞭解正在執行的項目。因此,我們打算將這個事件加入唯一用戶端 (Archivist) 的許可清單,並在日後使用新 API 並移除 running

  • Capability Routed:這個事件是為了在測試中使用而導入。我們最近已將其完全移除,並正在進行完全移除的作業。

  • Capability Requested:這項事件是暫時性解決方案,可為 fuchsia.logger/LogSink 連線提供元件歸因。自此之後,這項屬性也用於提供 fuchsia.debugdata/Publisher 連線的歸因資訊。這並非長期解決方案。由於事件系統僅適用於具備特殊權限的元件,因此使用元件事件建構這項功能,是一種不需承諾太多資源的方法。我們瞭解,如果用途增加,就會推出更標準化的解決方案。目前的計畫是完全移除這項事件,並同時將其加入白名單,讓以下唯一的用戶端可以使用:Archivist、偵錯資料和測試管理員 (針對偵錯資料)。

  • Directory Ready:這項事件是為了提供元件公開的 out/diagnostics 目錄,以便 Archivist 執行檢查資料匯總作業,而推出的解決方案。診斷團隊預計設計 VMO 支援記錄。由於元件必須先開始提供診斷目錄,才能提供記錄,因此這種做法已淘汰。我們將設計新的解決方案,為 Archivist 提供檢查和記錄 VMO,確保即使在元件啟動非同步迴圈前,也能取得記錄。目前的提案是將此事件加入許可清單,讓該事件的唯一使用者 (即封存管理員) 可以使用,並逐步移除這項事件。

路由 CML 語法

使用
{
    use: [
        {
            event_stream: [
                "running",
                "started",
                "stopped",
            ],
            from: "parent",
            mode: "async",
            path: "/my_stream"
        },
    ]
}

使用宣告包含:

  • event_stream:單一事件名稱或事件名稱清單。
  • from:能力的來源。允許的值:父項或子項參照。不允許使用架構或自身的事件。
  • path:事件串流的路徑。這不是必要的步驟。在指定元件傳入的命名空間時,服務檔案會包含元件管理員提供的 fuchsia.component.EventStream 指定名稱 (請參閱「使用」)。如果未提供,元件可以使用 EventSource 在特定時間點開始使用事件。
  • scope:當事件從父項使用時,scope 可用於將事件參照至特定子項範圍,否則事件會攜帶來自父項的範圍。
  • mode:預設為 async。如先前所述,唯一的事件模式將為非同步。因此,在完全移除模式之前,只有允許清單中的測試才能使用「同步」模式。這個欄位最終會完全移除。
  • filter:目前僅用於 DiagnosticsReadyCapabilityRequested。如先前所述,這些事件會完全移除,因此不會提供篩選器的詳細資料,因為這些資料與非診斷開發人員無關。

您也可以使用來自不同來源的事件。在以下範例中,元件會取得單一事件串流,其中包含父項提供的 startedstopped 事件,以及子項 #child 啟動時的 started

use: [
  {
    event_stream: [
      {
        name: [ "started", "stopped ],
        from: "parent",
      },
      {
        name: "started",
        from: "framework",
        scope: "#child",
      }
    ]
  }
]
優惠
{
    offer: [
        {
            event_stream: "started",
            from: [ "#child_a", "parent", ],
            to: "#archivist",
            as: "started_foo"
        },
    ]
}

優惠宣告包含:

  • event_stream:單一事件名稱或事件名稱清單。
  • scope:當事件從架構提供時,範圍可讓使用者定義事件的子項 (或子項陣列)。如果未指定範圍,則範圍為 self,表示事件與元件本身有關。當事件從父項提供時,範圍可將事件下調至子範圍。
  • from:一或多個能力來源。當提供多個來源時,系統會將串流視為合併。允許的值:架構、父項或子項參照。不允許自售。
  • to:提供能力的子項參照。
  • as:能力的目標名稱 (重新命名)。只有在提供單一事件串流名稱時,才能提供此值。
公開
{
    expose: [
        {
            event_stream: "started",
            from: "framework",
            scope: [ "#child", "#other_child" ],
            as: "foo_started"
        },
    ]
}

公開宣告包含:

  • event_stream:單一事件名稱或事件名稱清單。
  • scope:當事件從 framework 公開時,需要範圍,並允許定義事件的子項 (或子項陣列)。
  • from:能力的一個或多個來源。當提供多個來源時,系統會將串流視為合併。允許的值:父項或子項參照。不允許公開自身的事件。
  • as:能力的目標名稱 (重新命名)。只有在提供單一事件串流名稱時,才能提供此值。

EventSource 通訊協定

EventSource 是允許元件執行個體訂閱事件串流的通訊協定。這是內建能力,任何元件都能從 framework 使用,以便使用明確路由至該元件的事件。

靜態事件串流

靜態事件串流是指透過傳入的 /events 目錄中的通訊協定取得事件的方式。與透過 EventSource.Subscribe 的執行階段訂閱不同,事件可在到達時緩衝並觸發元件的啟動

元件會宣告在某個路徑上 use 事件串流,而元件管理員會在指定路徑的傳入命名空間中建立項目,這是由元件管理員提供的 fuchsia.component.EventStream (請參閱「使用」一節)。

事件會在內部緩衝,直到執行 GetNext 呼叫為止。這個緩衝區的大小會在實作期間決定 (可能是大型緩衝區、下一個批次的最大大小或其他內容),並會在 FIDL API 和事件文件中清楚記錄。如果緩衝事件數量超過定義的數量 (因為消費者速度太慢),元件管理員就會捨棄事件並關閉連線。元件準備好接收事件時,即可重新連線。這是 Zircon 缺乏控制流程的解決方法。我們希望避免管道溢位,如果我們要反轉提供 EventStream 通訊協定的對象 (元件而非元件管理員),就無法避免這種情況,而且我們不希望在元件管理員中使用過多的記憶體。良好的用戶端會以穩定的速度使用事件,因此不必擔心遺漏事件。如果用戶端消耗事件的速度過慢,系統會通知事件遭到捨棄。這會透過他們所取用的串流中的事件發生。執行這項通知的幾種替代做法如下:

  • 同類型事件,其中錯誤酬載會指出該類型事件遭到捨棄的次數。
  • fuchsia.component.EventErrorPayload 下的特殊欄位:dropped。這個欄位會包含 EventType 和一個數字,用來指出該類型事件遭到捨棄的次數。
  • 通訊協定中的 FIDL 事件,當事件遭到捨棄時,用戶端會收到此事件,並指定捨棄的事件數量和類型。不過,這個替代做法可能會引入另一個 DoS 向量,因為管道可能會填滿事件。

FIDL API

在撰寫本文件時,事件用戶端通訊協定已在 fuchsia.sys2/events.fidl 中定義。本提案會引入一些變更,目的是將通訊協定和結構升級至 fuchsia.component,並在 SDK 中提供這些項目:

元件管理員會提供 EventStream,而非元件

這表示您必須變更 EventSource#Subscribe 才能接收要求:

[Discoverable]
protocol EventSource {
    Subscribe(vector<EventSubscription> events, request<EventStream> stream)
        -> () error fuchsia.component.Error;
};

EventStream.GetNext 提供一批事件

如前一個部分所述,由於元件管理員現在會提供通訊協定和批次事件,因此新串流通訊協定會傳回 GetNext 上的一批事件,而不是單一事件。

protocol EventStream {
    GetNext() -> vec<Event>;
};

已捨棄 TakeStaticEventStream

如前一個章節所述,我們已移除 EventSource.TakeStaticEventStream 方法,改為直接將事件串流置於傳入命名空間。

更嚴格的 EventEventResult

在撰寫本文時,所有這些資料類型都是表格。不過,這些資料的定義非常明確,我們不打算再加入更多內容。為了透過移除選填欄位處理來改善人因工程,我們會將這些欄位設為 struct 和非彈性聯集:

struct Event {
    EventHeader header;
    EventResult event_result;
};


union EventResult {
    1: EventPayload payload;
    2: EventError error;
};

實作

這項設計的大部分內容都已實作。在 SDK 中公開此 API 之前,仍有幾個部分需要實作。請特別注意以下幾點:

  • 允許列入的事件:directory readyrunningcapability requested
  • 將靜態事件串流放入傳入的命名空間,並移除 EventStream.TakeStaticEventStream 方法。
  • CML 更新:自今天起,我們會將 events 轉送至 event streams,並在轉送作業中合併這兩個項目。
  • 提供從「根目錄以上」到根目錄的整個拓樸結構圖事件,並在事件路由宣告中實作 scope 關鍵字,以便取得拓樸結構圖中子樹狀結構的事件。
  • 公開事件。在撰寫本文時,我們只支援 offeruse
  • 在來源中,事件現在會參照單一元件,但從「根層級以上」提供給根領域的事件除外。在路由時,事件可以匯總,因此可以參照多個元件。

成效

事件系統是建構在元件管理員實作的現有內部掛鉤系統之上,因此將事件調度至感興趣的其他元件,對效能影響不大。事實上,現在效能已改善,因為如果元件只對單一元件的事件感興趣,而非整個子樹狀結構,現在可以只為該元件調度事件,而非整個子樹狀結構,進而減少涉及的系統呼叫數量。

人體工學

這項 RFC 針對事件的使用者體驗進行改善:

  • 如果元件只對單一元件事件感興趣,現在可以使用 CML 表示,而不需要程式碼來篩選事件。
  • 所有元件現在都提供 EventSource 通訊協定做為內建架構能力,且不需要明確地將其路由。
  • 只有單一能力會將 event stream 路由至 events,而不會像目前一樣將 events 路由至 event streams
  • Event streams 可在轉送時合併,減少開發人員需要在 CML 中編寫的轉送陳述式數量。

回溯相容性

這項變更不會破壞相容性。在元件管理員層級 (以及根層級、核心層級、Bootstrap 層級) 會有樹狀結構內的簡單重構,但在用戶端元件中則不會。此外,由於事件現在會是單一元件,因此測試中會重構使用事件 (全部樹狀結構內)。

安全性和隱私權注意事項

這項提案會維持 2020 年安全性和隱私權審查中討論的安全性屬性。先前,由於事件是關於整個子樹狀結構,因此 EventSource 通訊協定會明確路由。這個通訊協定現在是內建能力,而事件則與特定元件有關。系統會明確地將整個子樹狀結構的事件重新導向,並執行靜態驗證,確保非特權元件不會收到這些事件。

測試

所有事件功能和語意都必須在元件管理工具中進行整合測試。

說明文件

您必須在「事件功能」和「元件資訊清單」頁面中更新說明文件。

缺點、替代方案和未知事項

從元件提供 EventSink 能力至架構

根據這個概念,元件會將通訊協定 RealmEventSink 公開給架構。此時,架構會開啟這項能力,並在事件發生時將事件推送至該功能。此提案缺少一種方法,無法定義元件會從哪個範圍取得事件,也沒有明確的路徑可限制此範圍,或無法靜態驗證元件不會從拓樸結構的部分取得事件,因為該部分不應可見。在目前的設計下,您可以透過靜態方式表示取得特定元件集的事件 (適用於一般用途),或是拓樸結構中的整個子樹狀結構 (適用於實際工作環境和測試中的診斷用途)。

用於公開和提供路徑宣告的來源

self 中的 expose/offer 事件會保留為開放式區域,以便日後設計元件可能會公開及調度自有事件。

既有技術與參考資料

如要查看事件目前狀態的說明文件,請參閱「事件功能」和「事件 FIDL 定義」。