| RFC-0121:组件生命周期事件 | |
|---|---|
| 状态 | 已接受 |
| 区域 |
|
| 说明 | 此 RFC 正式确定了组件框架的事件功能。 |
| 问题 | |
| Gerrit 更改 | |
| 作者 | |
| 审核人 | |
| 提交日期(年-月-日) | 2021-05-26 |
| 审核日期(年-月-日) | 2021-08-11 |
摘要
本文档记录了有关组件事件的设计讨论、原则和决策:概念、清单语法和 FIDL 协议。
下文介绍的许多决策都是在 2020 年初制定和实施的。还提出了一些其他概念,以解决早期决策中的缺点和复杂性。我们建议将许多功能列入许可名单,以防止在现有使用场景之外使用这些功能,同时努力将这些功能迁移到更好的机制。
本文档的范围涵盖以下组件框架 API:
- CML:用于编写组件清单的语言
fuchsia.sys2/events.fidl:当前 API 界面,Component Events FIDL API 位于其中。目标是将所有事件 API 升级到fuchsia.component/events.fidl,并在 SDK 中提供这些 API。
设计初衷
组件管理器在内部处理组件的生命周期,不会向组件公开该信息。某些特权客户端(例如:诊断、测试、组件主管、调试器)需要更深入地了解组件的生命周期才能执行其工作。
组件事件的推出旨在解决向这些特权组件公开此类信息的问题。当组件实例发生生命周期转换时,组件管理器会调度所有这些事件。功能路由 API 在本文档中提及的某些位置保持开放,以便进一步设计,从而允许组件自行公开和提供自定义事件。
设计
本部分概述了组件事件的当前设计以及 API 修订,重点介绍了当前设计不适合作为长期设计的情况,并列出了未来可能设计和实现的功能。
组件事件流功能
组件事件以“事件流”功能的形式建模。每个事件流功能都指向拓扑中的单个组件或组件子树。在撰写本文档时,事件被视为单独的事件功能(而非事件流)。当生命周期发生转换时,组件管理器会代表组件发出事件。
与任何功能一样,事件流也可以进行路由。当事件流为以下情况时:
由框架公开/提供:事件是指公开或提供事件的组件。
用于父级/子级:组件可以监听从子级或父级路由到它的事件。组件的父级可以直接使用子级的生命周期事件流,而无需子级明确地从框架中公开事件。
来自根以上的事件流
根 realm 可以使用/提供来自“父级”的事件流。您可能想知道,如果根 realm 应该是拓扑的根,那么它的父级是什么?在组件管理器术语中,组件管理器向根 realm 提供的 capability 被称为来自“根以上”。
从“根以上”到根 realm 提供的事件流的范围是整个组件实例拓扑。这些事件流在路由时可以缩小范围,以引用拓扑的子树。这与目录功能可以作为整体或作为子目录进行路由的方式类似(目录的路由声明中的 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",
}
]
}
在此示例中:
foo可以获取bar的started事件,前提是test-12345将事件路由到foo。archivist可以获取tests下所有组件的started事件,前提是started是从test_manager提供的,而test_manager是从core获取的,core是从root获取的,root是从above root获取的,并且在传递过程中进行了降级。test-12345(测试根)可以获取其下所有组件的启动事件,原因与archivist相同。不过,与可以从tests获取所有事件的归档器不同,它只能获取与测试相关的事件,因为tests将test-12345的事件范围缩小了。
此示例展示了目前事件的一项核心使用情形。归档程序能够以隔离的方式观察每个测试内部发生的情况。此外,每个测试都可以获取有关测试下所有组件或特定组件的事件。
合并事件流
同一类型的多个事件流可以合并为一个流。
例如,在上面的示例中,#test-12345 可以将 foo 和 bar 中的 stopped 作为一项功能提供给其他组件。然后,该组件将同时获得 foo 和 bar 的 stopped 事件。
// core.cml
offer: [
{
event_stream: "stopped",
from: [ "#netstack", "#supervisor" ],
to: "#someone",
}
]
目前不允许自行公开/提供活动。不过,这里还有改进空间,可以允许活动公开/提供它们自己调度的自定义事件。
活动模式
在撰写本文档时,事件可以异步或同步方式提取。目的是仅使用异步事件,并完全弃用同步事件。
同步使用事件可让订阅者在处理事件时阻止组件管理器,然后在处理完毕后恢复。此功能最初是为测试而引入的,同时也是为了方便调试器。
自那时起,我们了解到,使用同步事件的测试通常可以使用异步事件来编写。因此,我们一直在努力完全消除同步事件,但在组件管理器内部测试中仍有少量用途。我们认为同步事件在调试器(例如 step 或 zxdb)中会很有用,但届时我们会设计出能够准确满足调试器需求的解决方案。
该提案旨在将使用同步事件的测试列入许可名单,并努力完全消除同步事件。
活动类型
在撰写本文档时,我们有两类事件:
- 生命周期事件。这些事件反映了组件实例的生命周期变化,由管理此类信息的组件管理器发出。
- 已弃用的事件。这些事件并不反映组件实例生命周期的变化,我们正在努力完全移除它们,以便采用更合适的解决方案。
我们有以下生命周期事件类型:
Discovered:这是组件生命周期的第一个阶段。 在创建动态子项时、在解析静态子项的父项时以及在组件管理器启动时,为动态子项、静态子项和根项调度。Resolved:实例的声明首次成功解析。Started:根据组件管理器的信息,此实例已启动。 不过,如果这是一个可执行组件,运行程序还需要做更多工作才能启动该组件。组件正在开始运行,但可能尚未开始执行代码。Stopped:实例已成功停止。此事件必须在 Destroyed 之前发生。Destroyed:实例的销毁已开始。实例已在此时间点停止。该实例仍存在于父级领域中,但很快就会被移除。Purged:实例已成功销毁。实例已停止,不再存在于父级 realm 中。
以及以下已弃用的事件类型:
Running:此事件由组件管理器为订阅时已在运行的所有组件合成。此事件派生自started,但适用于在监听器订阅事件之前已启动(且未停止)的组件。最终,我们希望拥有一个组件框架查询 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:目前仅用于DiagnosticsReady和CapabilityRequested。如前所述,这些事件将被完全移除,因此不会提供有关过滤条件的任何详细信息,因为这些信息与非诊断开发者无关。
您也可以使用来自不同来源的事件。在以下示例中,组件将获得一个包含 started 和 stopped 事件的单一事件流(范围与父级提供的范围相同),以及子级 #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,改为将事件流直接放置在传入命名空间中,如上一部分中所述。
更严格的 Event 和 EventResult
在撰写本文时,所有这些数据类型都是表格。不过,这些数据已非常明确,我们预计不会再添加更多数据。为了通过移除对可选字段的处理来提高人体工程学性能,我们将使它们成为 struct 和非灵活的联合:
struct Event {
EventHeader header;
EventResult event_result;
};
union EventResult {
1: EventPayload payload;
2: EventError error;
};
实现
此设计的大部分内容已实现。在 SDK 中公开此 API 之前,我们还需要实现一些功能。具体而言:
- 事件的许可名单:
directory ready、running和capability requested。 - 将静态事件流放置在传入的命名空间中,并移除了
EventStream.TakeStaticEventStream方法。 - 自今天以来,CML 更新了
events,我们希望路由event streams,这可以在路由中合并。 - 提供有关整个拓扑(从“根以上”到根)的事件,并在事件路由声明中实现
scope关键字,以允许从拓扑中的子树获取事件。 - 公开活动。在撰写本文档时,我们仅支持
offer和use。 - 在来源中,事件现在是指单个组件,但从“根以上”提供给根 realm 的事件除外。在路由时,事件可以进行汇总,因此可以指代多个组件。
性能
事件系统基于组件管理器实现的现有内部钩子系统构建,因此将事件分派给对这些事件感兴趣的其他组件的性能影响可以忽略不计。事实上,性能有所提升,因为现在如果组件只对来自单个组件的事件感兴趣,而不是对整个子树感兴趣,那么现在可以只调度该组件的事件,而不是整个子树的事件,从而减少涉及的系统调用次数。
工效学设计
此 RFC 引入了事件人体工程学方面的改进:
- 如果某个组件仅对单个组件事件感兴趣,现在可以在 CML 中表达该意图,而无需使用代码来过滤这些事件。
- 所有组件现在都将
EventSource协议作为内置框架功能提供,并且不需要显式路由。 - 有一个功能是路由
event stream,而不是像现在这样路由events并消耗event streams。 Event streams可在路由时合并,从而减少开发者需要在 CML 中编写的路由语句数量。
向后兼容性
此更改不会破坏兼容性。在组件管理器级别(以及根、核心、启动领域)会有树内简单重构,但在客户端组件中不会有。此外,由于事件现在将仅与单个组件相关,因此使用事件的测试(目前全部在树内)中将进行重构。
安全性和隐私权注意事项
此提案可保持 2020 年安全与隐私权审核中讨论的安全属性。之前,由于事件涉及整个子树,因此 EventSource 协议会被显式路由。现在,此协议已成为内置功能,事件与特定组件相关。有关整个子树的事件会明确路由,并且可以执行静态验证,以确保非特权组件不会收到这些事件。
测试
所有事件功能和语义都必须在组件管理器中进行集成测试。
文档
缺点、替代方案和未知因素
从组件向框架提供 EventSink 功能
根据此想法,组件将向框架公开协议 RealmEventSink。然后,框架会打开此功能,并在事件发生时将事件推送到该功能中。此提案缺少一种定义组件将从哪个范围获取事件的方法,并且没有明确的途径来限制此类范围,也无法静态验证组件是否未从其不应具有可见性的拓扑部分获取事件。在当前设计下,静态地表达获取有关特定组件集(对于常规使用情形)或拓扑中的整个子树(对于生产和测试中的诊断使用情形)的事件是可能的。
用于公开和提供路由声明的自我来源
expose/offer 中的 self 事件被留作开放区域,用于将来设计组件可能公开和自行调度的自定义事件。
在先技术和参考资料
有关事件当前状态的文档可在事件功能和事件 FIDL 定义中找到。