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

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

此 RFC 正式说明了组件框架事件功能。

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

总结

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

下述许多决定都是在 2020 年初制定并实施的。提出了一些其他概念,用于解决早期决策的缺点和复杂性。我们建议将许多功能列入许可名单,以便在我们努力将这些功能迁移到更好的机制期间,防止它们在现有用例之外使用。

本文档介绍了以下组件框架 API:

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

设计初衷

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

引入了组件事件,作为向这些特权组件公开此类信息的解决方案。当组件实例的生命周期转换发生时,组件管理器会分派所有这些事件。功能路由 API 在本文档中提到的一些位置保持打开状态,以进一步设计,从而允许组件自行公开和提供自定义事件。

设计

本部分概述了组件事件的当前设计以及 API 修订版本,重点说明了当前设计不适用于长期设计,并介绍了未来可以设计和实现的功能。

组件事件流功能

系统会基于“事件流”功能对组件事件进行建模。每个事件流功能指的是拓扑中的单个组件或组件子树。在编写本文档时,事件被视为单个事件功能(而非事件流)。当发生生命周期转换时,组件管理器会代表组件发出事件。

与任何功能一样,事件流也可以路由。如果事件流是:

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

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

来自上述根目录的事件流

事件流可由来自“父级”的根领域使用/提供。您可能想知道,如果根领域应该作为拓扑的根,那么其父领域是什么?在组件管理器术语中,组件管理器向根领域提供的功能称为“来自根领域”。

从“在根以上”到根领域提供的事件流的范围限定为整个组件实例拓扑。这些事件流可以缩小范围,在进行路由时引用拓扑的子树。这类似于目录功能可作为一个整体进行路由或作为子目录(目录的路由声明中的 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 可以获取 barstarted 事件,前提是 test-12345 将其路由到它。

  • archivist 可以获取 tests 下所有组件的 started 事件,前提是它之前从 test_manager 提供的 startedcore 获取,而又从 rootabove root 获取事件,并且已在此过程中缩小范围。

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

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

合并事件流

相同类型的事件流可以合并为单个流。例如,在上面的示例中,#test-12345 可以将 foobar 中的 stopped 作为单一功能提供给其他一些组件。然后,该组件将获取 foobarstopped 事件。

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

目前不允许公开/提供自己遇到的事件。不过,为了让事件公开/提供它们自行分派的自定义事件,这方面还有增长空间。

事件模式

在编写本文档时,可以异步或同步方式提取事件。这样做的目的是为了保留异步事件,并彻底弃用同步事件。

以同步方式消耗事件可让订阅者在处理事件时屏蔽组件管理器,然后恢复。此版本最初是为测试而引入的,并且还考虑了调试程序。

从那时起,我们便知道通常可以使用异步事件编写使用同步事件的测试。因此,您需要完成一些工作来彻底消除同步事件,但组件管理器内部测试中仍有一些用途。大家认为同步事件在调试程序(如 step 或 zxdb)中很有用,但那时候,我们将发明一个可准确满足调试程序需求的解决方案。

具体方案是将使用同步事件的测试列入许可名单,并努力完全消除同步事件。

事件类型

在编写本文档时,我们有两类事件:

  • 生命周期事件。这些事件反映了组件实例生命周期中的变化,并由管理此类信息的组件管理器发出。
  • 已弃用的事件。这些事件并不反映组件实例生命周期的变化,我们正在努力将其完全移除,以采用更合适的解决方案。

我们有以下生命周期事件类型:

  • Discovered:这是组件生命周期的第一阶段。在创建动态子项时分派;当静态子项的父项得到解析时,为静态子项分派;当组件管理器启动时,为根子项分派。
  • Resolved:实例的声明首次已成功解析。
  • Started:根据组件管理器,此实例已启动。 不过,如果这是一个可执行组件,运行程序还需要执行进一步操作才能启动该组件。组件已开始运行,但可能尚未开始执行代码。
  • Stopped:实例已成功停止。此事件必须在销毁之前发生。
  • Destroyed:已开始销毁实例。此时,该实例已停止。该实例仍然存在于父级的领域,但很快就会被移除。
  • Purged:实例已成功销毁。该实例会停止,并且不再存在于父级的领域。

以及以下已废弃的事件类型:

  • Running:此事件由组件管理器针对订阅时已在运行的所有组件合成。此事件派生自 started,但适用于在监听器订阅事件之前启动(且未停止)的组件。最终,我们希望通过 Component Framework Query API 了解正在运行的内容。因此,我们计划将此事件添加到其唯一客户端(即 Archivist)的许可名单,并在以后使用新的 API 并移除 running

  • Capability Routed:此事件是为在测试中使用而引入的。我们最近已将其从使用该功能的测试中彻底移除,并且正在着手彻底将其移除。

  • Capability Requested:此事件作为临时解决方案引入,用于为 fuchsia.logger/LogSink 连接提供组件归因。从那以后,它也用于提供对 fuchsia.debugdata/Publisher 连接的归因。这从来都不是长期的解决方案。由于事件系统仅适用于特权组件,因此使用组件事件构建此功能是一种只需投入少量资金的方法。人们理解,如果用例不断增加,就需要开发更加标准化的解决方案了。目前的计划是彻底移除此事件,同时将其列入许可名单到其仅有的客户端:Archivist、调试数据和测试管理器(针对调试数据)。

  • Directory Ready:此事件作为一种解决方案引入,用于向 Archivist 提供组件公开的 out/diagnostics 目录,用于检查数据汇总。诊断团队计划设计 VMO 支持的日志。由于日志需要在组件开始提供诊断目录之前可用,因此这种方法已作废。我们将设计一个新解决方案,用于向 Archivist 提供检查和日志 VMO,以便保证日志在组件启动其异步循环之前就可用。目前的建议是将此事件添加到其唯一用户(归档者)的许可名单中,并努力将其彻底移除。

路由 CML 语法

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

use 声明包含:

  • 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:当从框架提供事件时,该作用域允许定义事件的相关子项(或子项数组)。如果未指定范围,则范围为自身,这意味着事件与组件本身有关。从父级提供事件时,该范围允许将事件范围缩小到子级范围。
  • from:功能的一个或多个来源。当给定多个源时,流会被视为合并。允许使用的值:框架引用、父级引用或子引用。不允许自己提供。
  • to:提供 capability 的子项引用。
  • as:功能的目标名称(重命名)。只有在指定了单个事件流名称时,才能提供该名称。
公开
{
    expose: [
        {
            event_stream: "started",
            from: "framework",
            scope: [ "#child", "#other_child" ],
            as: "foo_started"
        },
    ]
}

公开声明包含:

  • event_stream:单个事件名称或事件名称列表。
  • scope:当从 framework 公开事件时,必须提供作用域,并允许定义事件的相关子项(或子项数组)。
  • from:功能的一个或多个来源。当给定多个源时,流会被视为合并。允许使用的值:父级引用或子引用。不允许公开来自 self 的事件。
  • 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 方法。
  • 自今天起,我们路由 events,并希望路由可以合并到路由中的 event streams 的 CML 更新。
  • 提供与整个拓扑相关的事件(从“在根以上”到根),并在事件路由声明中实现 scope 关键字,以允许具有来自拓扑中子树的事件。
  • 公开事件。在编写本文档时,我们仅支持 offeruse
  • 在最初,事件现在会引用单个组件,但从“在根之上”提供给根大区的事件除外。在路由时,事件可以汇总,以便它们可以引用多个组件。

性能

事件系统建立在组件管理器实现的现有内部钩子系统之上,因此将事件分派给对它们感兴趣的其他组件对性能的影响可以忽略不计。事实上,现在如果某个组件只关注来自单个组件(而非整个子树)的事件,则性能有所提升。现在,您可以仅分派该组件(而非整个子树)的事件,从而减少涉及的系统调用数量。

工效学设计

此 RFC 改进了事件的工效学设计:

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

向后兼容性

此更改不会破坏兼容性。系统会在组件管理器级别(以及根、核心和引导领域)进行树内细微重构,但不会在客户端组件中进行重构。此外,由于事件现在是关于单个组件的测试,因此将在目前使用事件的测试(全部在树内)进行重构。

安全性和隐私权注意事项

此方案维护着 2020 年一次安全和隐私权审查所讨论的安全属性。以前,由于事件与整个子树相关,因此 EventSource 协议是显式路由的。现在,此协议是一项内置 capability,而事件与特定组件相关。系统会明确路由有关整个子树的事件,并且可以执行静态验证以确保非特权组件不会获取这些事件。

测试

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

文档

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

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

提供从组件到框架的 EventSink 功能

按照这种想法,组件会向框架公开协议 RealmEventSink。然后,框架会打开此功能,并在事件发生时将事件推送到其中。该方案缺乏定义组件将从哪个作用域获取事件的方法,也没有明确的路径来限制此类作用域,也无法静态验证组件是否从其无法查看的拓扑部分获取事件。在当前设计下,可以静态方式表示获取拓扑中一组特定组件(针对常规用例)或整个子树(针对生产和测试中的诊断用例)的相关事件。

公开和提供路由声明的自身来源

self 中的 expose/offer 事件留作开放区域,供组件未来对自定义事件进行公开和分派。

早期技术和参考资料

您可以在事件功能事件 FIDL 定义中找到有关事件当前状态的文档。