RFC-0121:组件生命周期事件

RFC-0121:组件生命周期事件
状态已接受
区域
  • 组件框架
说明

此 RFC 对组件框架事件功能进行了形式化定义。

问题
Gerrit 更改
作者
审核人
提交日期(年-月-日)2021-05-26
审核日期(年-月-日)2021-08-11

摘要

本文档记录了与组件事件(概念、清单语法和 FIDL 协议)相关的设计讨论、原则和决策。

下文中所述的许多决定都是在 2020 年初做出并实施的。还提出了一些其他概念,以解决先前决策中的缺点和复杂性。我们建议将许多功能列入许可名单,以防止在现有用例之外使用这些功能,同时我们会努力将这些功能迁移到更完善的机制。

本文档涵盖以下组件框架 API:

  • CML:用于编写组件清单的语言
  • fuchsia.sys2/events.fidl:Component Events HIDL API 所在的当前 API 接口。目标是将所有事件 API 升级到 fuchsia.component/events.fidl,并在 SDK 中提供这些 API。

设计初衷

组件管理器在内部处理组件的生命周期,不会将这些信息公开给组件。某些特权客户端(例如诊断程序、测试、组件监督程序、调试程序)需要深入了解组件的生命周期才能执行其工作。

组件事件作为一种解决方案被引入,用于向这些特权组件公开此类信息。当组件实例发生生命周期转换时,组件管理器会分派所有这些事件。在本文档中提及的某些位置,我们保留了 capability routing API,以便进行进一步设计,让组件能够自行公开和提供自定义事件。

设计

本部分概述了组件事件的当前设计以及 API 修订,重点介绍了当前设计不适合作为长期设计的情况,并指出了可设计和实现的未来功能。

组件事件流功能

组件事件被建模为“事件流”功能。每个事件流功能都指拓扑中的单个组件或组件子树。在撰写本文档时,事件被视为个别事件功能(而非事件串)。当发生生命周期转换时,组件管理器会代表组件发出事件。

与任何其他功能一样,事件流也可以进行路由。事件流的以下情况:

  • 从框架公开/提供:事件是指公开或提供事件的组件。

  • 从父级/子级使用:组件可以监听从子级或父级路由到它的事件。组件的父级可以直接使用子级的生命周期事件流,而无需子级从框架中明确公开事件。

来自根目录上方的事件流

根令牌网域可以使用/提供来自“父级”的事件流。您可能会想知道,如果根王国应该是拓扑的根,那么它的父级是什么?在组件管理器术语中,组件管理器向根令牌网域提供的 capability 被称为来自“根之上”。

从“根之上”提供给根令牌网格的事件流的范围是整个组件实例拓扑。在路由这些事件流时,可以将其缩小范围,以引用拓扑的子树。这与目录功能可以作为整体或子目录进行路由的方式类似(目录路由声明中的 subdir 键)。如需监听范围为整个 Realm 的事件,唯一的方法是拥有从根源可用的范围缩小的事件流功能。涵盖整个 Realm 树的事件是特权/敏感事件,会破坏封装边界,因此,我们会从根上方显式路由这些事件,通过静态路由提供访问控制。

请参考以下示例:

下方显示的声明的直观树形表示

// 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",
    }
  ]
}

在此示例中:

  • 只要 test-12345bar 路由到 foofoo 便可以获取 barstarted 事件。

  • 假设 archivisttest_manager 获得了 startedtest_managercore 获得了 startedcoreroot 获得了 startedrootabove root 获得了 started,并且在传递过程中缩小了范围,那么 archivist 可以为 tests 下的所有组件获取 started 事件。

  • 测试根 test-12345 可以获取与其下所有组件相关的启动事件,原因与 archivist 相同。不过,与归档程序(可以从 tests 获取所有事件)不同,它只能获取与测试相关的事件,因为 tests 缩小了 test-12345 的事件范围。

此示例展示了事件目前的一项核心用例。归档管理员能够以隔离的方式观察每个测试内部发生的情况。此外,每个测试都可以获取与测试下的所有组件或特定组件相关的事件。

合并事件流

同一类型的事件流可以合并为单个流。例如,在上面的示例中,#test-12345 可以将 foobar 中的 stopped 作为一项功能提供给其他组件。然后,该组件将同时为 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 来了解正在运行的内容。因此,计划是将此事件列入其唯一客户端(归档管理器)的许可名单,并在日后使用新 API 并移除 running

  • Capability Routed:此事件旨在用于测试。我们最近已将其从所有使用它的测试中完全移除,并且正在逐步彻底移除它。

  • Capability Requested:此事件作为临时解决方案引入,用于为 fuchsia.logger/LogSink 连接提供组件归因。自那以后,它还被用于为 fuchsia.debugdata/Publisher 连接提供归因。这从来都不是长远的解决方案。由于事件系统仅适用于特权组件,因此使用组件事件构建此功能是一种低承诺的方法。我们知道,如果用例不断增加,那么就需要发明更标准化的解决方案。目前的计划是努力彻底移除此事件,同时将其列入唯一客户端(归档程序、调试数据和测试管理器 [适用于调试数据])的许可名单。

  • Directory Ready:此事件旨在提供一种解决方案,以便将组件公开的 out/diagnostics 目录提供给归档程序以进行数据汇总检查。诊断团队计划设计 VMO 支持的日志。由于日志需要在组件开始提供诊断目录之前可用,因此此方法已废弃。我们将设计一种新的解决方案,用于向归档程序提供检查和日志 VMO,以确保即使在组件启动异步循环之前,日志也能正常使用。目前的方案是将此事件列入其唯一用户(归档员)的许可名单,并逐步将其完全移除。

路由 CML 语法

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

use 声明包含:

  • event_stream:单个事件名称或事件名称列表。
  • from:capability 的来源。允许的值:父级或子级引用。不允许使用框架或自身中的事件。
  • 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:提供 capability 的子引用。
  • as:capability 的目标名称(重命名)。只有在指定单个事件流名称时,才能提供此参数。
公开
{
    expose: [
        {
            event_stream: "started",
            from: "framework",
            scope: [ "#child", "#other_child" ],
            as: "foo_started"
        },
    ]
}

公开声明包含:

  • event_stream:单个事件名称或事件名称列表。
  • scope:从 framework 公开事件时,此作用域是必需的,并且允许用户定义事件所涉及的子项(或子项数组)。
  • from:该 capability 的一个或多个来源。提供多个来源时,系统会将这些数据流视为合并的。允许的值:父级或子级引用。不允许通过 self 公开事件。
  • as:capability 的目标名称(重命名)。只有在指定单个事件流名称时,才能提供此参数。

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 方法。
  • 自今天起,我们将路由 events,并希望路由 event streams,后者可在路由中合并。
  • 从“根上方”向根提供有关整个拓扑的事件,并在事件路由声明中实现 scope 关键字,以允许从拓扑中的子树获取事件。
  • 公开事件。在撰写本文时,我们仅支持 offeruse
  • 在源头,事件现在是指单个组件,但从“根之上”提供给根令牌网格的事件除外。在路由时,事件可以汇总,以便引用多个组件。

性能

事件系统基于组件管理器实现的现有内部钩子系统构建,因此将事件分派给对其感兴趣的其他组件的影响可以忽略不计。事实上,性能得到了提升,因为现在,如果某个组件仅对单个组件(而非整个子树)中的事件感兴趣,则可以仅为该组件(而非整个子树)分派事件,从而减少涉及的系统调用次数。

工效学设计

此 RFC 改进了事件的人体工学:

  • 如果某个组件对单个组件事件感兴趣,现在可以使用 CML 表达该事件,而无需使用代码进行过滤。
  • 现在,所有组件都将 EventSource 协议作为内置框架功能提供,并且无需明确路由该协议。
  • 只有一项 capability 会路由 event stream,而不是像现在这样路由 events 并使用 event streams
  • Event streams 可以在路由时合并,从而减少开发者需要在 CML 中编写的路由语句数量。

向后兼容性

此更改不会破坏兼容性。组件管理器级别(以及根、核心、引导境界)会有树内琐碎的重构,但客户端组件中不会。此外,由于事件现在只会与单个组件相关,因此目前使用事件(全部在树中)的测试将进行重构。

安全性和隐私权注意事项

此提案保留了 2020 年安全和隐私权审核中讨论的安全属性。以前,由于事件涉及整个子树,因此 EventSource 协议会被显式路由。现在,此协议是一种内置功能,事件与特定组件相关。系统会明确路由与整个子树相关的事件,并且可以执行静态验证,以确保非特权组件不会收到这些事件。

测试

所有事件功能和语义都必须在组件管理器中进行集成测试。

文档

对文档的更新必须在事件功能组件清单页面中进行。

缺点、替代方案和未知情况

从组件向框架提供 EventSink 功能

根据这一理念,组件会向框架公开协议 RealmEventSink。然后,该框架会打开此 capability,并在事件发生时将事件推送到其中。此提案缺少定义组件将从哪个作用域获取事件的方法,并且没有明确的方法来限制此类作用域,也无法静态验证组件是否未从其不应具有可见性的拓扑结构部分获取事件。在当前设计中,可以静态地表达获取与特定组件集(适用于常规用例)或拓扑中的整个子树(适用于生产环境和测试中的诊断用例)相关的事件。

用于公开和提供路由声明的 self 的来源

self 中的 expose/offer 事件会保留为一个开放区域,以便日后设计组件可能会公开和自行调度的自定义事件。

在先技术和参考文档

如需了解事件的当前状态,请参阅事件功能Events FIDL 定义